diff --git a/.agents/skills/user-story-mapping/SKILL.md b/.agents/skills/user-story-mapping/SKILL.md new file mode 100644 index 0000000..026aa57 --- /dev/null +++ b/.agents/skills/user-story-mapping/SKILL.md @@ -0,0 +1,297 @@ +--- +name: user-story-mapping +argument-hint: "[product or workflow]" +description: Create a user story map that lays out activities, steps, tasks, and release slices. Use when planning a workflow, backlog, or MVP around the user journey. +intent: >- + Visualize the user journey by creating a hierarchical map that breaks down high-level activities into steps and tasks, organized left-to-right as a narrative flow. Use this to build shared understanding across product, design, and engineering, prioritize features based on user workflows, and identify gaps or opportunities in the user experience. +type: component +--- + + +## Purpose +Visualize the user journey by creating a hierarchical map that breaks down high-level activities into steps and tasks, organized left-to-right as a narrative flow. Use this to build shared understanding across product, design, and engineering, prioritize features based on user workflows, and identify gaps or opportunities in the user experience. + +This is not a backlog—it's a strategic artifact that shows *how* users accomplish their goals, which then informs *what* to build. + +## Input + +**Works best with:** The product or user workflow being mapped. +**Also useful:** The primary user, the end-to-end narrative as you understand it, existing backlog items to place, and release goals. + +Anything supplied with the invocation itself — text after the skill name, a pasted context dump, or an appended `ARGUMENTS:` line — counts as answers already given. Use it and skip whatever it covers; don't re-ask. + +**Arriving empty-handed? That works too.** The skill asks whose journey you're mapping and what they're trying to get done, then builds backbone → tasks → slices. + +**Example invocation:** `Story map for our expense-reporting flow, from receipt capture to reimbursement, with an MVP slice for the pilot.` + +## Key Concepts + +### The Jeff Patton Story Mapping Framework +Invented by Jeff Patton, story mapping organizes work into a 2D structure: + +**Horizontal axis (left-to-right):** User journey over time +- **Backbone:** High-level activities the user performs +- **Steps:** Specific actions within each activity +- **Tasks:** Detailed work required to complete each step + +**Vertical axis (top-to-bottom):** Priority and releases +- **Top rows:** Essential tasks (MVP / Release 1) +- **Lower rows:** Nice-to-have tasks (Future releases) + +### Story Map Structure + +``` +Segment → Persona → Narrative (User's goal) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[Activity 1] → [Activity 2] → [Activity 3] → [Activity 4] → [Activity 5] + ↓ ↓ ↓ ↓ ↓ + [Step 1.1] [Step 2.1] [Step 3.1] [Step 4.1] [Step 5.1] + [Step 1.2] [Step 2.2] [Step 3.2] [Step 4.2] [Step 5.2] + [Step 1.3] [Step 2.3] [Step 3.3] [Step 4.3] [Step 5.3] + ↓ ↓ ↓ ↓ ↓ + [Task 1.1.1] [Task 2.1.1] [Task 3.1.1] [Task 4.1.1] [Task 5.1.1] + [Task 1.1.2] [Task 2.1.2] [Task 3.1.2] [Task 4.1.2] [Task 5.1.2] + [Task 1.1.3] [Task 2.1.3] [Task 3.1.3] [Task 4.1.3] [Task 5.1.3] + ... ... ... ... ... +``` + +### Why This Works +- **User-centric:** Organizes work around user goals, not engineering modules +- **Shared understanding:** Product, design, engineering all see the same journey +- **Prioritization clarity:** Top tasks = MVP, lower tasks = future iterations +- **Gap identification:** Missing steps or tasks become obvious +- **Release planning:** Draw horizontal "release lines" to define scope + +### Anti-Patterns (What This Is NOT) +- **Not a Gantt chart:** This isn't project management—it's user journey visualization +- **Not a feature list:** Activities aren't features—they're user behaviors +- **Not static:** Story maps evolve as you learn more about users + +### When to Use This +- Kicking off a new product or major feature +- Aligning stakeholders on user workflow +- Prioritizing backlog based on user needs +- Identifying MVP vs. future releases +- Onboarding new team members to the product vision + +### When NOT to Use This +- For trivial features (don't map what you already understand) +- When user workflows are constantly changing (map stabilizes workflows) +- As a replacement for user stories (the map informs stories, doesn't replace them) + +--- + +## Application + +### Step 1: Define the Context + +Use `template.md` for the full fill-in structure. + +#### Segment +Who are you building for? + +```markdown +### Segment: +- [Specify the target segment, e.g., "Small business owners using DIY accounting software"] +``` + +**Quality checks:** +- **Specific:** Not "users" but "enterprise IT admins" or "freelance designers" + +--- + +#### Persona +Provide details about the persona within this segment (reference `skills/proto-persona/SKILL.md`). + +```markdown +### Persona: +- [Describe the persona: demographics, behaviors, pains, goals] +``` + +**Example:** +- "Sarah, 35-year-old freelance graphic designer, manages 5-10 client projects at once, struggles with invoicing and payment tracking, wants to spend less time on admin and more time designing" + +--- + +### Step 2: Define the Narrative +What is the user trying to accomplish? Frame this as a Jobs-to-be-Done statement (reference `skills/jobs-to-be-done/SKILL.md`). + +```markdown +### Narrative: +- [Concise narrative of the persona's objective, e.g., "Complete a client project from kickoff to final payment"] +``` + +**Quality checks:** +- **Outcome-focused:** Not "use the product" but "deliver a client project on time and get paid" +- **One sentence:** If it takes more than one sentence, the scope may be too broad + +--- + +### Step 3: Identify Activities (Backbone) +List 3-5 high-level activities the persona engages in to fulfill the narrative. These form the backbone of your map. + +```markdown +### Activities: +1. [Activity 1, e.g., "Negotiate project scope and pricing"] +2. [Activity 2, e.g., "Execute design work"] +3. [Activity 3, e.g., "Deliver final assets to client"] +4. [Activity 4, e.g., "Send invoice and receive payment"] +5. [Activity 5, optional] +``` + +**Quality checks:** +- **Sequential:** Activities happen in order (left-to-right) +- **User actions:** Describe what the user *does*, not what the product *provides* +- **3-5 activities:** Too few = oversimplified, too many = overwhelming + +--- + +### Step 4: Break Activities into Steps +For each activity, list 3-5 steps that detail how the activity is carried out. + +```markdown +### Steps: + +**For Activity 1: [Activity Name]** +- Step 1: [Detail step 1, e.g., "Review client brief"] +- Step 2: [Detail step 2, e.g., "Draft project proposal"] +- Step 3: [Detail step 3, e.g., "Negotiate timeline and budget"] +- Step 4: [Optional step 4] +- Step 5: [Optional step 5] + +**For Activity 2: [Activity Name]** +- Step 1: [Detail step 1] +- Step 2: [Detail step 2] +... +``` + +**Quality checks:** +- **Actionable:** Each step is something the user does +- **Observable:** You could watch someone perform this step +- **Logical sequence:** Steps follow a natural order + +--- + +### Step 5: Break Steps into Tasks +For each step, list 5-7 tasks that must be completed. + +```markdown +### Tasks: + +**For Activity 1, Step 1: [Step Name]** +- Task 1: [Detail task 1, e.g., "Read client brief document"] +- Task 2: [Detail task 2, e.g., "Identify key deliverables"] +- Task 3: [Detail task 3, e.g., "Note budget constraints"] +- Task 4: [Detail task 4, e.g., "Clarify timeline expectations"] +- Task 5: [Detail task 5, e.g., "List open questions for client"] +- Task 6: [Optional task 6] +- Task 7: [Optional task 7] + +**For Activity 1, Step 2: [Step Name]** +- Task 1: [Detail task 1] +... +``` + +**Quality checks:** +- **Granular:** Tasks are small, specific actions +- **User-facing or behind-the-scenes:** Include both (e.g., "Send email" and "Receive confirmation") +- **Prioritizable:** You'll prioritize tasks vertically (top = essential, bottom = nice-to-have) + +--- + +### Step 6: Prioritize Vertically +Arrange tasks top-to-bottom by priority: +- **Top rows:** MVP / Release 1 (must-have) +- **Middle rows:** Release 2 (important but not critical) +- **Bottom rows:** Future / Nice-to-have + +Draw horizontal "release lines" to demarcate scope. + +--- + +### Step 7: Identify Gaps and Opportunities +Review the map and ask: +- Are there missing steps or tasks? +- Are there pain points we're not addressing? +- Are there opportunities to delight users? +- Do all activities flow logically? + +--- + +## Examples + +See `examples/sample.md` for a full story map example. + +--- + +## Common Pitfalls + +### Pitfall 1: Activities Are Features, Not User Behaviors +**Symptom:** "Activity 1: Use the dashboard. Activity 2: Generate reports." + +**Consequence:** You've mapped the product, not the user journey. + +**Fix:** Reframe as user actions: "Activity 1: Monitor project progress. Activity 2: Summarize work for stakeholders." + +--- + +### Pitfall 2: Too Many Activities +**Symptom:** 10+ activities across the backbone + +**Consequence:** Map becomes overwhelming and loses focus. + +**Fix:** Consolidate. If you have 10 activities, you're likely mixing activities with steps. Aim for 3-5 high-level activities. + +--- + +### Pitfall 3: Tasks Are Too Vague +**Symptom:** "Task 1: Do the thing" + +**Consequence:** Can't prioritize or estimate vague tasks. + +**Fix:** Be specific: "Task 1: Enter client email address in the 'Bill To' field." + +--- + +### Pitfall 4: Ignoring Vertical Prioritization +**Symptom:** All tasks at the same level—no MVP vs. future releases defined + +**Consequence:** No clarity on what to build first. + +**Fix:** Explicitly prioritize. Draw release lines. Force hard choices about what's MVP. + +--- + +### Pitfall 5: Mapping in Isolation +**Symptom:** PM creates the map alone, then presents it to the team + +**Consequence:** No shared ownership or understanding. + +**Fix:** Map collaboratively. Run a story mapping workshop with product, design, and engineering. + +--- + +## References + +### Related Skills +- `skills/proto-persona/SKILL.md` — Defines the persona for the story map +- `skills/jobs-to-be-done/SKILL.md` — Informs the narrative and activities +- `skills/user-story/SKILL.md` — Tasks from the map become user stories +- `skills/problem-statement/SKILL.md` — Problem statement frames the narrative + +### External Frameworks +- Jeff Patton, *User Story Mapping* (2014) — Origin of the story mapping technique +- Teresa Torres, *Continuous Discovery Habits* (2021) — Opportunity solution trees (complementary to story maps) + +### Dean's Work +- User Story Mapping Prompt (adapted from Jeff Patton's methodology) + +### Provenance +- Adapted from `prompts/user-story-mapping.md` in the `https://github.com/deanpeters/product-manager-prompts` repo. + +--- + +**Skill type:** Component +**Suggested filename:** `user-story-mapping.md` +**Suggested placement:** `/skills/components/` +**Dependencies:** References `skills/proto-persona/SKILL.md`, `skills/jobs-to-be-done/SKILL.md`, `skills/user-story/SKILL.md`, `skills/problem-statement/SKILL.md` diff --git a/.agents/skills/user-story-mapping/examples/sample.md b/.agents/skills/user-story-mapping/examples/sample.md new file mode 100644 index 0000000..06213e3 --- /dev/null +++ b/.agents/skills/user-story-mapping/examples/sample.md @@ -0,0 +1,77 @@ +# User Story Mapping Example + +## Example: Story Map for Freelance Invoicing Product + +```markdown +## User Story Map: Freelance Invoicing + +### Who + +#### Segment: +- Freelance creative professionals (designers, writers, photographers) + +#### Persona: +- Sarah, 35, freelance graphic designer +- Manages 5-10 clients at once +- Struggles with invoicing, payment tracking, and follow-ups +- Wants to spend less time on admin, more time designing +- Currently uses Excel + email, which is error-prone and time-consuming + +### Backbone + +#### Narrative: +- Complete a client project from kickoff to final payment without admin hassle + +#### Activities: +1. Negotiate project scope and pricing +2. Execute design work +3. Deliver final assets +4. Send invoice and receive payment +5. Follow up on late payments + +### Steps: + +**For Activity 1: Negotiate project scope and pricing** +- Step 1: Review client brief +- Step 2: Draft project proposal +- Step 3: Negotiate timeline and budget + +**For Activity 2: Execute design work** +- Step 1: Create initial concepts +- Step 2: Share concepts for feedback +- Step 3: Iterate based on feedback +- Step 4: Finalize design + +**For Activity 3: Deliver final assets** +- Step 1: Export final files in client-requested formats +- Step 2: Upload files to shared folder or email +- Step 3: Confirm client receipt + +**For Activity 4: Send invoice and receive payment** +- Step 1: Create invoice with project details +- Step 2: Send invoice to client +- Step 3: Track payment status +- Step 4: Confirm payment received + +**For Activity 5: Follow up on late payments** +- Step 1: Identify overdue invoices +- Step 2: Send payment reminder +- Step 3: Escalate if still unpaid + +### Tasks (Sample for Activity 4, Step 1: Create invoice): + +**MVP (Release 1):** +- Task 1: Enter client name and contact info +- Task 2: Add line items (description, hours, rate) +- Task 3: Calculate total automatically +- Task 4: Preview invoice before sending + +**Release 2:** +- Task 5: Add logo and custom branding +- Task 6: Save invoice templates for repeat clients +- Task 7: Auto-populate line items from project notes + +**Future:** +- Task 8: Generate invoices from time tracking data +- Task 9: Multi-currency support +``` diff --git a/.agents/skills/user-story-mapping/template.md b/.agents/skills/user-story-mapping/template.md new file mode 100644 index 0000000..7029373 --- /dev/null +++ b/.agents/skills/user-story-mapping/template.md @@ -0,0 +1,41 @@ +# User Story Map Template + +Use this template to create a complete user story map (segment, persona, backbone, steps, and tasks). + +## Provenance +Adapted from `prompts/user-story-mapping.md` in the `https://github.com/deanpeters/product-manager-prompts` repo. + +## Template +```markdown +## User Story Map Template + +### Who + +#### Segment: +- [Specify the target segment] + +#### Persona: +- [Describe the persona and their key characteristics] + +### Backbone + +#### Narrative: +- [Insert the concise narrative of the persona's objective] + +#### Activities: +1. [Describe Activity 1] +2. [Describe Activity 2] +3. [Continue as necessary for up to 5 activities] + +#### Steps: +For [Activity 1]: +- Step 1: [Detail Step 1 for Activity 1] +- Step 2: [Detail Step 2 for Activity 1] +- Step 3: [Detail Step 3 for Activity 1] + +#### Tasks: +For [Activity 1, Step 1]: +- Task 1: [Detail Task 1 for Step 1 of Activity 1] +- Task 2: [Detail Task 2 for Step 1 of Activity 1] +- Task 3: [Detail Task 3 for Step 1 of Activity 1] +``` diff --git a/.axhub/make/sidebar-tree.json b/.axhub/make/sidebar-tree.json index 3df5294..05b4337 100644 --- a/.axhub/make/sidebar-tree.json +++ b/.axhub/make/sidebar-tree.json @@ -1,79 +1,18 @@ { "version": 1, - "updatedAt": "2026-07-20T07:33:31.296Z", + "updatedAt": "2026-07-21T08:13:15.367Z", "prototypes": [ - { - "id": "item:prototypes:oneos-prototype-demo", - "kind": "item", - "title": "原型演示", - "itemKey": "prototypes/oneos-prototype-demo" - }, - { - "id": "folder-1783875936186-27raza", - "kind": "folder", - "title": "旧ONEOS", - "children": [ - { - "id": "item:prototypes:oneos-web-business", - "kind": "item", - "title": "业务管理", - "itemKey": "prototypes/oneos-web-business" - }, - { - "id": "item:prototypes:oneos-web-data-analysis", - "kind": "item", - "title": "数据分析", - "itemKey": "prototypes/oneos-web-data-analysis" - }, - { - "id": "item:prototypes:oneos-web-finance", - "kind": "item", - "title": "财务管理(合包)", - "itemKey": "prototypes/oneos-web-finance" - }, - { - "id": "item:prototypes:oneos-web-help-center", - "kind": "item", - "title": "帮助中心", - "itemKey": "prototypes/oneos-web-help-center" - }, - { - "id": "item:prototypes:oneos-web-lease-contract", - "kind": "item", - "title": "车辆租赁合同", - "itemKey": "prototypes/oneos-web-lease-contract" - }, - { - "id": "item:prototypes:oneos-web-ledger-data", - "kind": "item", - "title": "台账数据", - "itemKey": "prototypes/oneos-web-ledger-data" - }, - { - "id": "item:prototypes:oneos-web-ops", - "kind": "item", - "title": "运维管理", - "itemKey": "prototypes/oneos-web-ops" - }, - { - "id": "item:prototypes:oneos-web-procurement", - "kind": "item", - "title": "采购管理", - "itemKey": "prototypes/oneos-web-procurement" - } - ] - }, - { - "id": "item:prototypes:oneos-prototype-nav", - "kind": "item", - "title": "原型导航", - "itemKey": "prototypes/oneos-prototype-nav" - }, { "id": "folder-1782874576229-r8atbu", "kind": "folder", "title": "OneOS", "children": [ + { + "id": "item:prototypes:oneos-prototype-demo", + "kind": "item", + "title": "原型演示", + "itemKey": "prototypes/oneos-prototype-demo" + }, { "id": "item:prototypes:oneos-web-workbench-new", "kind": "item", @@ -185,12 +124,6 @@ "title": "站点信息", "itemKey": "prototypes/oneos-web-h2-station-site" }, - { - "id": "item:prototypes:oneos-h5-h2-order", - "kind": "item", - "title": "加氢记录(H5)", - "itemKey": "prototypes/oneos-h5-h2-order" - }, { "id": "item:prototypes:oneos-web-h2-station", "kind": "item", @@ -204,9 +137,9 @@ "itemKey": "prototypes/oneos-web-h2-station-weekly" }, { - "id": "item-prototypes-oneos-web-h2-station-analysis", + "id": "item:prototypes:oneos-web-h2-station-analysis", "kind": "item", - "title": "oneos web h2 station analysis", + "title": "加氢站分析", "itemKey": "prototypes/oneos-web-h2-station-analysis" } ] @@ -328,8 +261,94 @@ "kind": "item", "title": "业务条线说明", "itemKey": "prototypes/lease-business-line-overview" + }, + { + "id": "item:prototypes:yunxiao-pipeline-handbook", + "kind": "item", + "title": "云效生产线手册", + "itemKey": "prototypes/yunxiao-pipeline-handbook" } ] + }, + { + "id": "folder-1784563868850-xi0guc", + "kind": "folder", + "title": "小羚羚", + "children": [ + { + "id": "item:prototypes:oneos-h5-vehicle-assets", + "kind": "item", + "title": "车辆资产(H5)", + "itemKey": "prototypes/oneos-h5-vehicle-assets" + }, + { + "id": "item:prototypes:oneos-h5-h2-order", + "kind": "item", + "title": "加氢记录(H5)", + "itemKey": "prototypes/oneos-h5-h2-order" + } + ] + }, + { + "id": "folder-1783875936186-27raza", + "kind": "folder", + "title": "旧ONEOS", + "children": [ + { + "id": "item:prototypes:oneos-web-business", + "kind": "item", + "title": "业务管理", + "itemKey": "prototypes/oneos-web-business" + }, + { + "id": "item:prototypes:oneos-web-data-analysis", + "kind": "item", + "title": "数据分析", + "itemKey": "prototypes/oneos-web-data-analysis" + }, + { + "id": "item:prototypes:oneos-web-finance", + "kind": "item", + "title": "财务管理(合包)", + "itemKey": "prototypes/oneos-web-finance" + }, + { + "id": "item:prototypes:oneos-web-help-center", + "kind": "item", + "title": "帮助中心", + "itemKey": "prototypes/oneos-web-help-center" + }, + { + "id": "item:prototypes:oneos-web-lease-contract", + "kind": "item", + "title": "车辆租赁合同", + "itemKey": "prototypes/oneos-web-lease-contract" + }, + { + "id": "item:prototypes:oneos-web-ledger-data", + "kind": "item", + "title": "台账数据", + "itemKey": "prototypes/oneos-web-ledger-data" + }, + { + "id": "item:prototypes:oneos-web-ops", + "kind": "item", + "title": "运维管理", + "itemKey": "prototypes/oneos-web-ops" + }, + { + "id": "item:prototypes:oneos-web-procurement", + "kind": "item", + "title": "采购管理", + "itemKey": "prototypes/oneos-web-procurement" + } + ] + }, + { + "id": "item:prototypes:oneos-prototype-nav", + "kind": "item", + "title": "原型导航", + "itemKey": "prototypes/oneos-prototype-nav" } ], "docs": [], @@ -1116,4 +1135,4 @@ "templates": [], "components": [], "canvas": [] -} +} \ No newline at end of file diff --git a/.claude/skills/AutoVUL/SKILL.md b/.claude/skills/AutoVUL/SKILL.md new file mode 100644 index 0000000..41c2365 --- /dev/null +++ b/.claude/skills/AutoVUL/SKILL.md @@ -0,0 +1,139 @@ +--- +name: AutoVUL +description: >- + Generates OneOS PC version update logs from a Yunxiao (云效) iteration name by + loading all linked requirements, or from a pasted changelog list. Use when testers + or PMs say AutoVUL, 版本更新日志, 按迭代生成更新日志, 发版公告, or give a 迭代名称. +--- + +# AutoVUL(OneOS PC 版本更新日志) + +面向**测试人员发版**:输入云效**迭代名称** → 拉取该迭代关联需求 → 自动生成对外版本更新日志。 + +也可手动粘贴变更清单(兜底)。**预计维护时长**由人工单独通知,不写进日志。 + +## 何时使用 + +- 测试说:`按 $AutoVUL` / `生成版本更新日志` / `按迭代 xxx 出更新日志` +- 输入含云效「迭代名称」 +- 发版前需要统一文案口径 + +## 主流程(测试 · 按迭代,默认) + +完整取数步骤见 [references/yunxiao-sprint.md](references/yunxiao-sprint.md)。 + +```text +1. 确认云效项目(缺省:统一运营管理平台PC端;可改) +2. 向用户索取「迭代名称」(若口令里已带则直接用) +3. 在云效查找该迭代 → 立即反馈成功或失败(见下) +4. 成功:列出迭代内关联「需求」清单(编号 + 标题 + 状态)供用户快速核对 +5. 从每条需求提取变更素材(优先级见「素材优先级」) +6. 按本文格式分类、合并、润色 → 输出成稿正文 +``` + +### 迭代读取 · 成功 / 失败反馈(强制) + +查找迭代后**必须先反馈**,再继续或停止: + +| 结果 | 反馈文案(可微调,语义不变) | 下一步 | +|------|------------------------------|--------| +| 精确命中 1 个迭代 | `✅ 已找到迭代「{名称}」(状态:{状态}),关联需求 {N} 条。` | 继续拉需求并生成 | +| 0 个匹配 | `❌ 未找到迭代「{输入}」。请核对后重新输入迭代名称(可复制云效迭代标题)。` | **停止生成**;等待用户重输 | +| 多个模糊匹配 | `⚠️ 找到多个相似迭代:1)… 2)… 请回复序号或完整精确名称。` | 等用户选定后再继续 | +| 登录/权限失败 | `❌ 无法访问云效(未登录或无权限)。请在浏览器登录云效后重试,或改用手动清单。` | 停止;提示登录或兜底 | +| 迭代存在但 0 条需求 | `⚠️ 迭代「{名称}」已找到,但暂无关联需求。请确认规划后再试,或改用手动清单。` | 停止生成对外正文 | + +**失败后**:用户只需再发一次迭代名称(或纠正项目名),从步骤 2 重跑;不要要求用户重装 Skill。 + +### 素材优先级(每条需求) + +| 顺序 | 来源 | 用法 | +|------|------|------| +| 1 | 需求描述中的「更新内容」段(非「更新内容·历史」) | 首选,直接改写成日志条目 | +| 2 | AutoPRD「功能变更记录」中可对外部分 | 次选 | +| 3 | 需求标题 + 简短描述摘要 | 仅当前两者皆空时;价值句可合理补全 | +| — | 纯技术债、内部监控、未对用户开放 | **不写入**对外日志 | + +Bug 类需求(类型/标签含缺陷修复、或更新内容写「修复」)归入【Bug修复】;其余归【新功能】。边界不清优先 Bug修复。 + +### 版本号与更新时间 + +| 字段 | 规则 | +|------|------| +| 版本号 | 优先从迭代名解析(如含 `V1.1.5` / `1.1.5`);解析不到再问用户 | +| 更新时间 | 用户提供则用;未提供可写迭代计划开始日的 `MM月DD日HH:MM`,或先问 | +| 产品线 | 默认 `OneOS PC版` | + +## 兜底流程(手动清单) + +无云效权限、或迭代为空时:用 [input-template.md](input-template.md) 粘贴清单,跳过迭代步骤,直接分类润色。 + +## 固定输出格式 + +```text +OneOS PC版V{主.次.修订}更新日志: +更新时间:{MM}月{DD}日{HH}:{MM} +---------------------------------------------------- +【新功能】 +1「{模块名}」{做了什么},{责任部门}可{用户价值 / 解决的问题} +2「{模块名}」{…} + +【Bug修复】 +3「{模块名}」{修复了什么},{责任部门}可{恢复的能力 / 避免的影响} +``` + +### 版式硬约束 + +| 项 | 规则 | +|----|------| +| 标题 | `OneOS PC版V` + 版本号 + `更新日志:`(如 `V1.1.5`,无空格) | +| 更新时间 | 仅开始时刻;`MM月DD日HH:MM`;**禁止**「预计x分钟内完成」 | +| 分隔线 | `----------------------------------------------------` | +| 分区 | 仅 `【新功能】` / `【Bug修复】`;空类整段省略 | +| 序号 | 全篇连续,从 1 起 | +| 模块名 | `「」`;与产品菜单正式名一致 | +| 对外正文 | **不写**需求号、缺陷号、迭代 ID(核对清单可另附) | +| 结尾 | 无签名、无「感谢使用」、无技术附录 | + +## 分类 / 合并 / 文案(成稿规则) + +| 归入【新功能】 | 归入【Bug修复】 | 不写进对外日志 | +|----------------|-----------------|----------------| +| 新增能力、新入口、新字段/流程、可感知体验优化 | 原有能力异常、报错、数据错、交互失效 | 技术债、无感性能、未开放灰度、仅内部监控 | + +- **同分区同模块**合并为一条,分点用 `;` +- **跨分区**同模块分栏各写一条 +- 每条含:做什么 + 用户价值 + 责任部门自然嵌入(`{部门}人员可…`) +- **禁止**「供…条线…使用」;部门对照 [module-role-mapping.md](module-role-mapping.md) +- 禁止编造迭代内不存在的能力;存疑跳过或标待确认(不进正式成稿) +- 单条建议 ≤100 字 + +排序:先新功能后修复;同分区全员影响优先;资金/合规修复置修复区最前。 + +## 输出纪律 + +1. **先**输出迭代读取反馈(✅/❌/⚠️) +2. 成功时:可先给「本迭代需求清单」短表(含编号,便于测试核对) +3. 再输出**纯文本成稿正文**(默认不要 Markdown 大标题包裹成稿) +4. 用户明确要求时,可在正文后附「分类依据 / 跳过项」 + +## 发布前自检 + +- [ ] 已反馈迭代查找结果;失败时未强行编造成稿 +- [ ] 成稿条目均可追溯到本迭代关联需求素材 +- [ ] 标题、时间行、分隔线正确;无预计时长 +- [ ] 同模块合并正确;每条含价值 + 部门自然嵌入 +- [ ] 对外正文无需求号/缺陷号/技术黑话;序号连续 + +## 给测试同事的口令 + +```text +按 $AutoVUL 生成版本更新日志。 +项目:统一运营管理平台PC端 +迭代名称:V1.1.5发版迭代 +更新时间:07月16日16:00 +``` + +迭代名写错时,按提示改完再发一次即可。 + +手动兜底见 [input-template.md](input-template.md);成稿范例见 [examples.md](examples.md)。 diff --git a/.claude/skills/AutoVUL/examples.md b/.claude/skills/AutoVUL/examples.md new file mode 100644 index 0000000..eef8c46 --- /dev/null +++ b/.claude/skills/AutoVUL/examples.md @@ -0,0 +1,77 @@ +# AutoVUL 成稿与交互范例 + +## 1. 迭代读取失败 → 重输 + +**用户:** + +```text +按 $AutoVUL,迭代名称:V1.1.5 +``` + +**Agent:** + +```text +❌ 未找到迭代「V1.1.5」。请核对后重新输入迭代名称(可复制云效迭代标题)。 +``` + +**用户:** + +```text +V1.1.5发版迭代 +``` + +**Agent:** + +```text +✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 3 条: +1. ONEOSP-201 车辆管理运营城市展示优化 +2. ONEOSP-202 还车应结款租金计算修复 +3. ONEOSP-203 业务管理新增租赁物流统计 + +OneOS PC版V1.1.5更新日志: +更新时间:07月16日16:00 +---------------------------------------------------- +【新功能】 +1「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源 +2「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况 + +【Bug修复】 +3「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确 +``` + +## 2. 完整成稿风格基准(手动/迭代素材均可) + +> 责任部门对照:`module-role-mapping.md`(OneOS 业务条线说明) + +```text +OneOS PC版V1.1.5更新日志: +更新时间:07月16日16:00 +---------------------------------------------------- +【新功能】 +1「加氢站管理」升级新版加氢站模块,采购部可统一维护签约站与非签约站信息,签约加氢站可便捷管理站点日常运营 +2「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源 +3「审批中心」支持按客户名称搜索关联合同,业务员、法务部审批时可快速定位合同;还车应结款添加照片附件后,业务管理组、安全部、运维部、能源部会签人员可在审批流中查看完整附件 +4「保险采购」支持保比价单按车辆单独勾选并分开提交审批,运维部、安全部人员可按车辆分批推进投保审批 +5「故障管理」增加「故障来源」字段展示,运维部人员可区分上报渠道并跟进处理 +6「证照管理」特种设备使用登记证/使用标识照片支持OCR识别,运维部、安全部人员可减少手工录入、加快证照建档 +7「全局显示」统一各模块列表分页样式与交互,各模块操作人员跨页面浏览时体验一致、上手更快 +8「交还车管理」支持交车/还车签章文件在线预览,运维部、业务管理组、司机可现场核对签章文件;提交时增加胎纹数据必填校验,减少漏填导致的后续结算争议 +9「还车应结款」调整安全部待办区域权限,安全部人员仅处理职责范围内的还车应结款审批 +10「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况 + +【Bug修复】 +11「还车管理」修复签字单与还车检查单/还车应结款轮胎数据不一致,运维部、业务管理组人员还车验收与结算依据保持一致 +12「租赁合同」修复编辑合同提交时异常提示内容冗余,业务员、法务部人员可快速定位真正错误原因 +13「交还车管理」修复交车/还车签章客户名称关联错误,避免运维部、业务管理组、司机现场核对时客户信息张冠李戴 +14「车辆氢费明细」修复同客户不同生效时段、以及不同客户成本单价显示异常,能源部人员可准确核对氢费核算单价 +15「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确 +``` + +## 对照要点 + +| 规则 | 体现 | +|------|------| +| 先反馈迭代结果 | 失败可重输;成功列出需求再成稿 | +| 无预计时长 | 更新时间仅 `07月16日16:00` | +| 对外无需求号 | 成稿正文不出现 ONEOSP-xxx | +| 部门自然嵌入 | `运维部人员可…`,无「供…条线使用」 | diff --git a/.claude/skills/AutoVUL/input-template.md b/.claude/skills/AutoVUL/input-template.md new file mode 100644 index 0000000..ab42bb7 --- /dev/null +++ b/.claude/skills/AutoVUL/input-template.md @@ -0,0 +1,69 @@ +# AutoVUL · 输入模板 + +## A. 测试人员 · 按云效迭代(推荐) + +复制下面口令发给 AI(改迭代名即可): + +```text +按 $AutoVUL 生成版本更新日志。 +项目:统一运营管理平台PC端 +迭代名称: +更新时间: +``` + +### 期望反馈 + +- 成功:`✅ 已找到迭代「…」,关联需求 N 条` → 随后输出成稿 +- 失败:`❌ 未找到迭代「…」。请重新输入迭代名称` → 改名后再发一次即可 + +**说明**:预计维护时长由人工单独通知,不写入版本更新日志。 + +--- + +## B. 手动清单(云效不可用时兜底) + +### 版本元信息 + +| 字段 | 填写 | 示例 | +|------|------|------| +| 产品线 | | OneOS PC版 | +| 版本号 | | V1.1.5 | +| 更新开始时间 | | 07月16日16:00 | +| 发布范围 | 全量 / 灰度(说明) | 全量 | +| 是否对外发布 | 是 / 否 | 是 | + +### 新功能(原始材料,可粗糙) + +| 模块 | 做了什么 | 解决什么问题 / 用户故事 | 业务条线 | 责任部门 | 是否写入日志 | +|------|----------|-------------------------|----------|----------|--------------| +| 例:加氢站管理 | 升级新版模块 | 统一维护站点 | 能源业务条线 | 采购部、加氢站 | 是 | +| | | | | | | + +### Bug修复(原始材料,可粗糙) + +| 模块 | 原问题现象 | 修复后表现 | 对用户的影响 | 业务条线 | 责任部门 | 是否写入日志 | +|------|------------|------------|--------------|----------|----------|--------------| +| 例:还车应结款 | 租金费用项重复 | 金额正确 | 避免结算偏差 | 租赁业务条线 | 业务管理组、安全部、运维部、业务管理组-能源部 | 是 | +| | | | | | | | + +### 明确不要写入对外日志 + +- (如:重构、依赖升级、仅内部监控、未上线配置) + +### 粘贴给 AI + +```text +请按 $AutoVUL 用下方手动清单生成成稿(跳过云效迭代)。 +要求:同模块合并;每条含「做什么 + 用户价值」;责任部门自然融入;只输出正文。 + +【版本元信息】 +- 产品线:OneOS PC版 +- 版本号:V +- 更新开始时间: + +【新功能】 +(粘贴) + +【Bug修复】 +(粘贴) +``` diff --git a/.claude/skills/AutoVUL/module-role-mapping.md b/.claude/skills/AutoVUL/module-role-mapping.md new file mode 100644 index 0000000..2deea13 --- /dev/null +++ b/.claude/skills/AutoVUL/module-role-mapping.md @@ -0,0 +1,60 @@ +# 模块 → 业务条线 / 责任部门对照 + +> 数据源:OneOS「业务条线说明」页责任部门定义(租赁 / 能源 / 运维 / 安全 / 自营) +> 生成更新日志时,**适用对象**须从此表取 `roles`,不得自造部门名。 + +## 五条业务条线 + +| ID | 条线名称 | +|----|----------| +| lease | 租赁业务条线 | +| energy | 能源业务条线 | +| ops | 运维管理条线 | +| safety | 安全管理条线 | +| self-operated | 自营业务条线 | + +## 本版变更模块对照 + +| 更新日志模块名 | 业务条线 | lines.ts 模块 | 责任部门(roles,原文) | +|----------------|----------|---------------|-------------------------| +| 加氢站管理 | 能源业务条线 | 加氢站管理 | 采购部、加氢站 | +| 车辆管理 | 运维管理条线 | 验车入库 / 车辆管理(资产底座) | 运维部、采购部 | +| 审批中心 | 跨条线 | 租赁合同 + 还车应结款(审批场景) | 业务员、法务部、业务管理组、安全部、运维部、业务管理组-能源部 | +| 保险采购 | 运维管理条线 | 证照 / 保险(车辆资产关联) | 运维部、安全部 | +| 故障管理 | 运维管理条线 | 故障管理 | 运维部、司机 | +| 租赁合同 | 租赁业务条线 | 租赁合同 | 业务员、法务部 | +| 证照管理 | 运维管理条线 | 证照管理 | 运维部、安全部 | +| 全局显示 | 全系统 | 系统介绍(五条条线) | 业务管理组、运维部、采购部、安全部、法务部、财务部、业务管理组-能源部、业务服务组 | +| 交还车管理 | 运维管理条线 | 交车管理 + 还车管理 | 运维部、业务管理组、司机 | +| 还车应结款 | 租赁业务条线 | 还车应结款 | 业务管理组、安全部、运维部、业务管理组-能源部 | +| 还车管理 | 运维管理条线 | 还车管理 | 运维部、业务管理组 | +| 车辆氢费明细 | 能源业务条线 | 车辆氢费明细 | 业务管理组-能源部 | +| 业务管理 · 租赁统计 | 租赁业务条线 | 租赁业务台账 / 客户管理 | 业务管理组、业务员 | +| 业务管理 · 物流统计 | 自营业务条线 | 自营台账 | 业务服务组、财务部 | + +## 写法规则 + +- 部门名与 `roles` **完全一致**(`业务管理组-能源部` 对外可简写「能源部人员」) +- **禁止**「供…业务条线…使用」「供…人员使用」 +- **推荐**:`{部门}人员可…` / `{部门}可…` / `{部门}审批时可…` +- 同模块合并后,多部门用顿号或分号自然串联 +- 找不到模块时:查 `lines.ts`;仍无则标「待产品确认」不进成稿 + +## 自然表述示例 + +| 模块 | 生硬(禁止) | 自然(推荐) | +|------|--------------|--------------| +| 车辆管理 | 供运维管理条线运维部使用 | 运维部人员可快速辨认车辆所属运营区域 | +| 加氢站管理 | 供能源业务条线采购部、加氢站使用 | 采购部可统一维护站点信息,加氢站可便捷管理日常运营 | +| 还车应结款 | 供租赁业务条线业务管理组…使用 | 业务管理组、安全部、运维部、能源部还车结算金额更准确 | + +## 常见错误(禁止) + +| 错误写法 | 正确写法 | +|----------|----------| +| 氢能运营业务人员 | 采购部、加氢站(能源 · 加氢站管理) | +| 车辆运营及资产管理业务人员 | 运维部、采购部(运维 · 车辆管理) | +| 各业务条线审批人员 | 业务员、法务部、…(按审批场景枚举) | +| 保险采购业务人员 | 运维部、安全部 | +| 还车结算业务人员 | 业务管理组、安全部、运维部、业务管理组-能源部 | +| 经营分析及管理部门 | 业务管理组、业务服务组(分租赁 / 自营) | diff --git a/.claude/skills/AutoVUL/references/yunxiao-sprint.md b/.claude/skills/AutoVUL/references/yunxiao-sprint.md new file mode 100644 index 0000000..93fc195 --- /dev/null +++ b/.claude/skills/AutoVUL/references/yunxiao-sprint.md @@ -0,0 +1,81 @@ +# 云效迭代 → 需求清单(AutoVUL 取数) + +测试人员只提供**迭代名称**;Agent 负责在云效定位迭代、拉取关联需求、抽出更新素材。 + +## 安全边界(强制) + +- **禁止**在对话、Skill、日志、成稿中写入 Token、密码、Cookie、私钥。 +- 优先复用**已登录浏览器会话**操作云效 Projex(与 `yunxiao-requirement-lifecycle` 一致)。 +- 若团队已配置本机环境变量访问 OpenAPI,可走 API;Token 只读自环境,永不回显。 + +建议环境变量(可选,名称可按团队调整): + +| 变量 | 用途 | +|------|------| +| `YUNXIAO_TOKEN` | 个人访问令牌(`x-yunxiao-token`) | +| `YUNXIAO_DOMAIN` | 服务接入点域名 | +| `YUNXIAO_ORG_ID` | 组织 ID(中心版) | +| `YUNXIAO_PROJECT_ID` | 默认项目 ID(或运行时按项目名解析) | + +## 默认项目 + +口令未指定时:`统一运营管理平台PC端`。项目名不精确时先问清再查迭代。 + +## 判定顺序:解析迭代 + +| 顺序 | 条件 | 结果 | +|------|------|------| +| 1 | 用户未给迭代名称 | 询问:「请输入云效迭代名称(可复制标题)」;不继续 | +| 2 | 项目未确定 | 询问精确项目名;不继续 | +| 3 | 云效不可访问 | `❌ 无法访问云效…`;不继续 | +| 4 | 名称精确匹配 1 条迭代 | `✅ 已找到迭代…`;进入拉需求 | +| 5 | 0 条 | `❌ 未找到迭代…请重新输入`;不继续 | +| 6 | >1 条相似 | `⚠️ 多个相似…请回复序号或精确名称`;等用户选定 | +| 7 | 命中但关联需求 0 | `⚠️ …暂无关联需求`;不生成对外正文 | + +匹配规则:先**全等**(去首尾空格);全等失败再做包含/去空格模糊;模糊多条必须让用户确认,禁止擅自挑一个。 + +## 拉取关联需求 + +只收集工作项类型为**需求(Req)**且正式关联到该迭代的项。 + +- **浏览器路径**:打开项目 → 迭代 → 该迭代规划/工作项列表 → 筛选类型=需求 → 导出或逐条打开描述。 +- **OpenAPI 路径(示意)**: + 1. `ListSprints`:按项目列迭代,用 `name` 匹配用户输入,得到 `sprintId` + 2. `SearchWorkitems`:`category=Req`,`spaceId=项目Id`,`conditions` 中按字段 `sprint` CONTAINS `[sprintId]`(以租户实际 fieldIdentifier 为准;可先在需求列表页过滤后复制 `workitem/list` 的 conditions) + 3. 分页直到收齐(关注响应头 `x-total`) + 4. 需要正文时再 `GetWorkitem` 取 description + +每条需求最少记录: + +| 字段 | 说明 | +|------|------| +| serialNumber | 如 ONEOSP-123(仅用于核对清单,**不进对外成稿**) | +| subject | 标题 | +| status | 状态展示名 | +| description | 用于抽取「更新内容」 | +| category / type / labels | 辅助判断新功能 vs 修复 | + +## 从描述抽「更新内容」 + +1. 定位标题「更新内容」或「## 更新内容」段落 +2. **不要**把「更新内容·历史」并入本版素材(历史仅备查) +3. 若无「更新内容」:用标题 + 需求说明摘要;仍无有效产品语义则跳过并在内部备注 + +## 成功反馈后的核对清单(推荐短表) + +在生成成稿前输出,例如: + +```text +✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 5 条: +1. ONEOSP-101 保险采购比价分车提交 +2. ONEOSP-102 交还车签章预览 +… +正在按 AutoVUL 格式生成更新日志… +``` + +用户若立刻纠正「某条不要进日志」,从素材中剔除后再出成稿。 + +## 与发版任务关系 + +本 Skill **只读**迭代与需求,**不创建**【发版】任务、不改云效状态。发版挂链仍用 `yunxiao-requirement-lifecycle`(如「准备发布」口令 / A08)。 diff --git a/.claude/skills/oneos-autoprd/SKILL.md b/.claude/skills/oneos-autoprd/SKILL.md new file mode 100644 index 0000000..a10d84f --- /dev/null +++ b/.claude/skills/oneos-autoprd/SKILL.md @@ -0,0 +1,183 @@ +--- +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 + +为 OneOS 业务模块生成**产品经理可读、可评审、可排期**的需求说明,并**自动挂到 Axhub Make 标注工具 → 原型目录**。 + +另支持:**需求定稿**时汇总功能/逻辑变更记录;与云效组合时提供「需求说明 + 更新内容」;需求进入**分析中 / 设计中 / 待开发**时**自动创建与需求同名的关联任务**(规则见下,写在本 Skill,不改云效 Skill)。 + +颗粒度对齐「保险采购」全模块 PRD:讲清做什么、谁用、故事点、正逆向、流程图与关键业务逻辑;**不写**表结构、接口、字段代码名、文件路径、实现清单。 + +## 何时使用(含自动触发) + +**主动调用时** + +- AutoPRD、OneOS 需求说明、整模块 PRD、故事点 + 流程图 + +**改原型时必须自动跟进(全局规则)** + +- 正在修改 `src/prototypes//` 下页面、交互、文案、判定、验收相关内容 +- 同一轮交付内同步更新 PRD Markdown + 标注目录,不得只改代码 + +**需求定稿时(强制)** + +- 用户回复含:`需求定稿` / `定稿` / `确认定稿` / `本次定稿` +- 执行 [references/release-changelog.md](references/release-changelog.md):写第 10 章 + 更新基线 + +**云效建需求 / 完善需求时(强制组合)** + +- 与 `$yunxiao-requirement-lifecycle` 一起使用时:先跑本 Skill,再写云效描述 +- **需求说明** ← PRD 正文(或浓缩交付口径 + 关键章节) +- **更新内容** ← 第 10 章中**自上次定稿以来**的条目(无增量则「首版定稿 / 本轮无功能增量」) + +**云效需求推进至分析中 / 设计中 / 待开发时(强制)** + +- 无论口令来自本 Skill 还是云效 Skill,只要本轮把需求推到上述状态,就执行 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md) +- 自动建**与需求同名**的任务并正式关联;标签与创建时间口径沿用需求;分析中/设计中负责人=创建人,待开发负责人=**何斐** + +不要用本 Skill 替代:需求探索访谈、设计比稿、纯样式微调(无产品语义变化时可跳过全量重写)。 + +## 工作流 + +### 主流程(写/同步 PRD) + +1. **定模块**:确认 OneOS 模块名与 `src/prototypes//`。 +2. **读上下文(只取产品语义)** + - 用户说明、已确认口径、原型标注、`.spec/`、业务条线说明(`lines.ts`) + - 忽略实现细节;字段名/接口改写成业务语言。 +3. **收敛边界**:做什么 / 不做什么、外部依赖、与其它模块关系。 +4. **按模板成文**:下方「输出结构」;缺关键信息最多问 1~2 个问题,其余写「假设」。 +5. **落盘 + 标注同步(强制)** — 见 [references/annotation-sync.md](references/annotation-sync.md)。 + + | 顺序 | 动作 | + |------|------| + | A | 写/更新 `src/prototypes//.spec/requirements-prd.md` | + | B | 写/更新 `src/resources/prd/-autoprd.md` | + | C | 更新 `annotation-source.json` **顶层** `directory.nodes`(PRD 全文 + 推荐分章);禁止只写 `data.directory` | + | D | 若存在 `scripts/sync-annotation-directory.mjs`,执行之 | + +6. **交付说明**:路径、标注目录入口、故事点合计、开放问题/假设。 + +### 定稿流程(关键字触发) + +见 [references/release-changelog.md](references/release-changelog.md)。摘要: + +1. 读 PRD + `.spec/autoprd-baseline.json` +2. 汇总自上次定稿以来的**功能/逻辑**变更(排除样式/UI/表结构) +3. 追加到 `## 10. 功能变更记录`(最新在上) +4. 更新基线 JSON + 标注目录 +5. 若同时发云效:本次定稿块 →「更新内容」;旧段 →「更新内容·历史」 + +### 云效交接格式(给 yunxiao skill 直接粘贴) + +```markdown +## 原型链接 +<对象存储或预览 URL> + +## 需求说明 + + +## 更新内容 +<第 10 章本次定稿块;无则「首版定稿」或「本轮无功能/逻辑增量」> + +## 更新内容·历史 +<以往更新内容倒序,勿删除> +``` + +### 云效阶段任务(状态推进时强制) + +见 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)。摘要: + +| 需求状态 | 任务标签 | 负责人 | 标题 | +|----------|----------|--------|------| +| 分析中 | 分析 | 需求创建人 | 与需求同名 | +| 设计中 | 设计 | 需求创建人 | 与需求同名 | +| 待开发 | 交付(或开发) | 何斐 | 与需求同名 | + +正式关联需求;创建时间口径沿用需求;同阶段未取消任务不重复建。 + +## 写作硬约束 + +**必须写** + +- 一句话定位 + 目标 / 非目标 +- 模块边界(含 mermaid 总览更好) +- 角色与目标(角色名优先对齐业务条线说明) +- **用户故事**(业务条线说明口径)+ Epic 级**故事点(SP)**粗估 +- 分功能**正向**与**逆向/边界** +- 至少 1~2 个 **mermaid** 流程图 +- **关键业务逻辑**(业务话) +- 验收清单 + 「交付口径」一段话 +- 定稿后:**功能变更记录**(第 10 章) + +**禁止写** + +- 数据库表、字段名、接口路径、代码路径、组件名、存储 key +- 研发实现指令(可写「正式环境由审批中心回写」这类业务依赖) +- 在变更记录里写样式/UI/表结构优化 + +## 用户故事口径(强制 · 对齐业务条线说明) + +真相源:原型 **业务条线说明**(`lease-business-line-overview` / `lines.ts`)。 +每条能力用「责任部门 → **起点** → **怎么运作** → **闭环**」叙述;**不要**用「作为…我希望…」宽表作主叙述。 + +| 块 | 写什么 | +|----|--------| +| 角色 | 谁负责、谁协同 | +| 起点 | 谁在什么前提下启动 | +| 怎么运作 | 有序步骤,含跨角色协作 | +| 关键结果 | 可选标签 | +| 闭环 | 业务终点与可追溯性 | + +可选:`US-xx`、压缩句「作为…我想…以便…」、规模 S/M/L 或 SP(仅排期,不替代主叙述)。 + +## 输出结构 + +完整模板:[references/template.md](references/template.md)。 + +```markdown +# <模块名> · 产品需求说明(全模块) + +## 1. 一句话与目标 +## 2. 模块边界(最重要) +## 3. 用户与角色 +## 4. 用户故事与故事点(业务条线说明口径) +## 5. 功能模块说明(正向 / 逆向) +## 6. 关键业务逻辑(必须对齐) +## 7. 总览流程图 +## 8. 验收清单 +## 9. 交付口径 +## 10. 功能变更记录 ← 定稿后维护;日常改原型不强制每改必写 +``` + +## 质量自检 + +- [ ] 产品经理不看代码也能评审 +- [ ] 用户故事为起点 / 怎么运作 / 闭环 +- [ ] `.spec/requirements-prd.md` 已更新 +- [ ] 标注目录「产品需求说明(PRD)」已同步且正文一致 +- [ ] 定稿时:第 10 章 + `autoprd-baseline.json` 已更新 +- [ ] 推进至分析中/设计中/待开发时:同名任务已创建或复用,正式关联,负责人正确 +- [ ] 无表结构 / 接口 / 代码路径;变更记录无样式/UI 废话 + +## 参考 + +- 定稿变更日志:[references/release-changelog.md](references/release-changelog.md) +- 云效阶段任务:[references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md) +- 标注同步细则:[references/annotation-sync.md](references/annotation-sync.md) +- 章节模板:[references/template.md](references/template.md) +- 故事示例:[references/granularity-example.md](references/granularity-example.md) +- 业务条线:`src/prototypes/lease-business-line-overview/lines.ts` +- 复杂判定规格:配合项目规则 `business-logic-documentation` +- 云效组合:`$yunxiao-requirement-lifecycle`(建需求时先本 Skill) diff --git a/.claude/skills/oneos-autoprd/references/annotation-sync.md b/.claude/skills/oneos-autoprd/references/annotation-sync.md new file mode 100644 index 0000000..2cdd7c6 --- /dev/null +++ b/.claude/skills/oneos-autoprd/references/annotation-sync.md @@ -0,0 +1,98 @@ +# Axhub 标注目录同步(强制) + +使用 OneOS AutoPRD 时,除资源库归档外,**必须**把 PRD 挂到 Axhub Make 标注工具的「原型目录」里,用户才能在右侧面板直接打开。 + +## 落点(三份必须一致) + +| # | 路径 | 作用 | +|---|------|------| +| 1 | `src/prototypes//.spec/requirements-prd.md` | **主真相**:产品可读 PRD 全文 | +| 2 | `src/prototypes//annotation-source.json` → **顶层** `directory.nodes` | **标注目录入口**:面板里可见的 Markdown/PRD | +| 3 | `src/resources/prd/-autoprd.md` | 资源库归档(可选但默认写) | + +可选:复杂判定另写 `.spec/.md`,目录里再挂一条,并在 PRD 里链过去(与 `business-logic-documentation` 一致)。 + +## ⚠️ 目录必须写在文档顶层(常见踩坑) + +Wire format(与 `prototype-annotation` 一致): + +```json +{ + "documentVersion": 1, + "format": "axhub-annotation-source", + "data": { "version": 2, "prototypeName": "...", "nodes": [] }, + "markdownMap": {}, + "assetMap": {}, + "directory": { "nodes": [] } +} +``` + +| 正确 | 错误 | +|------|------| +| 顶层 `directory.nodes` | 只写在 `data.directory` | +| `PrototypeAnnotationHost` / Make 读的是 `source.directory` | 写在 `data` 下时右侧「原型目录」为空 | + +若误写在 `data.directory`:迁移到顶层 `directory`,并删除 `data.directory`,避免双源。 + +## 目录节点写法(推荐) + +在说明根 `folder` 的 `children` **靠前**插入或更新: + +```json +{ + "type": "markdown", + "id": "-doc-prd", + "title": "产品需求说明(PRD)", + "markdownPath": ".spec/requirements-prd.md", + "markdown": "<与 .spec/requirements-prd.md 相同的全文>" +} +``` + +### 分章节 PRD(推荐,便于右侧目录浏览) + +在「产品需求说明(PRD)」旁增加文件夹 **「PRD 分章」**,按 PRD 的 `##` 一级标题拆成多个 `markdown` 子节点(标题用章节名,正文为该章全文)。专题规格(`.spec/.md`)放在 **「专题规格」** 文件夹。 + +参考结构: + +```text +folder <模块>说明 + markdown 产品需求说明(PRD) ← 全文 + folder PRD 分章 + markdown 1. 一句话与目标 + markdown 2. 模块边界 + … + folder 专题规格 + markdown +``` + +有 `scripts/sync-annotation-directory.mjs` 的原型:改 PRD / `.spec` 后**必须执行**该脚本再生目录。 + +定稿后第 10 章「功能变更记录」必须进入 PRD 全文节点;若有「PRD 分章」,同步该章节点。详见 [release-changelog.md](release-changelog.md)。 + +约定: + +- `id`:`<短前缀>-doc-prd`(如 `ipc-doc-prd`、`wb-doc-prd`、`h5-va-doc-prd`) +- `title`:优先「产品需求说明(PRD)」;已有「PRD」可保留不改名 +- **同时写** `markdownPath` + `markdown`:构建可内联路径,运行时目录也能直接读到正文 +- 若已有同 id / 同标题节点:只更新正文与 path,不新建重复入口 +- 遵守 `prototype-annotation-layout`:目录以 `markdown` 为主,不要用 `link`/`route` 顶替 PRD + +## 增量改原型时怎么更新 + +1. 根据改动的文件路径确定 ``。 +2. 读现有 `.spec/requirements-prd.md`(没有则按 AutoPRD 模板新建)。 +3. 只改受影响章节(故事、正逆向、关键逻辑、验收);其余保留。 +4. 回写 `.spec` + `resources/prd` + **顶层** `annotation-source.json` → `directory`(正文与文件一致;含分章节点)。 +5. 若原型有 `scripts/sync-annotation-directory.mjs`,改完后执行它。 + +## 可跳过全量重写的情况 + +纯样式、无文案/无流程/无判定变化的微调:不必重写整份 PRD,但若改了用户可见文案或交互结果,仍须同步对应段落与目录节点。 + +## 验收 + +- [ ] `.spec/requirements-prd.md` 存在且为最新 +- [ ] `annotation-source.json` **顶层**有 `directory.nodes`(不是只在 `data` 下) +- [ ] 标注目录能打开「产品需求说明(PRD)」且内容与文件一致 +- [ ] 若有「PRD 分章」:各章可单独打开 +- [ ] 用户故事仍为业务条线说明口径(起点 / 怎么运作 / 闭环) diff --git a/.claude/skills/oneos-autoprd/references/granularity-example.md b/.claude/skills/oneos-autoprd/references/granularity-example.md new file mode 100644 index 0000000..5126671 --- /dev/null +++ b/.claude/skills/oneos-autoprd/references/granularity-example.md @@ -0,0 +1,58 @@ +# 颗粒度标杆与用户故事口径 + +## 用户故事:对齐「业务条线说明」页面 + +页面:`/prototypes/lease-business-line-overview` +数据:`src/prototypes/lease-business-line-overview/lines.ts` + +每个能力卡片的产品叙述结构: + +```text +责任部门(roles) + → 起点(start) + → 怎么运作(process[],有序) + → 关键结果(outcomes,可选) + → 闭环(closure) +``` + +AutoPRD 第 4 章必须用同一套结构写用户故事,而不是「作为…我希望…」宽表。 + +### 示例(保险采购 · 交车合规,示意) + +#### US · 保险有效才允许交车 +- **角色**:采购部、运维部 +- **起点**:采购或运营维护车辆交强险与商业险台账,作为交车前置合规数据。 +- **怎么运作**: + 1. 在保险采购维护一车一档险种台账(录入 / 识别 / 导入)。 + 2. 交车时系统校验交强 + 商业是否均为正常或临期。 + 3. 不合规则拦截交车并给出可理解原因。 +- **关键结果**:`允许交车` / `禁止交车 · 保险异常` +- **闭环**:交车合规与台账一致、可追溯;异常车辆不能进入履约运营。 +- **排期(可选)**:US-17 · 规模 M · 作为采购/运维,我想维护交强+商业险并在交车时校验,以便不合规车辆无法交车。 + +### 反例(不要作为主叙述) + +| 编号 | 用户故事 | SP | +|------|----------|----| +| B1 | 作为运营,我能看到失败原因 | 2 | + +↑ 信息太扁,缺少起点、协作步骤与闭环结果。 + +--- + +## AutoPRD 其余层次(仍要对齐) + +1. 一句话 + 目标/非目标 +2. 模块边界(独立业务线写清「不自动同步」) +3. 角色表(命名对齐业务条线) +4. **用户故事:起点 / 怎么运作 / 闭环** + SP/规模 +5. 分模块正逆向 +6. 关键业务逻辑 +7. mermaid 流程图 +8. 验收清单 +9. 交付口径 + +## 刻意不写 + +- 表结构、接口、代码路径 +- 用实现指令代替业务闭环描述 diff --git a/.claude/skills/oneos-autoprd/references/release-changelog.md b/.claude/skills/oneos-autoprd/references/release-changelog.md new file mode 100644 index 0000000..998e9bf --- /dev/null +++ b/.claude/skills/oneos-autoprd/references/release-changelog.md @@ -0,0 +1,78 @@ +# 功能变更记录(定稿触发) + +用户回复**需求定稿关键字**后,必须生成「自上次定稿以来」的功能变更日志,追加到 PRD **最下方**,并更新定稿基线。 + +## 触发关键字(命中任一) + +`需求定稿` · `定稿` · `确认定稿` · `本次定稿` + +(可带模块名,如「保险采购需求定稿」。) + +## 写什么 / 不写什么 + +| 要写(产品发版视角) | 不写 | +|----------------------|------| +| 哪个功能做了什么修改 | 布局、页面样式、UI 设计优化 | +| 变更了什么业务逻辑 / 流程 / 验收口径 | 「优化了哪些表」「改了哪些字段/接口」 | +| 用户可感知的交互结果变化 | 纯样式、动效、无语义文案微调 | + +条目口吻示例: + +- 「识别失败」:失败数可点击查看失败文件与原因;仅失败时也可打开明细 +- 「比价单」:审批通过后仍须在保单管理另行录入正式保单(强调不自动同步) + +## 落点 + +1. **PRD 文末章节**(强制) + `src/prototypes//.spec/requirements-prd.md` → `## 10. 功能变更记录` + 新定稿块插在该章**最上方**(倒序:最新在上);历史块保留。 + +2. **定稿基线**(强制) + `src/prototypes//.spec/autoprd-baseline.json` + +```json +{ + "prototypeId": "insurance-procurement", + "lastConfirmedAt": "2026-07-21T12:00:00+08:00", + "lastConfirmedLabel": "定稿 · 2026-07-21", + "summaryBullets": ["…", "…"] +} +``` + +3. **标注目录**:同步更新「产品需求说明(PRD)」全文节点;若有「PRD 分章」,增加或更新「功能变更记录」分章。 + +4. **云效交接**(若本轮同时走云效):把**本次定稿块**正文作为需求描述「更新内容」;旧「更新内容」挪到「更新内容·历史」。 + +## 定稿工作流 + +1. 确认 ``(从对话 / 打开文件 / 用户指名推断)。 +2. 读现有 PRD 与 `autoprd-baseline.json`(无基线则本轮视为**首版定稿**)。 +3. 收集自 `lastConfirmedAt` 以来的产品语义变更: + 对话确认口径 → 批注/操作说明 → PRD 新旧差异 →(可选)相关提交说明。 + 过滤掉样式/表结构类改动。 +4. 若无功能增量:仍写定稿块,正文为「本轮无功能/逻辑增量(仅样式或未达产品语义变更)」或「首版定稿」。 +5. 追加 `### 定稿 · YYYY-MM-DD` + 条目列表到第 10 章顶部。 +6. 更新 `autoprd-baseline.json`;执行 annotation-sync。 +7. 向用户回报:基线时间、条目数、PRD 路径;若需发云效,提示可用口令。 + +## PRD 章节格式 + +```markdown +## 10. 功能变更记录 + +> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。 + +### 定稿 · 2026-07-21 + +- 「能力名」:做了什么 / 逻辑如何变 +- … + +### 定稿 · 2026-07-10 + +- … +``` + +## 与日常改原型的关系 + +- **改原型过程中**:按 AutoPRD 主流程增量更新第 1–9 章;**不必**每改一次就写第 10 章。 +- **用户说定稿时**:才汇总写入第 10 章并刷新基线。 diff --git a/.claude/skills/oneos-autoprd/references/template.md b/.claude/skills/oneos-autoprd/references/template.md new file mode 100644 index 0000000..449b8bf --- /dev/null +++ b/.claude/skills/oneos-autoprd/references/template.md @@ -0,0 +1,163 @@ +# OneOS AutoPRD 输出模板 + +复制下列结构填写。方括号为占位。**第 4 章用户故事必须用业务条线说明口径。** + +```markdown +# <模块名> · 产品需求说明(全模块) + +## 1. 一句话与目标 + +**一句话** +<用一句话说明模块解决什么> + +**要解决的问题** +- … + +**本期目标** +1. … +2. … + +**非目标(本期不做)** +- … + +## 2. 模块边界(最重要) + +(可选 mermaid:模块与外部系统关系) + +| 业务线 / 子域 | 做什么 | 不做什么 | +|---------------|--------|----------| +| … | … | … | + +**外部依赖(产品口径)** +| 依赖方 | 交互方式 | 产品要求 | +|--------|----------|----------| +| … | … | … | + +**关键约束** +- … + +## 3. 用户与角色 + +| 角色 | 主要目标 | +|------|----------| +| … | … | + +> 角色名优先与「业务条线说明」一致(如业务管理组、采购部、运维部)。 + +## 4. 用户故事与故事点(业务条线说明口径) + +> 故事点 / 规模供排期参考,可按团队基准调整。合计约 **N SP**(或 S/M/L 汇总说明)。 + +按 Epic 或能力单元分组。**每一条**按下面四块写(对齐业务条线说明页:责任部门 → 起点 → 怎么运作 → 闭环): + +### Epic A · <名称>(约 N SP) + +#### A1 · <能力标题> +- **角色**:… +- **起点**:… +- **怎么运作**: + 1. … + 2. … + 3. … +- **关键结果**(可选):`可…` / `禁止…` +- **闭环**:… +- **排期(可选)**:US-xx · 规模 S/M/L · 「作为…,我想…,以便…」 + +#### A2 · <能力标题> +… + +### Epic B · … + +## 5. 功能模块说明(正向 / 逆向) + +### 5.x <功能名> + +**正向** +1. … +2. … + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| … | … | + +## 6. 关键业务逻辑(必须对齐) + +### 6.x <规则名> + +用编号优先级、表格或短列表写清业务规则。 + +## 7. 总览流程图 + +```mermaid +flowchart TB + home[入口] --> a[能力A] + home --> b[能力B] +``` + +## 8. 验收清单(产品 / 测试) + +**子域 A** +- [ ] … + +## 9. 交付口径 + +> <一段可贴需求单开头的浓缩描述> + +## 10. 功能变更记录 + +> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。 +> 由「需求定稿」关键字触发追加;日常改原型不强制每改必写。详见 [release-changelog.md](release-changelog.md)。 + +### 定稿 · YYYY-MM-DD + +- 「<能力名>」:<做了什么 / 逻辑如何变> +- … +``` + +成文后**必须**同步 Axhub 标注目录(见 [annotation-sync.md](annotation-sync.md)): + +1. `src/prototypes//.spec/requirements-prd.md` +2. `annotation-source.json` → 目录节点「产品需求说明(PRD)」(`markdownPath` + `markdown`) +3. `src/resources/prd/-autoprd.md` +4. 定稿时另写:`src/prototypes//.spec/autoprd-baseline.json` + +## 用户故事写法(对照业务条线说明) + +业务条线说明页字段 → AutoPRD 字段: + +| 页面 | AutoPRD | +|------|---------| +| 责任部门标签 | **角色** | +| 起点 | **起点** | +| 怎么运作(有序列表) | **怎么运作** | +| 结果色签 | **关键结果**(可选) | +| 闭环 | **闭环** | + +示例口吻(摘自业务条线风格,非照抄某模块): + +- **起点**:采购部维护交强与商业险台账,作为交车合规的前置数据。 +- **怎么运作**:1. 运营录入或识别保单… 2. 运维交车时校验… +- **闭环**:核心险种有效则允许交车;异常则拦截并提示原因,过程可追溯。 + +**不要**用下面这种宽表作为第 4 章唯一形态: + +| 编号 | 作为…我希望… | SP | +|------|--------------|----| + +「作为…我想…以便…」只允许作为**排期可选一行**,主叙述必须是起点/怎么运作/闭环。 + +## 正逆向写法约定 + +- **正向**:用户成功完成任务的最短路径 +- **逆向**:取消、关闭、禁用、冲突状态、不可操作项 +- 审批类:写清「本页只读 / 在外部系统办理」 + +## 规模与故事点 + +| 规模 | 建议含义 | 约 SP | +|------|----------|-------| +| S | 轻交互 / 列表展示 | 1~2 | +| M | 标准流程 + 筛选详情 | 3~5 | +| L | 状态机 / 跨模块 / 批量识别 | 6+ | diff --git a/.claude/skills/oneos-autoprd/references/yunxiao-stage-tasks.md b/.claude/skills/oneos-autoprd/references/yunxiao-stage-tasks.md new file mode 100644 index 0000000..09cfe27 --- /dev/null +++ b/.claude/skills/oneos-autoprd/references/yunxiao-stage-tasks.md @@ -0,0 +1,44 @@ +# 云效阶段任务自动创建(AutoPRD 负责) + +本规则写在 **oneos-autoprd**,不依赖改写 `yunxiao-requirement-lifecycle`。 +凡本 Skill 参与的云效操作中,需求推进到下列状态时,**同一轮必须**建同名任务并正式关联需求。 + +## 触发状态 + +| 需求推进至 | 任务标签(右侧基础字段) | 负责人 | 任务标题 | +|------------|--------------------------|--------|----------| +| **分析中** | `分析` | **需求创建人**姓名 | 与需求标题**完全同名** | +| **设计中** | `设计` | **需求创建人**姓名 | 与需求标题**完全同名** | +| **待开发** | `交付`(若项目无「交付」标签则用 `开发`) | **何斐** | 与需求标题**完全同名** | + +## 强制字段与关系 + +1. **正式关联需求**:父子或关联项;只挂标题不算关联。创建后必须在界面上能看到需求 ↔ 任务关系。 +2. **标签**:写入任务「标签」基础字段(见上表);标题前缀不能代替标签。 +3. **创建时间沿用需求**:任务的创建时间(及标签侧可见的创建时间口径)尽量与**需求创建时间**一致。 + - 平台允许改创建时间 / 用 API 指定时:写成需求的创建时间。 + - 平台不允许:在任务描述首行注明 `创建时间口径=需求创建时间(YYYY-MM-DD HH:mm)`,并在回报里说明「平台限制未改系统创建时间」。 +4. **幂等**:同一需求、同一阶段标签下,已存在**未取消**且已正式关联的同名任务 → **不新建**,只校验负责人/标签/关联是否正确,缺则补齐。 +5. **顺序**:先确认需求已进入目标状态(或本轮动作将写入该状态),再创建/补齐任务;回报时给出任务编号与链接。 + +## 执行步骤(apply 时) + +```text +1. 读取需求:标题、创建人、创建时间、当前/目标状态、已关联任务列表 +2. 按上表确定本轮阶段标签与负责人 +3. 查重(需求ID + 阶段标签 + 未取消) +4. 无则创建:标题=需求标题;标签=阶段标签;负责人=上表;创建时间口径=需求创建时间;正式关联需求 +5. 有则校验并修补标签/负责人/关联 +6. 回报:需求状态、任务编号、负责人、是否新建/复用 +``` + +## 不在本规则内 + +- 不自动建「测试 / 发版」任务(除非用户另行要求)。 +- 不因样式类 PRD 变更触发建任务;仅**需求状态**进入分析中/设计中/待开发时触发。 +- 云效生命周期细则、流水线、缺陷闭环仍可由 `$yunxiao-requirement-lifecycle` 配合执行;**本建任务逻辑以 AutoPRD 本文为准**。 + +## 与定稿 / 需求说明的关系 + +- 写「需求说明 / 更新内容」仍按 AutoPRD 主流程与定稿流程。 +- 推进到待开发且需要描述时:先 AutoPRD 文档,再改状态,再按本文件建任务(负责人何斐)。 diff --git a/.cursor/rules/oneos-autoprd-sync.mdc b/.cursor/rules/oneos-autoprd-sync.mdc new file mode 100644 index 0000000..a679800 --- /dev/null +++ b/.cursor/rules/oneos-autoprd-sync.mdc @@ -0,0 +1,42 @@ +--- +description: 改原型跟进 AutoPRD;定稿写功能变更;云效建需求先跑 AutoPRD;状态推进建同名任务 +alwaysApply: true +--- + +# OneOS AutoPRD 全局同步 + +## 改原型 + +修改 `src/prototypes/**` 且涉及**行为、文案、流程、判定或验收**时,同一轮必须跟进 **oneos-autoprd**: + +1. 更新 `.spec/requirements-prd.md`(用户故事:起点 → 怎么运作 → 闭环)。 +2. 更新 `annotation-source.json` **顶层** `directory.nodes` 中「产品需求说明(PRD)」。 +3. 同步 `src/resources/prd/-autoprd.md`(若有)。 + +纯样式且无产品语义变化:可跳过全量重写。 + +## 需求定稿 + +用户回复含 `需求定稿` / `定稿` / `确认定稿` / `本次定稿` 时: + +1. 按 skill `references/release-changelog.md` 汇总**自上次定稿**的功能/逻辑变更。 +2. 追加到 PRD `## 10. 功能变更记录`(最新在上);更新 `.spec/autoprd-baseline.json`。 +3. **不要**写布局/UI/表结构类条目。 + +## 云效建需求 + +使用 `yunxiao-requirement-lifecycle` **创建或完善**需求描述时,**先**跑 `oneos-autoprd`: + +- 需求说明 ← AutoPRD 正文 +- 更新内容 ← 第 10 章自上次定稿以来的条目(无则首版/无增量) +- 旧更新内容 →「更新内容·历史」 + +## 云效状态 → 同名任务(AutoPRD 规则,不改云效 Skill) + +需求推进至 **分析中 / 设计中 / 待开发** 时,按 `references/yunxiao-stage-tasks.md`: + +- 建与需求**同名**任务并正式关联 +- 标签:分析 / 设计 / 交付(或开发) +- 创建时间口径沿用需求 +- 分析中、设计中负责人=创建人;待开发负责人=**何斐** +- 同阶段已有未取消关联任务则复用,不重复建 diff --git a/.cursor/rules/yunxiao-record-requirement-fast-path.mdc b/.cursor/rules/yunxiao-record-requirement-fast-path.mdc new file mode 100644 index 0000000..d250731 --- /dev/null +++ b/.cursor/rules/yunxiao-record-requirement-fast-path.mdc @@ -0,0 +1,17 @@ +--- +description: 云效「记录需求」须先点选优先级/推进至;统一运营管理平台走快路径,禁止默认参数与盲目探测 +globs: +alwaysApply: true +--- + +# 云效记录需求 · 参数门禁与快路径 + +当用户使用 `$yunxiao-requirement-lifecycle` / 「记录需求」,且涉及优先级、推进至时: + +1. **必须先拿到明确选择**:优先级(紧急/高/中/低)、推进至(分析中/设计中/设计完成/开发中)、**标签(从云效标签 catalog 点选,可多选)**。优先 `AskQuestion`;不可用则用 Plan 确认并由用户写明 A+B+C。 +2. **禁止**在未选择时默认「中 + 分析中」并自动建单。「批准/Implement 计划」≠ 已完成点选。 +3. 项目为**统一运营管理平台**(含 PC 端别名)时:先读 + `.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-fast-path.md` 与 + `.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-runtime-ids.json`(标签清单见同目录 `oneos-pc-tag-catalog.md`),按已验证 API/ID 执行;禁止重复探测 create URL。 +4. **标签禁止**按 `lines.ts` / 业务条线说明自动推断;只能用用户点选的 catalog 名。API 打标失败时 UI 兜底;仍失败则停下请用户回复标签名,不得声称已成功。 +5. 优先 API(创建时写入描述;标签用 `PATCH … propertyKey=tag`);浏览器仅作标签失败或状态连跳兜底;少截图、同失败不循环重试。 diff --git a/.cursor/skills/AutoVUL/SKILL.md b/.cursor/skills/AutoVUL/SKILL.md new file mode 100644 index 0000000..41c2365 --- /dev/null +++ b/.cursor/skills/AutoVUL/SKILL.md @@ -0,0 +1,139 @@ +--- +name: AutoVUL +description: >- + Generates OneOS PC version update logs from a Yunxiao (云效) iteration name by + loading all linked requirements, or from a pasted changelog list. Use when testers + or PMs say AutoVUL, 版本更新日志, 按迭代生成更新日志, 发版公告, or give a 迭代名称. +--- + +# AutoVUL(OneOS PC 版本更新日志) + +面向**测试人员发版**:输入云效**迭代名称** → 拉取该迭代关联需求 → 自动生成对外版本更新日志。 + +也可手动粘贴变更清单(兜底)。**预计维护时长**由人工单独通知,不写进日志。 + +## 何时使用 + +- 测试说:`按 $AutoVUL` / `生成版本更新日志` / `按迭代 xxx 出更新日志` +- 输入含云效「迭代名称」 +- 发版前需要统一文案口径 + +## 主流程(测试 · 按迭代,默认) + +完整取数步骤见 [references/yunxiao-sprint.md](references/yunxiao-sprint.md)。 + +```text +1. 确认云效项目(缺省:统一运营管理平台PC端;可改) +2. 向用户索取「迭代名称」(若口令里已带则直接用) +3. 在云效查找该迭代 → 立即反馈成功或失败(见下) +4. 成功:列出迭代内关联「需求」清单(编号 + 标题 + 状态)供用户快速核对 +5. 从每条需求提取变更素材(优先级见「素材优先级」) +6. 按本文格式分类、合并、润色 → 输出成稿正文 +``` + +### 迭代读取 · 成功 / 失败反馈(强制) + +查找迭代后**必须先反馈**,再继续或停止: + +| 结果 | 反馈文案(可微调,语义不变) | 下一步 | +|------|------------------------------|--------| +| 精确命中 1 个迭代 | `✅ 已找到迭代「{名称}」(状态:{状态}),关联需求 {N} 条。` | 继续拉需求并生成 | +| 0 个匹配 | `❌ 未找到迭代「{输入}」。请核对后重新输入迭代名称(可复制云效迭代标题)。` | **停止生成**;等待用户重输 | +| 多个模糊匹配 | `⚠️ 找到多个相似迭代:1)… 2)… 请回复序号或完整精确名称。` | 等用户选定后再继续 | +| 登录/权限失败 | `❌ 无法访问云效(未登录或无权限)。请在浏览器登录云效后重试,或改用手动清单。` | 停止;提示登录或兜底 | +| 迭代存在但 0 条需求 | `⚠️ 迭代「{名称}」已找到,但暂无关联需求。请确认规划后再试,或改用手动清单。` | 停止生成对外正文 | + +**失败后**:用户只需再发一次迭代名称(或纠正项目名),从步骤 2 重跑;不要要求用户重装 Skill。 + +### 素材优先级(每条需求) + +| 顺序 | 来源 | 用法 | +|------|------|------| +| 1 | 需求描述中的「更新内容」段(非「更新内容·历史」) | 首选,直接改写成日志条目 | +| 2 | AutoPRD「功能变更记录」中可对外部分 | 次选 | +| 3 | 需求标题 + 简短描述摘要 | 仅当前两者皆空时;价值句可合理补全 | +| — | 纯技术债、内部监控、未对用户开放 | **不写入**对外日志 | + +Bug 类需求(类型/标签含缺陷修复、或更新内容写「修复」)归入【Bug修复】;其余归【新功能】。边界不清优先 Bug修复。 + +### 版本号与更新时间 + +| 字段 | 规则 | +|------|------| +| 版本号 | 优先从迭代名解析(如含 `V1.1.5` / `1.1.5`);解析不到再问用户 | +| 更新时间 | 用户提供则用;未提供可写迭代计划开始日的 `MM月DD日HH:MM`,或先问 | +| 产品线 | 默认 `OneOS PC版` | + +## 兜底流程(手动清单) + +无云效权限、或迭代为空时:用 [input-template.md](input-template.md) 粘贴清单,跳过迭代步骤,直接分类润色。 + +## 固定输出格式 + +```text +OneOS PC版V{主.次.修订}更新日志: +更新时间:{MM}月{DD}日{HH}:{MM} +---------------------------------------------------- +【新功能】 +1「{模块名}」{做了什么},{责任部门}可{用户价值 / 解决的问题} +2「{模块名}」{…} + +【Bug修复】 +3「{模块名}」{修复了什么},{责任部门}可{恢复的能力 / 避免的影响} +``` + +### 版式硬约束 + +| 项 | 规则 | +|----|------| +| 标题 | `OneOS PC版V` + 版本号 + `更新日志:`(如 `V1.1.5`,无空格) | +| 更新时间 | 仅开始时刻;`MM月DD日HH:MM`;**禁止**「预计x分钟内完成」 | +| 分隔线 | `----------------------------------------------------` | +| 分区 | 仅 `【新功能】` / `【Bug修复】`;空类整段省略 | +| 序号 | 全篇连续,从 1 起 | +| 模块名 | `「」`;与产品菜单正式名一致 | +| 对外正文 | **不写**需求号、缺陷号、迭代 ID(核对清单可另附) | +| 结尾 | 无签名、无「感谢使用」、无技术附录 | + +## 分类 / 合并 / 文案(成稿规则) + +| 归入【新功能】 | 归入【Bug修复】 | 不写进对外日志 | +|----------------|-----------------|----------------| +| 新增能力、新入口、新字段/流程、可感知体验优化 | 原有能力异常、报错、数据错、交互失效 | 技术债、无感性能、未开放灰度、仅内部监控 | + +- **同分区同模块**合并为一条,分点用 `;` +- **跨分区**同模块分栏各写一条 +- 每条含:做什么 + 用户价值 + 责任部门自然嵌入(`{部门}人员可…`) +- **禁止**「供…条线…使用」;部门对照 [module-role-mapping.md](module-role-mapping.md) +- 禁止编造迭代内不存在的能力;存疑跳过或标待确认(不进正式成稿) +- 单条建议 ≤100 字 + +排序:先新功能后修复;同分区全员影响优先;资金/合规修复置修复区最前。 + +## 输出纪律 + +1. **先**输出迭代读取反馈(✅/❌/⚠️) +2. 成功时:可先给「本迭代需求清单」短表(含编号,便于测试核对) +3. 再输出**纯文本成稿正文**(默认不要 Markdown 大标题包裹成稿) +4. 用户明确要求时,可在正文后附「分类依据 / 跳过项」 + +## 发布前自检 + +- [ ] 已反馈迭代查找结果;失败时未强行编造成稿 +- [ ] 成稿条目均可追溯到本迭代关联需求素材 +- [ ] 标题、时间行、分隔线正确;无预计时长 +- [ ] 同模块合并正确;每条含价值 + 部门自然嵌入 +- [ ] 对外正文无需求号/缺陷号/技术黑话;序号连续 + +## 给测试同事的口令 + +```text +按 $AutoVUL 生成版本更新日志。 +项目:统一运营管理平台PC端 +迭代名称:V1.1.5发版迭代 +更新时间:07月16日16:00 +``` + +迭代名写错时,按提示改完再发一次即可。 + +手动兜底见 [input-template.md](input-template.md);成稿范例见 [examples.md](examples.md)。 diff --git a/.cursor/skills/AutoVUL/examples.md b/.cursor/skills/AutoVUL/examples.md new file mode 100644 index 0000000..eef8c46 --- /dev/null +++ b/.cursor/skills/AutoVUL/examples.md @@ -0,0 +1,77 @@ +# AutoVUL 成稿与交互范例 + +## 1. 迭代读取失败 → 重输 + +**用户:** + +```text +按 $AutoVUL,迭代名称:V1.1.5 +``` + +**Agent:** + +```text +❌ 未找到迭代「V1.1.5」。请核对后重新输入迭代名称(可复制云效迭代标题)。 +``` + +**用户:** + +```text +V1.1.5发版迭代 +``` + +**Agent:** + +```text +✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 3 条: +1. ONEOSP-201 车辆管理运营城市展示优化 +2. ONEOSP-202 还车应结款租金计算修复 +3. ONEOSP-203 业务管理新增租赁物流统计 + +OneOS PC版V1.1.5更新日志: +更新时间:07月16日16:00 +---------------------------------------------------- +【新功能】 +1「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源 +2「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况 + +【Bug修复】 +3「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确 +``` + +## 2. 完整成稿风格基准(手动/迭代素材均可) + +> 责任部门对照:`module-role-mapping.md`(OneOS 业务条线说明) + +```text +OneOS PC版V1.1.5更新日志: +更新时间:07月16日16:00 +---------------------------------------------------- +【新功能】 +1「加氢站管理」升级新版加氢站模块,采购部可统一维护签约站与非签约站信息,签约加氢站可便捷管理站点日常运营 +2「车辆管理」优化车辆「运营城市」展示,运维部人员可快速辨认车辆所属运营区域及数据来源 +3「审批中心」支持按客户名称搜索关联合同,业务员、法务部审批时可快速定位合同;还车应结款添加照片附件后,业务管理组、安全部、运维部、能源部会签人员可在审批流中查看完整附件 +4「保险采购」支持保比价单按车辆单独勾选并分开提交审批,运维部、安全部人员可按车辆分批推进投保审批 +5「故障管理」增加「故障来源」字段展示,运维部人员可区分上报渠道并跟进处理 +6「证照管理」特种设备使用登记证/使用标识照片支持OCR识别,运维部、安全部人员可减少手工录入、加快证照建档 +7「全局显示」统一各模块列表分页样式与交互,各模块操作人员跨页面浏览时体验一致、上手更快 +8「交还车管理」支持交车/还车签章文件在线预览,运维部、业务管理组、司机可现场核对签章文件;提交时增加胎纹数据必填校验,减少漏填导致的后续结算争议 +9「还车应结款」调整安全部待办区域权限,安全部人员仅处理职责范围内的还车应结款审批 +10「业务管理」新增租赁、物流业务统计模块,业务管理组、业务员可查看租赁经营数据,业务服务组、财务部可跟踪自营物流经营情况 + +【Bug修复】 +11「还车管理」修复签字单与还车检查单/还车应结款轮胎数据不一致,运维部、业务管理组人员还车验收与结算依据保持一致 +12「租赁合同」修复编辑合同提交时异常提示内容冗余,业务员、法务部人员可快速定位真正错误原因 +13「交还车管理」修复交车/还车签章客户名称关联错误,避免运维部、业务管理组、司机现场核对时客户信息张冠李戴 +14「车辆氢费明细」修复同客户不同生效时段、以及不同客户成本单价显示异常,能源部人员可准确核对氢费核算单价 +15「还车应结款」修复租金费用项重复及计算错误,业务管理组、安全部、运维部、能源部人员还车结算金额更准确 +``` + +## 对照要点 + +| 规则 | 体现 | +|------|------| +| 先反馈迭代结果 | 失败可重输;成功列出需求再成稿 | +| 无预计时长 | 更新时间仅 `07月16日16:00` | +| 对外无需求号 | 成稿正文不出现 ONEOSP-xxx | +| 部门自然嵌入 | `运维部人员可…`,无「供…条线使用」 | diff --git a/.cursor/skills/AutoVUL/input-template.md b/.cursor/skills/AutoVUL/input-template.md new file mode 100644 index 0000000..ab42bb7 --- /dev/null +++ b/.cursor/skills/AutoVUL/input-template.md @@ -0,0 +1,69 @@ +# AutoVUL · 输入模板 + +## A. 测试人员 · 按云效迭代(推荐) + +复制下面口令发给 AI(改迭代名即可): + +```text +按 $AutoVUL 生成版本更新日志。 +项目:统一运营管理平台PC端 +迭代名称: +更新时间: +``` + +### 期望反馈 + +- 成功:`✅ 已找到迭代「…」,关联需求 N 条` → 随后输出成稿 +- 失败:`❌ 未找到迭代「…」。请重新输入迭代名称` → 改名后再发一次即可 + +**说明**:预计维护时长由人工单独通知,不写入版本更新日志。 + +--- + +## B. 手动清单(云效不可用时兜底) + +### 版本元信息 + +| 字段 | 填写 | 示例 | +|------|------|------| +| 产品线 | | OneOS PC版 | +| 版本号 | | V1.1.5 | +| 更新开始时间 | | 07月16日16:00 | +| 发布范围 | 全量 / 灰度(说明) | 全量 | +| 是否对外发布 | 是 / 否 | 是 | + +### 新功能(原始材料,可粗糙) + +| 模块 | 做了什么 | 解决什么问题 / 用户故事 | 业务条线 | 责任部门 | 是否写入日志 | +|------|----------|-------------------------|----------|----------|--------------| +| 例:加氢站管理 | 升级新版模块 | 统一维护站点 | 能源业务条线 | 采购部、加氢站 | 是 | +| | | | | | | + +### Bug修复(原始材料,可粗糙) + +| 模块 | 原问题现象 | 修复后表现 | 对用户的影响 | 业务条线 | 责任部门 | 是否写入日志 | +|------|------------|------------|--------------|----------|----------|--------------| +| 例:还车应结款 | 租金费用项重复 | 金额正确 | 避免结算偏差 | 租赁业务条线 | 业务管理组、安全部、运维部、业务管理组-能源部 | 是 | +| | | | | | | | + +### 明确不要写入对外日志 + +- (如:重构、依赖升级、仅内部监控、未上线配置) + +### 粘贴给 AI + +```text +请按 $AutoVUL 用下方手动清单生成成稿(跳过云效迭代)。 +要求:同模块合并;每条含「做什么 + 用户价值」;责任部门自然融入;只输出正文。 + +【版本元信息】 +- 产品线:OneOS PC版 +- 版本号:V +- 更新开始时间: + +【新功能】 +(粘贴) + +【Bug修复】 +(粘贴) +``` diff --git a/.cursor/skills/AutoVUL/module-role-mapping.md b/.cursor/skills/AutoVUL/module-role-mapping.md new file mode 100644 index 0000000..2deea13 --- /dev/null +++ b/.cursor/skills/AutoVUL/module-role-mapping.md @@ -0,0 +1,60 @@ +# 模块 → 业务条线 / 责任部门对照 + +> 数据源:OneOS「业务条线说明」页责任部门定义(租赁 / 能源 / 运维 / 安全 / 自营) +> 生成更新日志时,**适用对象**须从此表取 `roles`,不得自造部门名。 + +## 五条业务条线 + +| ID | 条线名称 | +|----|----------| +| lease | 租赁业务条线 | +| energy | 能源业务条线 | +| ops | 运维管理条线 | +| safety | 安全管理条线 | +| self-operated | 自营业务条线 | + +## 本版变更模块对照 + +| 更新日志模块名 | 业务条线 | lines.ts 模块 | 责任部门(roles,原文) | +|----------------|----------|---------------|-------------------------| +| 加氢站管理 | 能源业务条线 | 加氢站管理 | 采购部、加氢站 | +| 车辆管理 | 运维管理条线 | 验车入库 / 车辆管理(资产底座) | 运维部、采购部 | +| 审批中心 | 跨条线 | 租赁合同 + 还车应结款(审批场景) | 业务员、法务部、业务管理组、安全部、运维部、业务管理组-能源部 | +| 保险采购 | 运维管理条线 | 证照 / 保险(车辆资产关联) | 运维部、安全部 | +| 故障管理 | 运维管理条线 | 故障管理 | 运维部、司机 | +| 租赁合同 | 租赁业务条线 | 租赁合同 | 业务员、法务部 | +| 证照管理 | 运维管理条线 | 证照管理 | 运维部、安全部 | +| 全局显示 | 全系统 | 系统介绍(五条条线) | 业务管理组、运维部、采购部、安全部、法务部、财务部、业务管理组-能源部、业务服务组 | +| 交还车管理 | 运维管理条线 | 交车管理 + 还车管理 | 运维部、业务管理组、司机 | +| 还车应结款 | 租赁业务条线 | 还车应结款 | 业务管理组、安全部、运维部、业务管理组-能源部 | +| 还车管理 | 运维管理条线 | 还车管理 | 运维部、业务管理组 | +| 车辆氢费明细 | 能源业务条线 | 车辆氢费明细 | 业务管理组-能源部 | +| 业务管理 · 租赁统计 | 租赁业务条线 | 租赁业务台账 / 客户管理 | 业务管理组、业务员 | +| 业务管理 · 物流统计 | 自营业务条线 | 自营台账 | 业务服务组、财务部 | + +## 写法规则 + +- 部门名与 `roles` **完全一致**(`业务管理组-能源部` 对外可简写「能源部人员」) +- **禁止**「供…业务条线…使用」「供…人员使用」 +- **推荐**:`{部门}人员可…` / `{部门}可…` / `{部门}审批时可…` +- 同模块合并后,多部门用顿号或分号自然串联 +- 找不到模块时:查 `lines.ts`;仍无则标「待产品确认」不进成稿 + +## 自然表述示例 + +| 模块 | 生硬(禁止) | 自然(推荐) | +|------|--------------|--------------| +| 车辆管理 | 供运维管理条线运维部使用 | 运维部人员可快速辨认车辆所属运营区域 | +| 加氢站管理 | 供能源业务条线采购部、加氢站使用 | 采购部可统一维护站点信息,加氢站可便捷管理日常运营 | +| 还车应结款 | 供租赁业务条线业务管理组…使用 | 业务管理组、安全部、运维部、能源部还车结算金额更准确 | + +## 常见错误(禁止) + +| 错误写法 | 正确写法 | +|----------|----------| +| 氢能运营业务人员 | 采购部、加氢站(能源 · 加氢站管理) | +| 车辆运营及资产管理业务人员 | 运维部、采购部(运维 · 车辆管理) | +| 各业务条线审批人员 | 业务员、法务部、…(按审批场景枚举) | +| 保险采购业务人员 | 运维部、安全部 | +| 还车结算业务人员 | 业务管理组、安全部、运维部、业务管理组-能源部 | +| 经营分析及管理部门 | 业务管理组、业务服务组(分租赁 / 自营) | diff --git a/.cursor/skills/AutoVUL/references/yunxiao-sprint.md b/.cursor/skills/AutoVUL/references/yunxiao-sprint.md new file mode 100644 index 0000000..93fc195 --- /dev/null +++ b/.cursor/skills/AutoVUL/references/yunxiao-sprint.md @@ -0,0 +1,81 @@ +# 云效迭代 → 需求清单(AutoVUL 取数) + +测试人员只提供**迭代名称**;Agent 负责在云效定位迭代、拉取关联需求、抽出更新素材。 + +## 安全边界(强制) + +- **禁止**在对话、Skill、日志、成稿中写入 Token、密码、Cookie、私钥。 +- 优先复用**已登录浏览器会话**操作云效 Projex(与 `yunxiao-requirement-lifecycle` 一致)。 +- 若团队已配置本机环境变量访问 OpenAPI,可走 API;Token 只读自环境,永不回显。 + +建议环境变量(可选,名称可按团队调整): + +| 变量 | 用途 | +|------|------| +| `YUNXIAO_TOKEN` | 个人访问令牌(`x-yunxiao-token`) | +| `YUNXIAO_DOMAIN` | 服务接入点域名 | +| `YUNXIAO_ORG_ID` | 组织 ID(中心版) | +| `YUNXIAO_PROJECT_ID` | 默认项目 ID(或运行时按项目名解析) | + +## 默认项目 + +口令未指定时:`统一运营管理平台PC端`。项目名不精确时先问清再查迭代。 + +## 判定顺序:解析迭代 + +| 顺序 | 条件 | 结果 | +|------|------|------| +| 1 | 用户未给迭代名称 | 询问:「请输入云效迭代名称(可复制标题)」;不继续 | +| 2 | 项目未确定 | 询问精确项目名;不继续 | +| 3 | 云效不可访问 | `❌ 无法访问云效…`;不继续 | +| 4 | 名称精确匹配 1 条迭代 | `✅ 已找到迭代…`;进入拉需求 | +| 5 | 0 条 | `❌ 未找到迭代…请重新输入`;不继续 | +| 6 | >1 条相似 | `⚠️ 多个相似…请回复序号或精确名称`;等用户选定 | +| 7 | 命中但关联需求 0 | `⚠️ …暂无关联需求`;不生成对外正文 | + +匹配规则:先**全等**(去首尾空格);全等失败再做包含/去空格模糊;模糊多条必须让用户确认,禁止擅自挑一个。 + +## 拉取关联需求 + +只收集工作项类型为**需求(Req)**且正式关联到该迭代的项。 + +- **浏览器路径**:打开项目 → 迭代 → 该迭代规划/工作项列表 → 筛选类型=需求 → 导出或逐条打开描述。 +- **OpenAPI 路径(示意)**: + 1. `ListSprints`:按项目列迭代,用 `name` 匹配用户输入,得到 `sprintId` + 2. `SearchWorkitems`:`category=Req`,`spaceId=项目Id`,`conditions` 中按字段 `sprint` CONTAINS `[sprintId]`(以租户实际 fieldIdentifier 为准;可先在需求列表页过滤后复制 `workitem/list` 的 conditions) + 3. 分页直到收齐(关注响应头 `x-total`) + 4. 需要正文时再 `GetWorkitem` 取 description + +每条需求最少记录: + +| 字段 | 说明 | +|------|------| +| serialNumber | 如 ONEOSP-123(仅用于核对清单,**不进对外成稿**) | +| subject | 标题 | +| status | 状态展示名 | +| description | 用于抽取「更新内容」 | +| category / type / labels | 辅助判断新功能 vs 修复 | + +## 从描述抽「更新内容」 + +1. 定位标题「更新内容」或「## 更新内容」段落 +2. **不要**把「更新内容·历史」并入本版素材(历史仅备查) +3. 若无「更新内容」:用标题 + 需求说明摘要;仍无有效产品语义则跳过并在内部备注 + +## 成功反馈后的核对清单(推荐短表) + +在生成成稿前输出,例如: + +```text +✅ 已找到迭代「V1.1.5发版迭代」(进行中),关联需求 5 条: +1. ONEOSP-101 保险采购比价分车提交 +2. ONEOSP-102 交还车签章预览 +… +正在按 AutoVUL 格式生成更新日志… +``` + +用户若立刻纠正「某条不要进日志」,从素材中剔除后再出成稿。 + +## 与发版任务关系 + +本 Skill **只读**迭代与需求,**不创建**【发版】任务、不改云效状态。发版挂链仍用 `yunxiao-requirement-lifecycle`(如「准备发布」口令 / A08)。 diff --git a/.cursor/skills/oneos-autoprd/SKILL.md b/.cursor/skills/oneos-autoprd/SKILL.md new file mode 100644 index 0000000..a10d84f --- /dev/null +++ b/.cursor/skills/oneos-autoprd/SKILL.md @@ -0,0 +1,183 @@ +--- +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 + +为 OneOS 业务模块生成**产品经理可读、可评审、可排期**的需求说明,并**自动挂到 Axhub Make 标注工具 → 原型目录**。 + +另支持:**需求定稿**时汇总功能/逻辑变更记录;与云效组合时提供「需求说明 + 更新内容」;需求进入**分析中 / 设计中 / 待开发**时**自动创建与需求同名的关联任务**(规则见下,写在本 Skill,不改云效 Skill)。 + +颗粒度对齐「保险采购」全模块 PRD:讲清做什么、谁用、故事点、正逆向、流程图与关键业务逻辑;**不写**表结构、接口、字段代码名、文件路径、实现清单。 + +## 何时使用(含自动触发) + +**主动调用时** + +- AutoPRD、OneOS 需求说明、整模块 PRD、故事点 + 流程图 + +**改原型时必须自动跟进(全局规则)** + +- 正在修改 `src/prototypes//` 下页面、交互、文案、判定、验收相关内容 +- 同一轮交付内同步更新 PRD Markdown + 标注目录,不得只改代码 + +**需求定稿时(强制)** + +- 用户回复含:`需求定稿` / `定稿` / `确认定稿` / `本次定稿` +- 执行 [references/release-changelog.md](references/release-changelog.md):写第 10 章 + 更新基线 + +**云效建需求 / 完善需求时(强制组合)** + +- 与 `$yunxiao-requirement-lifecycle` 一起使用时:先跑本 Skill,再写云效描述 +- **需求说明** ← PRD 正文(或浓缩交付口径 + 关键章节) +- **更新内容** ← 第 10 章中**自上次定稿以来**的条目(无增量则「首版定稿 / 本轮无功能增量」) + +**云效需求推进至分析中 / 设计中 / 待开发时(强制)** + +- 无论口令来自本 Skill 还是云效 Skill,只要本轮把需求推到上述状态,就执行 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md) +- 自动建**与需求同名**的任务并正式关联;标签与创建时间口径沿用需求;分析中/设计中负责人=创建人,待开发负责人=**何斐** + +不要用本 Skill 替代:需求探索访谈、设计比稿、纯样式微调(无产品语义变化时可跳过全量重写)。 + +## 工作流 + +### 主流程(写/同步 PRD) + +1. **定模块**:确认 OneOS 模块名与 `src/prototypes//`。 +2. **读上下文(只取产品语义)** + - 用户说明、已确认口径、原型标注、`.spec/`、业务条线说明(`lines.ts`) + - 忽略实现细节;字段名/接口改写成业务语言。 +3. **收敛边界**:做什么 / 不做什么、外部依赖、与其它模块关系。 +4. **按模板成文**:下方「输出结构」;缺关键信息最多问 1~2 个问题,其余写「假设」。 +5. **落盘 + 标注同步(强制)** — 见 [references/annotation-sync.md](references/annotation-sync.md)。 + + | 顺序 | 动作 | + |------|------| + | A | 写/更新 `src/prototypes//.spec/requirements-prd.md` | + | B | 写/更新 `src/resources/prd/-autoprd.md` | + | C | 更新 `annotation-source.json` **顶层** `directory.nodes`(PRD 全文 + 推荐分章);禁止只写 `data.directory` | + | D | 若存在 `scripts/sync-annotation-directory.mjs`,执行之 | + +6. **交付说明**:路径、标注目录入口、故事点合计、开放问题/假设。 + +### 定稿流程(关键字触发) + +见 [references/release-changelog.md](references/release-changelog.md)。摘要: + +1. 读 PRD + `.spec/autoprd-baseline.json` +2. 汇总自上次定稿以来的**功能/逻辑**变更(排除样式/UI/表结构) +3. 追加到 `## 10. 功能变更记录`(最新在上) +4. 更新基线 JSON + 标注目录 +5. 若同时发云效:本次定稿块 →「更新内容」;旧段 →「更新内容·历史」 + +### 云效交接格式(给 yunxiao skill 直接粘贴) + +```markdown +## 原型链接 +<对象存储或预览 URL> + +## 需求说明 + + +## 更新内容 +<第 10 章本次定稿块;无则「首版定稿」或「本轮无功能/逻辑增量」> + +## 更新内容·历史 +<以往更新内容倒序,勿删除> +``` + +### 云效阶段任务(状态推进时强制) + +见 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)。摘要: + +| 需求状态 | 任务标签 | 负责人 | 标题 | +|----------|----------|--------|------| +| 分析中 | 分析 | 需求创建人 | 与需求同名 | +| 设计中 | 设计 | 需求创建人 | 与需求同名 | +| 待开发 | 交付(或开发) | 何斐 | 与需求同名 | + +正式关联需求;创建时间口径沿用需求;同阶段未取消任务不重复建。 + +## 写作硬约束 + +**必须写** + +- 一句话定位 + 目标 / 非目标 +- 模块边界(含 mermaid 总览更好) +- 角色与目标(角色名优先对齐业务条线说明) +- **用户故事**(业务条线说明口径)+ Epic 级**故事点(SP)**粗估 +- 分功能**正向**与**逆向/边界** +- 至少 1~2 个 **mermaid** 流程图 +- **关键业务逻辑**(业务话) +- 验收清单 + 「交付口径」一段话 +- 定稿后:**功能变更记录**(第 10 章) + +**禁止写** + +- 数据库表、字段名、接口路径、代码路径、组件名、存储 key +- 研发实现指令(可写「正式环境由审批中心回写」这类业务依赖) +- 在变更记录里写样式/UI/表结构优化 + +## 用户故事口径(强制 · 对齐业务条线说明) + +真相源:原型 **业务条线说明**(`lease-business-line-overview` / `lines.ts`)。 +每条能力用「责任部门 → **起点** → **怎么运作** → **闭环**」叙述;**不要**用「作为…我希望…」宽表作主叙述。 + +| 块 | 写什么 | +|----|--------| +| 角色 | 谁负责、谁协同 | +| 起点 | 谁在什么前提下启动 | +| 怎么运作 | 有序步骤,含跨角色协作 | +| 关键结果 | 可选标签 | +| 闭环 | 业务终点与可追溯性 | + +可选:`US-xx`、压缩句「作为…我想…以便…」、规模 S/M/L 或 SP(仅排期,不替代主叙述)。 + +## 输出结构 + +完整模板:[references/template.md](references/template.md)。 + +```markdown +# <模块名> · 产品需求说明(全模块) + +## 1. 一句话与目标 +## 2. 模块边界(最重要) +## 3. 用户与角色 +## 4. 用户故事与故事点(业务条线说明口径) +## 5. 功能模块说明(正向 / 逆向) +## 6. 关键业务逻辑(必须对齐) +## 7. 总览流程图 +## 8. 验收清单 +## 9. 交付口径 +## 10. 功能变更记录 ← 定稿后维护;日常改原型不强制每改必写 +``` + +## 质量自检 + +- [ ] 产品经理不看代码也能评审 +- [ ] 用户故事为起点 / 怎么运作 / 闭环 +- [ ] `.spec/requirements-prd.md` 已更新 +- [ ] 标注目录「产品需求说明(PRD)」已同步且正文一致 +- [ ] 定稿时:第 10 章 + `autoprd-baseline.json` 已更新 +- [ ] 推进至分析中/设计中/待开发时:同名任务已创建或复用,正式关联,负责人正确 +- [ ] 无表结构 / 接口 / 代码路径;变更记录无样式/UI 废话 + +## 参考 + +- 定稿变更日志:[references/release-changelog.md](references/release-changelog.md) +- 云效阶段任务:[references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md) +- 标注同步细则:[references/annotation-sync.md](references/annotation-sync.md) +- 章节模板:[references/template.md](references/template.md) +- 故事示例:[references/granularity-example.md](references/granularity-example.md) +- 业务条线:`src/prototypes/lease-business-line-overview/lines.ts` +- 复杂判定规格:配合项目规则 `business-logic-documentation` +- 云效组合:`$yunxiao-requirement-lifecycle`(建需求时先本 Skill) diff --git a/.cursor/skills/oneos-autoprd/references/annotation-sync.md b/.cursor/skills/oneos-autoprd/references/annotation-sync.md new file mode 100644 index 0000000..2cdd7c6 --- /dev/null +++ b/.cursor/skills/oneos-autoprd/references/annotation-sync.md @@ -0,0 +1,98 @@ +# Axhub 标注目录同步(强制) + +使用 OneOS AutoPRD 时,除资源库归档外,**必须**把 PRD 挂到 Axhub Make 标注工具的「原型目录」里,用户才能在右侧面板直接打开。 + +## 落点(三份必须一致) + +| # | 路径 | 作用 | +|---|------|------| +| 1 | `src/prototypes//.spec/requirements-prd.md` | **主真相**:产品可读 PRD 全文 | +| 2 | `src/prototypes//annotation-source.json` → **顶层** `directory.nodes` | **标注目录入口**:面板里可见的 Markdown/PRD | +| 3 | `src/resources/prd/-autoprd.md` | 资源库归档(可选但默认写) | + +可选:复杂判定另写 `.spec/.md`,目录里再挂一条,并在 PRD 里链过去(与 `business-logic-documentation` 一致)。 + +## ⚠️ 目录必须写在文档顶层(常见踩坑) + +Wire format(与 `prototype-annotation` 一致): + +```json +{ + "documentVersion": 1, + "format": "axhub-annotation-source", + "data": { "version": 2, "prototypeName": "...", "nodes": [] }, + "markdownMap": {}, + "assetMap": {}, + "directory": { "nodes": [] } +} +``` + +| 正确 | 错误 | +|------|------| +| 顶层 `directory.nodes` | 只写在 `data.directory` | +| `PrototypeAnnotationHost` / Make 读的是 `source.directory` | 写在 `data` 下时右侧「原型目录」为空 | + +若误写在 `data.directory`:迁移到顶层 `directory`,并删除 `data.directory`,避免双源。 + +## 目录节点写法(推荐) + +在说明根 `folder` 的 `children` **靠前**插入或更新: + +```json +{ + "type": "markdown", + "id": "-doc-prd", + "title": "产品需求说明(PRD)", + "markdownPath": ".spec/requirements-prd.md", + "markdown": "<与 .spec/requirements-prd.md 相同的全文>" +} +``` + +### 分章节 PRD(推荐,便于右侧目录浏览) + +在「产品需求说明(PRD)」旁增加文件夹 **「PRD 分章」**,按 PRD 的 `##` 一级标题拆成多个 `markdown` 子节点(标题用章节名,正文为该章全文)。专题规格(`.spec/.md`)放在 **「专题规格」** 文件夹。 + +参考结构: + +```text +folder <模块>说明 + markdown 产品需求说明(PRD) ← 全文 + folder PRD 分章 + markdown 1. 一句话与目标 + markdown 2. 模块边界 + … + folder 专题规格 + markdown +``` + +有 `scripts/sync-annotation-directory.mjs` 的原型:改 PRD / `.spec` 后**必须执行**该脚本再生目录。 + +定稿后第 10 章「功能变更记录」必须进入 PRD 全文节点;若有「PRD 分章」,同步该章节点。详见 [release-changelog.md](release-changelog.md)。 + +约定: + +- `id`:`<短前缀>-doc-prd`(如 `ipc-doc-prd`、`wb-doc-prd`、`h5-va-doc-prd`) +- `title`:优先「产品需求说明(PRD)」;已有「PRD」可保留不改名 +- **同时写** `markdownPath` + `markdown`:构建可内联路径,运行时目录也能直接读到正文 +- 若已有同 id / 同标题节点:只更新正文与 path,不新建重复入口 +- 遵守 `prototype-annotation-layout`:目录以 `markdown` 为主,不要用 `link`/`route` 顶替 PRD + +## 增量改原型时怎么更新 + +1. 根据改动的文件路径确定 ``。 +2. 读现有 `.spec/requirements-prd.md`(没有则按 AutoPRD 模板新建)。 +3. 只改受影响章节(故事、正逆向、关键逻辑、验收);其余保留。 +4. 回写 `.spec` + `resources/prd` + **顶层** `annotation-source.json` → `directory`(正文与文件一致;含分章节点)。 +5. 若原型有 `scripts/sync-annotation-directory.mjs`,改完后执行它。 + +## 可跳过全量重写的情况 + +纯样式、无文案/无流程/无判定变化的微调:不必重写整份 PRD,但若改了用户可见文案或交互结果,仍须同步对应段落与目录节点。 + +## 验收 + +- [ ] `.spec/requirements-prd.md` 存在且为最新 +- [ ] `annotation-source.json` **顶层**有 `directory.nodes`(不是只在 `data` 下) +- [ ] 标注目录能打开「产品需求说明(PRD)」且内容与文件一致 +- [ ] 若有「PRD 分章」:各章可单独打开 +- [ ] 用户故事仍为业务条线说明口径(起点 / 怎么运作 / 闭环) diff --git a/.cursor/skills/oneos-autoprd/references/granularity-example.md b/.cursor/skills/oneos-autoprd/references/granularity-example.md new file mode 100644 index 0000000..5126671 --- /dev/null +++ b/.cursor/skills/oneos-autoprd/references/granularity-example.md @@ -0,0 +1,58 @@ +# 颗粒度标杆与用户故事口径 + +## 用户故事:对齐「业务条线说明」页面 + +页面:`/prototypes/lease-business-line-overview` +数据:`src/prototypes/lease-business-line-overview/lines.ts` + +每个能力卡片的产品叙述结构: + +```text +责任部门(roles) + → 起点(start) + → 怎么运作(process[],有序) + → 关键结果(outcomes,可选) + → 闭环(closure) +``` + +AutoPRD 第 4 章必须用同一套结构写用户故事,而不是「作为…我希望…」宽表。 + +### 示例(保险采购 · 交车合规,示意) + +#### US · 保险有效才允许交车 +- **角色**:采购部、运维部 +- **起点**:采购或运营维护车辆交强险与商业险台账,作为交车前置合规数据。 +- **怎么运作**: + 1. 在保险采购维护一车一档险种台账(录入 / 识别 / 导入)。 + 2. 交车时系统校验交强 + 商业是否均为正常或临期。 + 3. 不合规则拦截交车并给出可理解原因。 +- **关键结果**:`允许交车` / `禁止交车 · 保险异常` +- **闭环**:交车合规与台账一致、可追溯;异常车辆不能进入履约运营。 +- **排期(可选)**:US-17 · 规模 M · 作为采购/运维,我想维护交强+商业险并在交车时校验,以便不合规车辆无法交车。 + +### 反例(不要作为主叙述) + +| 编号 | 用户故事 | SP | +|------|----------|----| +| B1 | 作为运营,我能看到失败原因 | 2 | + +↑ 信息太扁,缺少起点、协作步骤与闭环结果。 + +--- + +## AutoPRD 其余层次(仍要对齐) + +1. 一句话 + 目标/非目标 +2. 模块边界(独立业务线写清「不自动同步」) +3. 角色表(命名对齐业务条线) +4. **用户故事:起点 / 怎么运作 / 闭环** + SP/规模 +5. 分模块正逆向 +6. 关键业务逻辑 +7. mermaid 流程图 +8. 验收清单 +9. 交付口径 + +## 刻意不写 + +- 表结构、接口、代码路径 +- 用实现指令代替业务闭环描述 diff --git a/.cursor/skills/oneos-autoprd/references/release-changelog.md b/.cursor/skills/oneos-autoprd/references/release-changelog.md new file mode 100644 index 0000000..998e9bf --- /dev/null +++ b/.cursor/skills/oneos-autoprd/references/release-changelog.md @@ -0,0 +1,78 @@ +# 功能变更记录(定稿触发) + +用户回复**需求定稿关键字**后,必须生成「自上次定稿以来」的功能变更日志,追加到 PRD **最下方**,并更新定稿基线。 + +## 触发关键字(命中任一) + +`需求定稿` · `定稿` · `确认定稿` · `本次定稿` + +(可带模块名,如「保险采购需求定稿」。) + +## 写什么 / 不写什么 + +| 要写(产品发版视角) | 不写 | +|----------------------|------| +| 哪个功能做了什么修改 | 布局、页面样式、UI 设计优化 | +| 变更了什么业务逻辑 / 流程 / 验收口径 | 「优化了哪些表」「改了哪些字段/接口」 | +| 用户可感知的交互结果变化 | 纯样式、动效、无语义文案微调 | + +条目口吻示例: + +- 「识别失败」:失败数可点击查看失败文件与原因;仅失败时也可打开明细 +- 「比价单」:审批通过后仍须在保单管理另行录入正式保单(强调不自动同步) + +## 落点 + +1. **PRD 文末章节**(强制) + `src/prototypes//.spec/requirements-prd.md` → `## 10. 功能变更记录` + 新定稿块插在该章**最上方**(倒序:最新在上);历史块保留。 + +2. **定稿基线**(强制) + `src/prototypes//.spec/autoprd-baseline.json` + +```json +{ + "prototypeId": "insurance-procurement", + "lastConfirmedAt": "2026-07-21T12:00:00+08:00", + "lastConfirmedLabel": "定稿 · 2026-07-21", + "summaryBullets": ["…", "…"] +} +``` + +3. **标注目录**:同步更新「产品需求说明(PRD)」全文节点;若有「PRD 分章」,增加或更新「功能变更记录」分章。 + +4. **云效交接**(若本轮同时走云效):把**本次定稿块**正文作为需求描述「更新内容」;旧「更新内容」挪到「更新内容·历史」。 + +## 定稿工作流 + +1. 确认 ``(从对话 / 打开文件 / 用户指名推断)。 +2. 读现有 PRD 与 `autoprd-baseline.json`(无基线则本轮视为**首版定稿**)。 +3. 收集自 `lastConfirmedAt` 以来的产品语义变更: + 对话确认口径 → 批注/操作说明 → PRD 新旧差异 →(可选)相关提交说明。 + 过滤掉样式/表结构类改动。 +4. 若无功能增量:仍写定稿块,正文为「本轮无功能/逻辑增量(仅样式或未达产品语义变更)」或「首版定稿」。 +5. 追加 `### 定稿 · YYYY-MM-DD` + 条目列表到第 10 章顶部。 +6. 更新 `autoprd-baseline.json`;执行 annotation-sync。 +7. 向用户回报:基线时间、条目数、PRD 路径;若需发云效,提示可用口令。 + +## PRD 章节格式 + +```markdown +## 10. 功能变更记录 + +> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。 + +### 定稿 · 2026-07-21 + +- 「能力名」:做了什么 / 逻辑如何变 +- … + +### 定稿 · 2026-07-10 + +- … +``` + +## 与日常改原型的关系 + +- **改原型过程中**:按 AutoPRD 主流程增量更新第 1–9 章;**不必**每改一次就写第 10 章。 +- **用户说定稿时**:才汇总写入第 10 章并刷新基线。 diff --git a/.cursor/skills/oneos-autoprd/references/template.md b/.cursor/skills/oneos-autoprd/references/template.md new file mode 100644 index 0000000..449b8bf --- /dev/null +++ b/.cursor/skills/oneos-autoprd/references/template.md @@ -0,0 +1,163 @@ +# OneOS AutoPRD 输出模板 + +复制下列结构填写。方括号为占位。**第 4 章用户故事必须用业务条线说明口径。** + +```markdown +# <模块名> · 产品需求说明(全模块) + +## 1. 一句话与目标 + +**一句话** +<用一句话说明模块解决什么> + +**要解决的问题** +- … + +**本期目标** +1. … +2. … + +**非目标(本期不做)** +- … + +## 2. 模块边界(最重要) + +(可选 mermaid:模块与外部系统关系) + +| 业务线 / 子域 | 做什么 | 不做什么 | +|---------------|--------|----------| +| … | … | … | + +**外部依赖(产品口径)** +| 依赖方 | 交互方式 | 产品要求 | +|--------|----------|----------| +| … | … | … | + +**关键约束** +- … + +## 3. 用户与角色 + +| 角色 | 主要目标 | +|------|----------| +| … | … | + +> 角色名优先与「业务条线说明」一致(如业务管理组、采购部、运维部)。 + +## 4. 用户故事与故事点(业务条线说明口径) + +> 故事点 / 规模供排期参考,可按团队基准调整。合计约 **N SP**(或 S/M/L 汇总说明)。 + +按 Epic 或能力单元分组。**每一条**按下面四块写(对齐业务条线说明页:责任部门 → 起点 → 怎么运作 → 闭环): + +### Epic A · <名称>(约 N SP) + +#### A1 · <能力标题> +- **角色**:… +- **起点**:… +- **怎么运作**: + 1. … + 2. … + 3. … +- **关键结果**(可选):`可…` / `禁止…` +- **闭环**:… +- **排期(可选)**:US-xx · 规模 S/M/L · 「作为…,我想…,以便…」 + +#### A2 · <能力标题> +… + +### Epic B · … + +## 5. 功能模块说明(正向 / 逆向) + +### 5.x <功能名> + +**正向** +1. … +2. … + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| … | … | + +## 6. 关键业务逻辑(必须对齐) + +### 6.x <规则名> + +用编号优先级、表格或短列表写清业务规则。 + +## 7. 总览流程图 + +```mermaid +flowchart TB + home[入口] --> a[能力A] + home --> b[能力B] +``` + +## 8. 验收清单(产品 / 测试) + +**子域 A** +- [ ] … + +## 9. 交付口径 + +> <一段可贴需求单开头的浓缩描述> + +## 10. 功能变更记录 + +> 产品经理原型发版记录。仅记功能与业务逻辑变更;不含样式/UI/表结构。 +> 由「需求定稿」关键字触发追加;日常改原型不强制每改必写。详见 [release-changelog.md](release-changelog.md)。 + +### 定稿 · YYYY-MM-DD + +- 「<能力名>」:<做了什么 / 逻辑如何变> +- … +``` + +成文后**必须**同步 Axhub 标注目录(见 [annotation-sync.md](annotation-sync.md)): + +1. `src/prototypes//.spec/requirements-prd.md` +2. `annotation-source.json` → 目录节点「产品需求说明(PRD)」(`markdownPath` + `markdown`) +3. `src/resources/prd/-autoprd.md` +4. 定稿时另写:`src/prototypes//.spec/autoprd-baseline.json` + +## 用户故事写法(对照业务条线说明) + +业务条线说明页字段 → AutoPRD 字段: + +| 页面 | AutoPRD | +|------|---------| +| 责任部门标签 | **角色** | +| 起点 | **起点** | +| 怎么运作(有序列表) | **怎么运作** | +| 结果色签 | **关键结果**(可选) | +| 闭环 | **闭环** | + +示例口吻(摘自业务条线风格,非照抄某模块): + +- **起点**:采购部维护交强与商业险台账,作为交车合规的前置数据。 +- **怎么运作**:1. 运营录入或识别保单… 2. 运维交车时校验… +- **闭环**:核心险种有效则允许交车;异常则拦截并提示原因,过程可追溯。 + +**不要**用下面这种宽表作为第 4 章唯一形态: + +| 编号 | 作为…我希望… | SP | +|------|--------------|----| + +「作为…我想…以便…」只允许作为**排期可选一行**,主叙述必须是起点/怎么运作/闭环。 + +## 正逆向写法约定 + +- **正向**:用户成功完成任务的最短路径 +- **逆向**:取消、关闭、禁用、冲突状态、不可操作项 +- 审批类:写清「本页只读 / 在外部系统办理」 + +## 规模与故事点 + +| 规模 | 建议含义 | 约 SP | +|------|----------|-------| +| S | 轻交互 / 列表展示 | 1~2 | +| M | 标准流程 + 筛选详情 | 3~5 | +| L | 状态机 / 跨模块 / 批量识别 | 6+ | diff --git a/.cursor/skills/oneos-autoprd/references/yunxiao-stage-tasks.md b/.cursor/skills/oneos-autoprd/references/yunxiao-stage-tasks.md new file mode 100644 index 0000000..09cfe27 --- /dev/null +++ b/.cursor/skills/oneos-autoprd/references/yunxiao-stage-tasks.md @@ -0,0 +1,44 @@ +# 云效阶段任务自动创建(AutoPRD 负责) + +本规则写在 **oneos-autoprd**,不依赖改写 `yunxiao-requirement-lifecycle`。 +凡本 Skill 参与的云效操作中,需求推进到下列状态时,**同一轮必须**建同名任务并正式关联需求。 + +## 触发状态 + +| 需求推进至 | 任务标签(右侧基础字段) | 负责人 | 任务标题 | +|------------|--------------------------|--------|----------| +| **分析中** | `分析` | **需求创建人**姓名 | 与需求标题**完全同名** | +| **设计中** | `设计` | **需求创建人**姓名 | 与需求标题**完全同名** | +| **待开发** | `交付`(若项目无「交付」标签则用 `开发`) | **何斐** | 与需求标题**完全同名** | + +## 强制字段与关系 + +1. **正式关联需求**:父子或关联项;只挂标题不算关联。创建后必须在界面上能看到需求 ↔ 任务关系。 +2. **标签**:写入任务「标签」基础字段(见上表);标题前缀不能代替标签。 +3. **创建时间沿用需求**:任务的创建时间(及标签侧可见的创建时间口径)尽量与**需求创建时间**一致。 + - 平台允许改创建时间 / 用 API 指定时:写成需求的创建时间。 + - 平台不允许:在任务描述首行注明 `创建时间口径=需求创建时间(YYYY-MM-DD HH:mm)`,并在回报里说明「平台限制未改系统创建时间」。 +4. **幂等**:同一需求、同一阶段标签下,已存在**未取消**且已正式关联的同名任务 → **不新建**,只校验负责人/标签/关联是否正确,缺则补齐。 +5. **顺序**:先确认需求已进入目标状态(或本轮动作将写入该状态),再创建/补齐任务;回报时给出任务编号与链接。 + +## 执行步骤(apply 时) + +```text +1. 读取需求:标题、创建人、创建时间、当前/目标状态、已关联任务列表 +2. 按上表确定本轮阶段标签与负责人 +3. 查重(需求ID + 阶段标签 + 未取消) +4. 无则创建:标题=需求标题;标签=阶段标签;负责人=上表;创建时间口径=需求创建时间;正式关联需求 +5. 有则校验并修补标签/负责人/关联 +6. 回报:需求状态、任务编号、负责人、是否新建/复用 +``` + +## 不在本规则内 + +- 不自动建「测试 / 发版」任务(除非用户另行要求)。 +- 不因样式类 PRD 变更触发建任务;仅**需求状态**进入分析中/设计中/待开发时触发。 +- 云效生命周期细则、流水线、缺陷闭环仍可由 `$yunxiao-requirement-lifecycle` 配合执行;**本建任务逻辑以 AutoPRD 本文为准**。 + +## 与定稿 / 需求说明的关系 + +- 写「需求说明 / 更新内容」仍按 AutoPRD 主流程与定稿流程。 +- 推进到待开发且需要描述时:先 AutoPRD 文档,再改状态,再按本文件建任务(负责人何斐)。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/SKILL.md b/.cursor/skills/yunxiao-requirement-lifecycle/SKILL.md new file mode 100644 index 0000000..4dede21 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/SKILL.md @@ -0,0 +1,157 @@ +--- +name: yunxiao-requirement-lifecycle +description: >- + Audit, design, configure, migrate, test, troubleshoot, and document Alibaba Cloud + Yunxiao/Projex requirement lifecycle automation. Use for short conversational Chinese + commands such as 记录需求、确认需求、需求定稿、开始分析、开始开发、提交测试、测试完成、 + 发布成功 and 验收通过; OneOS delivery or legacy stage-task model. When creating or + refreshing OneOS product requirements, first call oneos-autoprd for 需求说明 and + 更新内容 (functional changelog since last 定稿), then write Yunxiao description. + Also covers role handoffs, Codeup, Testhub, pipelines, release acceptance, migration, + and Chinese runbooks without exposing credentials or changing production pipelines unexpectedly. +--- + +# Yunxiao Requirement Lifecycle + +Use this as the single entry skill. Load only the internal modules needed for the request. Installing the skill does not change Yunxiao; live transitions start only after an authorized `apply` and verified `test` run. + +## Route to modules + +Read each selected file completely before acting: + +| Request scope | Required module | +|---|---| +| OneOS终态模型、`【交付】`主任务、多开发子任务、迭代测试/发版、Y01–Y40、A01–A10、旧规则迁移 | [references/oneos-terminal-flow.md](references/oneos-terminal-flow.md) | +| Lifecycle statuses, requirement labels, related tasks, R01–R12, stage entry/completion | [references/lifecycle-rules.md](references/lifecycle-rules.md) | +| Design/development/test task creation, task states, rollups, TK01–TK08 | [references/task-lifecycle.md](references/task-lifecycle.md) | +| Test plans, case results, failed cases, bugs, retest, TC01–TC03, DF01–DF03 | [references/test-defect-closure.md](references/test-defect-closure.md) | +| Repositories, `dev`/`develop`, branches, commits, MRs, multi-repository gate, CI01 | [references/codeup-integration.md](references/codeup-integration.md) | +| test/prod pipelines, Docker callback, release changes, acceptance, CI02–CI03, L01–L02 | [references/pipeline-release.md](references/pipeline-release.md) | +| Live audit/apply/test, browser execution, evidence, diagnosis, rollback, governance | [references/live-operations.md](references/live-operations.md) | +| 日常口语化操作,使用“记录需求、开始分析、安排开发、提交测试、复测通过、发布成功”等短口令 | [references/simple-role-commands.md](references/simple-role-commands.md) | +| **统一运营管理平台**日常「记录需求」加速执行(已验证 ID / API / 状态三跳) | [references/oneos-pc-fast-path.md](references/oneos-pc-fast-path.md) + [assets/oneos-pc-runtime-ids.json](assets/oneos-pc-runtime-ids.json) | +| **统一运营管理平台**「记录需求到云效」完整可复制提示词(A/B/C 门禁 + 30 标签) | [references/oneos-pc-record-requirement-prompt.md](references/oneos-pc-record-requirement-prompt.md) | +| 需求进入分析中/设计中/待开发时自动建同名任务、关联需求、负责人与时间沿用 | [references/auto-stage-task.md](references/auto-stage-task.md) | +| 产品、分析、设计、开发、评审、测试、缺陷、运维、验收等角色的节点沟通话术和交接回报 | [references/role-trigger-prompts.md](references/role-trigger-prompts.md) | +| Final Chinese report | [references/report-template.md](references/report-template.md) | + +## 记录需求 · 参数门禁与加速(强制) + +当口令为「记录需求」且项目为统一运营管理平台(含 PC 端别名)时: + +1. **先读** [references/oneos-pc-fast-path.md](references/oneos-pc-fast-path.md) 与 [assets/oneos-pc-runtime-ids.json](assets/oneos-pc-runtime-ids.json),按快路径执行,禁止重复探测 create URL / 优先级 ID。 +2. 若用户要求 Plan/单选优先级、推进至与标签:必须先拿到明确 **A+B+C**;**禁止**未选时默认「中 + 分析中」并建单。「批准计划」≠ 已选 A+B+C。完整用户口令见 [references/oneos-pc-record-requirement-prompt.md](references/oneos-pc-record-requirement-prompt.md)。 +3. 优先 API 建单(创建时写入 `document`)、同名任务与打标签(`PATCH` `propertyKey=tag`);浏览器仅用于标签 API 失败或状态连跳兜底。统一运营管理平台标签从 `assets/oneos-pc-tag-catalog.md` 选择器点选,禁止按 `lines.ts` 自动映射。 + +## AutoPRD 组合(强制 · OneOS 产品需求) + +当请求涉及**创建产品类需求、写入/刷新需求描述、快轨待开发、完善需求说明/更新内容**,或用户口令含「记录需求(带模块/原型)」「需求已经确定」「需求定稿」并要进云效时: + +1. **先**加载并执行项目/全局的 **`oneos-autoprd`** skill(勿跳过)。 +2. 用其产出填充云效需求描述: + - **需求说明** ← AutoPRD 正文(产品语言;不写表结构/接口/代码) + - **更新内容** ← AutoPRD 第 10 章「功能变更记录」中自上次定稿以来的条目;无则「首版定稿」或「本轮无功能/逻辑增量」 + - 已有「更新内容」时:旧段挪到 **更新内容·历史**(倒序追加,勿删) +3. 原型发版增量以 AutoPRD 功能变更记录为准;PC 整包发版公告仍可另用 `AutoVUL`。 +4. 对象存储链接、主任务、状态推进等仍按 `oneos-terminal-flow.md` / `simple-role-commands.md`。 + +`A02` / `A02B` 在 OneOS 项目上的默认实现方式即为调用 AutoPRD,不得只写占位文案。 + +## 阶段同名任务(强制 · AI apply) + +凡将需求推进到 **分析中 / 设计中 / 待开发**(含口令「开始分析」「开始设计」「快速开发」「需求已经确定…待开发」等),必须先读并执行 [references/auto-stage-task.md](references/auto-stage-task.md): + +| 状态 | 任务 | 负责人 | +|---|---|---| +| 分析中 | 与需求**同名**;标签`分析`;正式关联需求 | 需求**创建人** | +| 设计中 | 与需求**同名**;标签`设计`;正式关联需求 | 需求**创建人** | +| 待开发 | 与需求**同名**;标签`交付`(或阶段模型下`开发`);正式关联需求 | 固定**何斐** | + +时间:任务计划开始(及可写的创建时间)**沿用需求**的计划开始/创建时间。查重幂等,禁止同阶段重复建单。 + +First identify `flow_model`: + +- `stage_tasks`: use R01–R12 and TK01–TK08; read the five domain modules plus `live-operations.md` for a full audit. +- `oneos_delivery`: read `oneos-terminal-flow.md`, `codeup-integration.md`, `test-defect-closure.md`, `pipeline-release.md`, and `live-operations.md`. + +For a narrow diagnosis, read the relevant domain module plus `live-operations.md`. Do not blend both models in one project or load unrelated modules merely because they exist. + +## Classify authority + +- `audit`: inspect and report only. +- `plan`: prepare a project profile and change plan only. +- `apply`: create or modify only explicitly named projects, labels, rules, and test assets. +- `test`: exercise approved rules with isolated artifacts and collect evidence. +- `document`: produce a Chinese runbook from verified facts. + +Treat ambiguous requests as `audit` or `plan`. Documentation never authorizes live changes. + +When the user uses a write verb defined in `simple-role-commands.md`—for example `记录需求`、`确认需求`、`开始分析`、`开始设计`、`开始开发`、`提交测试`、`复测通过`、`发布成功` or `验收通过`—treat it as `apply` authorization only for the current exact project and named work item. If the project is not exact, ask only for the project before writing. Query verbs such as `查状态`、`为什么没流转`、`下一步` and `给我方案` remain `audit` or `plan`. + +Whenever apply changes a requirement status to `分析中`、`设计中` or `待开发`, also apply [references/auto-stage-task.md](references/auto-stage-task.md) in the same turn (same-name task, formal link, assignee and time rules) before reporting success. + +## Build the project profile + +1. Choose exactly one template and copy it outside the skill: + - `stage_tasks`: `assets/project-profile.template.json`. + - `oneos_delivery`: `assets/oneos-project-profile.template.json`. +2. Keep one independent object in `projects` for every target project; duplicate the template object when needed. +3. Replace observed statuses, rule instances, repositories, branches, pipelines, test plans, defect workflows, release evidence, and control dispositions. +4. Never add usernames, passwords, cookies, tokens, OTPs, Webhook secrets, or private keys. +5. Validate before applying: + +```text +python scripts/profile_tool.py validate +python scripts/profile_tool.py render --output + +python scripts/oneos_profile_tool.py validate +python scripts/oneos_profile_tool.py render --output +``` + +Do not apply while placeholders remain or validation fails. + +## Completeness contract + +For `stage_tasks`, account for every control for every project independently: + +- `R01`–`R12`: requirement lifecycle and manual gates. +- `TK01`–`TK08`: stage-task creation, task-state transitions, requirement rollups, and idempotency. +- `TC01`–`TC03`: test-case execution, failed-case/defect association, and retest result update. +- `DF01`–`DF03`: fixed handoff, tester-pass closure, and tester-fail reopen. +- `CI01`–`CI03`: multi-repository coverage, callback safeguards, and environment/release separation. +- `L01`–`L02`: legacy immediate-close rule disposition. + +Record the actual R01–R12 `rule_instances`, TK01–TK08 `task_rule_instances`, and all 31 `control_coverage` entries. Valid modes are `manual`, `native_rule`, `integration_bridge`, `disabled_legacy`, and `not_supported`. `not_supported` needs an owner and fallback and is never equivalent to automated. + +Completeness covers the requirement lifecycle, stage-task lifecycle and rollups, Testhub closure, Codeup assets, pipeline/release evidence, and legacy-close controls. Notifications, SLA reminders, generic automatic assignment, and unrelated custom-field synchronization remain outside scope unless explicitly added to `adjunct_automations`. + +For `oneos_delivery`, account for all 48 controls independently: + +- `Y01`–`Y16`, `Y20`–`Y22`, `Y30`–`Y35`, `Y40`: native/mirrored lifecycle controls. +- `A01`, `A02`, `A02B`, `A03`–`A10`: AI/Webhook bridge controls. +- `TC01`–`TC03`, `DF01`–`DF03`, `CI01`–`CI03`, `L01`–`L02`: test, defect, code/release separation, and legacy-close controls. + +Do not disable the old stage rules until the OneOS migration readiness gate passes: required requirement statuses exist, the main-task identity is filterable or procedurally exclusive, task workflows are ready, bridge owners and idempotency exist, and rollback evidence is saved. `L01` and `L02` remain disabled because they bypass acceptance. + +## Global safety rules + +- Reuse the authenticated browser session or approved semantic connector. +- Preserve unrelated rules and user edits. +- Never modify an existing production pipeline unless the user authorizes that exact change. +- Clone a test pipeline before callback experiments and modify only the clone. +- Require the expected current state in every automatic transition. +- Separate observed evidence from proposals and inference. +- Wait 5–30 seconds for asynchronous events and inspect execution logs. +- Stop at CAPTCHA, OTP, login approval, permission elevation, protected-branch ambiguity, or indistinguishable similarly named resources. + +## Required outputs + +Return only the applicable artifacts: + +1. Observed inventory and current-versus-target matrix. +2. Per-project R01–R12 and TK01–TK08 rule-instance inventory with a 31-control ledger. +3. Live change log with reopen verification. +4. Test result per transition: `通过`、`异步通过`、`未触发`、`误触发` or `阻塞`. +5. Remaining risks, manual gates, and rollback steps. +6. When requested, a role-and-trigger communication playbook with copyable prompts, required evidence, next owner, and stop conditions. +7. For daily commands, answer briefly with the work item, actual state change, created/linked asset, blocker if any, and the exact next short command. diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/agents/openai.yaml b/.cursor/skills/yunxiao-requirement-lifecycle/agents/openai.yaml new file mode 100644 index 0000000..ca6e380 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "云效需求生命周期自动化" + short_description: "使用口语化短口令驱动云效需求开发测试发布闭环并自动核查" + default_prompt: "使用 $yunxiao-requirement-lifecycle,把当前项目设为统一运营管理平台PC端,然后通过“记录需求、开始开发、提交测试、验收通过”等短口令协作。" diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-runtime-ids.json b/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-runtime-ids.json new file mode 100644 index 0000000..2787bf4 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-runtime-ids.json @@ -0,0 +1,209 @@ +{ + "schema_version": 2, + "verified_at": "2026-07-21", + "verified_by_example": { + "requirement_serial": "ONEOS-88", + "requirement_identifier": "a34e700286d456ee74ddc16e58", + "note": "标签 API:PATCH /workitem/workitem/{id} propertyKey=tag operateType=COVER;全量标签来自 space/tag/search(spaceType=Space)" + }, + "project": { + "name_aliases": ["统一运营管理平台", "统一运营管理平台PC端", "统一运营管理平台 PC 端"], + "spaceIdentifier": "1280be963a5a2cc126a4118dca", + "customCode": "ONEOS", + "spaceType": "Project" + }, + "people": { + "wangmian": { + "displayName": "王冕", + "identifier": "6811df000601d2fea60144a9" + }, + "hefei": { + "displayName": "何斐", + "identifier": "695f0400562f09713f9c3a93" + } + }, + "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": "epoch_ms_string_china_noon", + "example": "Date.parse('YYYY-MM-DDT12:00:00+08:00') 转 String,避免 00:00 显示成前一天" + }, + "plan_end": { + "fieldIdentifier": "80", + "name": "计划完成时间" + }, + "tag": { + "fieldIdentifier": "tag", + "name": "标签", + "propertyKey": "tag" + } + }, + "status_ui_paths": { + "from_待处理": { + "分析中": ["分析中"], + "设计中": ["设计中"], + "设计完成": ["设计完成"], + "开发中": ["设计完成", "待开发", "开发中"], + "note": "待处理下拉无「开发中」;须按路径连跳。状态 identifier 未在 UI 路径中固化,优先 UI 三跳;有可靠 API 后再补 statusIdentifier。" + } + }, + "assignee_rules": { + "分析中": "creator", + "设计中": "creator", + "设计完成": "creator", + "开发中": "hefei", + "待开发": "hefei" + }, + "api": { + "create_workitem": { + "url": "https://devops.aliyun.com/projex/api/workitem/workitem?_input_charset=utf-8", + "methods": ["POST", "PUT"], + "do_not_use": ["/projex/api/workitem/workitem/create"], + "required_create_fields": [ + "subject", + "spaceType", + "spaceIdentifier", + "category", + "categoryIdentifier", + "workitemTypeIdentifier", + "workitemType", + "assignedTo" + ], + "document_on_create": { + "supported": true, + "shape": { "content": "", "formatType": "RICHTEXT" }, + "note": "创建时一并写入描述,避免再开 Cangjie 编辑器" + } + }, + "get_workitem": { + "url_template": "https://devops.aliyun.com/projex/api/workitem/workitem/{identifier}?_input_charset=utf-8" + }, + "list_workitem": { + "url": "https://devops.aliyun.com/projex/api/workitem/workitem/list?_input_charset=utf-8", + "method": "POST" + }, + "update_field_value": { + "url": "https://devops.aliyun.com/projex/api/workitem/workitem/updateWorkitemFieldValue?_input_charset=utf-8", + "method": "PATCH", + "note": "部分会话返回 InvalidWorkitem.NotFound;失败则改用详情页 UI onChange / 下拉点选,勿盲目重试超过 1 次" + }, + "update_document": { + "preferred": "create 时写入 document;若需补写,用详情页 onWorkItemEditorSubmit(html, 'RICHTEXT')", + "avoid": "对 Cangjie 编辑器直接改 DOM / 错误 onChange 形状(曾导致页面崩溃)" + }, + "list_tags": { + "url": "https://devops.aliyun.com/projex/api/workspace/space/tag/search?spaceType=Space&spaceIdentifier=1280be963a5a2cc126a4118dca&q=&_input_charset=utf-8", + "method": "GET", + "note": "注意 spaceType=Space(不是 Project)。刷新全量标签时用此接口覆盖 tags.catalog" + }, + "apply_tags": { + "url_template": "https://devops.aliyun.com/projex/api/workitem/workitem/{identifier}?_input_charset=utf-8", + "method": "PATCH", + "body_shape": { + "workitemIdentifier": "{identifier}", + "propertyKey": "tag", + "propertyValue": "{comma_separated_tag_identifiers}", + "operateType": "COVER" + }, + "note": "propertyValue 为标签 identifier 逗号拼接;COVER=覆盖整组。已验证于 ONEOS-88。" + } + }, + "tags": { + "selection_mode": "plan_or_askquestion_selector", + "do_not_match_from": ["lease-business-line-overview/lines.ts", "业务条线说明自动推断"], + "apply_strategy": "用户从 catalog 点选标签名 → 用 identifier 调 apply_tags API;失败 1 次再 UI 兜底;仍失败则停并请用户回标签名,禁止假装已打上", + "refresh_hint": "标签库变更时重新 GET list_tags 并更新本 catalog / fetched_at", + "fetched_at": "2026-07-21", + "source": "GET /projex/api/workspace/space/tag/search?spaceType=Space&spaceIdentifier=1280be963a5a2cc126a4118dca&q=", + "count": 30, + "by_name": { + "运维管理条线": "a611b2f2b7c3fd3623ba2d5f52", + "报表中心": "8428d15f45a5b34173202db758", + "能源管理": "3396ebb527358fe6f89fe42849", + "维修站管理": "e7b4da7984801e75630645f437", + "充电站管理": "6ea830485f28a0d8a6e14851a8", + "还车应结款": "4204960ce658c94b17abb7c5f6", + "交车应收款": "6e7a9127d0f70281ca65d335d9", + "加氢站管理": "6cdacae673f9e1fba96296f2ad", + "保险管理": "578ed5ba12bfcf274721e956f2", + "合同管理": "23873be81931cb0dc1bf87a456", + "租赁账单": "3e9433f571b20e80b02626d2de", + "供应商管理": "7b86b7c1f3b3da21b814c5a39d", + "客户管理": "dbf959a70bc979106dc78afa90", + "还车任务": "763de323cefee45e5270e4661e", + "安全培训": "76988b5f73ef515de5930d59b9", + "备件管理": "5d09c864767d898a4f8c0ca804", + "停车场管理": "7c522e2fb29213d7d390e78b97", + "备车管理": "22c6a3ed0e4bf95fd99bf95695", + "异动管理": "84b4333ec7c76d0fa2adcc47bf", + "调拨管理": "fa88c16c152c6fb5978e3b4339", + "上牌管理": "990af2fb716dd85568bc7019bf", + "替换车管理": "8d1decf30259b941016cadc9d2", + "还车管理": "1e0fce2d6929ff4ac13b310e97", + "交车管理": "b6947b5aca82a8c759612ba039", + "车辆管理": "cb71b6db9373d6d8c7aae452a5", + "审批中心": "338add796f2221bbc36dd77d35", + "故障管理": "ceb526a7343995577645317e9a", + "车辆年审": "5193165dad1e91a161502b54c2", + "维修管理": "f5202633870ff409e0ebc16d5f", + "工作台": "b62d21beac55c0b43389915eab" + }, + "catalog": [ + { "name": "运维管理条线", "identifier": "a611b2f2b7c3fd3623ba2d5f52", "color": "#49AEAC" }, + { "name": "报表中心", "identifier": "8428d15f45a5b34173202db758", "color": "#49AEAC" }, + { "name": "能源管理", "identifier": "3396ebb527358fe6f89fe42849", "color": "#49AEAC" }, + { "name": "维修站管理", "identifier": "e7b4da7984801e75630645f437", "color": "#49AEAC" }, + { "name": "充电站管理", "identifier": "6ea830485f28a0d8a6e14851a8", "color": "#49AEAC" }, + { "name": "还车应结款", "identifier": "4204960ce658c94b17abb7c5f6", "color": "#49AEAC" }, + { "name": "交车应收款", "identifier": "6e7a9127d0f70281ca65d335d9", "color": "#49AEAC" }, + { "name": "加氢站管理", "identifier": "6cdacae673f9e1fba96296f2ad", "color": "#49AEAC" }, + { "name": "保险管理", "identifier": "578ed5ba12bfcf274721e956f2", "color": "#49AEAC" }, + { "name": "合同管理", "identifier": "23873be81931cb0dc1bf87a456", "color": "#49AEAC" }, + { "name": "租赁账单", "identifier": "3e9433f571b20e80b02626d2de", "color": "#49AEAC" }, + { "name": "供应商管理", "identifier": "7b86b7c1f3b3da21b814c5a39d", "color": "#49AEAC" }, + { "name": "客户管理", "identifier": "dbf959a70bc979106dc78afa90", "color": "#49AEAC" }, + { "name": "还车任务", "identifier": "763de323cefee45e5270e4661e", "color": "#49AEAC" }, + { "name": "安全培训", "identifier": "76988b5f73ef515de5930d59b9", "color": "#49AEAC" }, + { "name": "备件管理", "identifier": "5d09c864767d898a4f8c0ca804", "color": "#49AEAC" }, + { "name": "停车场管理", "identifier": "7c522e2fb29213d7d390e78b97", "color": "#49AEAC" }, + { "name": "备车管理", "identifier": "22c6a3ed0e4bf95fd99bf95695", "color": "#49AEAC" }, + { "name": "异动管理", "identifier": "84b4333ec7c76d0fa2adcc47bf", "color": "#49AEAC" }, + { "name": "调拨管理", "identifier": "fa88c16c152c6fb5978e3b4339", "color": "#49AEAC" }, + { "name": "上牌管理", "identifier": "990af2fb716dd85568bc7019bf", "color": "#49AEAC" }, + { "name": "替换车管理", "identifier": "8d1decf30259b941016cadc9d2", "color": "#49AEAC" }, + { "name": "还车管理", "identifier": "1e0fce2d6929ff4ac13b310e97", "color": "#49AEAC" }, + { "name": "交车管理", "identifier": "b6947b5aca82a8c759612ba039", "color": "#49AEAC" }, + { "name": "车辆管理", "identifier": "cb71b6db9373d6d8c7aae452a5", "color": "#49AEAC" }, + { "name": "审批中心", "identifier": "338add796f2221bbc36dd77d35", "color": "#49AEAC" }, + { "name": "故障管理", "identifier": "ceb526a7343995577645317e9a", "color": "#49AEAC" }, + { "name": "车辆年审", "identifier": "5193165dad1e91a161502b54c2", "color": "#49AEAC" }, + { "name": "维修管理", "identifier": "f5202633870ff409e0ebc16d5f", "color": "#49AEAC" }, + { "name": "工作台", "identifier": "b62d21beac55c0b43389915eab", "color": "#49AEAC" } + ] + }, + "performance_rules": { + "prefer_api_over_browser": true, + "max_endpoint_probes_per_step": 1, + "screenshot_only_at": ["after_create", "after_final_status"], + "browser_fallback_for": ["tag_apply_when_api_fails", "status_multi_hop_when_api_unknown"] + } +} diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-tag-catalog.md b/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-tag-catalog.md new file mode 100644 index 0000000..f01c22f --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-pc-tag-catalog.md @@ -0,0 +1,50 @@ +# 统一运营管理平台 · 云效标签清单(选择器用) + +来源:`GET /projex/api/workspace/space/tag/search?spaceType=Space&spaceIdentifier=1280be963a5a2cc126a4118dca&q=` +抓取时间:2026-07-21 +机器可读:同目录 `oneos-pc-runtime-ids.json` → `tags` + +## 使用约定 + +- **记录需求**时标签由用户用选择器点选(`AskQuestion` / Plan ○),**不要**再按 `lines.ts` 业务条线说明自动推断。 +- Agent 用点选到的 `name` 查 `tags.by_name` → `identifier`,再 `PATCH` 打标。 +- 完整建单提示词(含 A/B/C 与 30 标签枚举):[references/oneos-pc-record-requirement-prompt.md](../references/oneos-pc-record-requirement-prompt.md) + +## 全量标签(30) + +| # | 标签名 | identifier | +|---|---|---| +| 1 | 运维管理条线 | `a611b2f2b7c3fd3623ba2d5f52` | +| 2 | 报表中心 | `8428d15f45a5b34173202db758` | +| 3 | 能源管理 | `3396ebb527358fe6f89fe42849` | +| 4 | 维修站管理 | `e7b4da7984801e75630645f437` | +| 5 | 充电站管理 | `6ea830485f28a0d8a6e14851a8` | +| 6 | 还车应结款 | `4204960ce658c94b17abb7c5f6` | +| 7 | 交车应收款 | `6e7a9127d0f70281ca65d335d9` | +| 8 | 加氢站管理 | `6cdacae673f9e1fba96296f2ad` | +| 9 | 保险管理 | `578ed5ba12bfcf274721e956f2` | +| 10 | 合同管理 | `23873be81931cb0dc1bf87a456` | +| 11 | 租赁账单 | `3e9433f571b20e80b02626d2de` | +| 12 | 供应商管理 | `7b86b7c1f3b3da21b814c5a39d` | +| 13 | 客户管理 | `dbf959a70bc979106dc78afa90` | +| 14 | 还车任务 | `763de323cefee45e5270e4661e` | +| 15 | 安全培训 | `76988b5f73ef515de5930d59b9` | +| 16 | 备件管理 | `5d09c864767d898a4f8c0ca804` | +| 17 | 停车场管理 | `7c522e2fb29213d7d390e78b97` | +| 18 | 备车管理 | `22c6a3ed0e4bf95fd99bf95695` | +| 19 | 异动管理 | `84b4333ec7c76d0fa2adcc47bf` | +| 20 | 调拨管理 | `fa88c16c152c6fb5978e3b4339` | +| 21 | 上牌管理 | `990af2fb716dd85568bc7019bf` | +| 22 | 替换车管理 | `8d1decf30259b941016cadc9d2` | +| 23 | 还车管理 | `1e0fce2d6929ff4ac13b310e97` | +| 24 | 交车管理 | `b6947b5aca82a8c759612ba039` | +| 25 | 车辆管理 | `cb71b6db9373d6d8c7aae452a5` | +| 26 | 审批中心 | `338add796f2221bbc36dd77d35` | +| 27 | 故障管理 | `ceb526a7343995577645317e9a` | +| 28 | 车辆年审 | `5193165dad1e91a161502b54c2` | +| 29 | 维修管理 | `f5202633870ff409e0ebc16d5f` | +| 30 | 工作台 | `b62d21beac55c0b43389915eab` | + +## 选择器文案(C 段) + +已并入 [oneos-pc-record-requirement-prompt.md](../references/oneos-pc-record-requirement-prompt.md)「用户口令」;此处仅作 identifier 对照表。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-project-profile.template.json b/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-project-profile.template.json new file mode 100644 index 0000000..633af24 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/assets/oneos-project-profile.template.json @@ -0,0 +1,119 @@ +{ + "schema_version": 1, + "flow_model": "oneos_delivery", + "organization": "请填写组织名称", + "work_item_type": "产品类需求", + "target_lifecycle": [ + "待处理", "已确认", "分析中", "分析完成", "设计中", "设计完成", + "待开发", "开发中", "开发完成", "待测试", "测试中", "测试完成", + "发布中", "发布完成", "发布失败", "已关闭" + ], + "task_labels": ["交付", "开发", "测试", "发版"], + "policies": { + "require_formal_relations": true, + "require_current_state_condition": true, + "allow_automatic_final_close_without_acceptance": false, + "require_nonzero_development_tasks": true, + "test_success_is_production_release": false, + "bridge_idempotency_key_components": ["项目ID", "需求ID", "动作类型"], + "require_signed_timestamped_replay_protected_callbacks": true + }, + "projects": [ + { + "name": "请填写项目名称", + "migration_phase": "P0", + "requirement_statuses_observed": ["请填写当前全部需求状态"], + "task_workflow_mode": "B", + "task_identity_mode": "task_label", + "native_rule_can_filter_related_task_identity": false, + "main_task_prefix": "【交付】", + "development_task_prefix": "【开发】", + "test_task_prefix": "【测试】", + "release_task_prefix": "【发版】", + "repositories": [ + { + "name": "请填写仓库名称", + "role": "frontend", + "integration_branch": "develop", + "codeup_integrated": true, + "required_for_mr_gate": true, + "evidence": "请填写观察证据" + } + ], + "test_plan": { + "name": "请填写测试计划名称", + "iteration_formally_related": true, + "cases_partitioned_by_requirement": true, + "fixed_defect_is_terminal": false, + "tester_retest_required": true, + "evidence": "请填写观察证据" + }, + "release": { + "release_task_formally_related_to_iteration": true, + "production_evidence_separate_from_test": true, + "acceptance_required_after_release": true, + "production_pipeline_unchanged": true, + "evidence": "请填写观察证据" + }, + "legacy_rules": [ + { + "name": "请填写旧规则显示名", + "disposition": "keep_until_ready", + "enabled": true, + "reason": "请填写保留、停用或替换原因", + "evidence": "请填写观察证据" + } + ], + "controls": [ + {"id":"Y01","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接创建并关联唯一主任务","evidence":"请填写观察证据"}, + {"id":"Y02","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步分析完成","evidence":"请填写观察证据"}, + {"id":"Y03","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步设计中","evidence":"请填写观察证据"}, + {"id":"Y04","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步设计完成","evidence":"请填写观察证据"}, + {"id":"Y05","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步待开发","evidence":"请填写观察证据"}, + {"id":"Y06","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步开发中","evidence":"请填写观察证据"}, + {"id":"Y07","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步开发完成","evidence":"请填写观察证据"}, + {"id":"Y08","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步待测试","evidence":"请填写观察证据"}, + {"id":"Y09","mode":"not_supported","enabled":false,"owner":"请填写负责人","fallback":"桥接或人工同步测试中","evidence":"请填写观察证据"}, + {"id":"Y10","mode":"not_supported","enabled":false,"owner":"请填写测试负责人","fallback":"测试负责人按用例和缺陷闭环同步测试完成","evidence":"请填写观察证据"}, + {"id":"Y11","mode":"not_supported","enabled":false,"owner":"请填写发布负责人","fallback":"发版任务提交后人工同步发布中","evidence":"请填写观察证据"}, + {"id":"Y12","mode":"not_supported","enabled":false,"owner":"请填写发布负责人","fallback":"核对生产发布证据后人工同步发布完成","evidence":"请填写观察证据"}, + {"id":"Y13","mode":"not_supported","enabled":false,"owner":"请填写发布负责人","fallback":"人工记录发布失败及原因","evidence":"请填写观察证据"}, + {"id":"Y14","mode":"manual","enabled":true,"owner":"请填写产品负责人","fallback":"产品验收后人工关闭","evidence":"请填写观察证据"}, + {"id":"Y15","mode":"not_supported","enabled":false,"owner":"请填写项目管理员","fallback":"只保留一个权威镜像方向","evidence":"请填写观察证据"}, + {"id":"Y16","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"快轨时桥接同步主任务","evidence":"请填写观察证据"}, + {"id":"Y20","mode":"native_rule","enabled":true,"owner":"请填写研发负责人","fallback":"开发负责人人工开始任务和需求","evidence":"请填写观察证据"}, + {"id":"Y21","mode":"integration_bridge","enabled":false,"owner":"请填写研发负责人","fallback":"人工核对全部开发任务和MR","evidence":"请填写观察证据"}, + {"id":"Y22","mode":"integration_bridge","enabled":false,"owner":"请填写项目管理员","fallback":"人工创建并关联唯一测试任务","evidence":"请填写观察证据"}, + {"id":"Y30","mode":"integration_bridge","enabled":false,"owner":"请填写测试负责人","fallback":"人工同步待测试","evidence":"请填写观察证据"}, + {"id":"Y31","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"测试负责人开始测试","evidence":"请填写观察证据"}, + {"id":"Y32","mode":"integration_bridge","enabled":false,"owner":"请填写测试负责人","fallback":"按需求用例包人工验收","evidence":"请填写观察证据"}, + {"id":"Y33","mode":"integration_bridge","enabled":false,"owner":"请填写发布负责人","fallback":"人工提交发版任务并同步范围","evidence":"请填写观察证据"}, + {"id":"Y34","mode":"integration_bridge","enabled":false,"owner":"请填写流水线负责人","fallback":"人工核对生产发布成功","evidence":"请填写观察证据"}, + {"id":"Y35","mode":"integration_bridge","enabled":false,"owner":"请填写流水线负责人","fallback":"人工记录生产发布失败","evidence":"请填写观察证据"}, + {"id":"Y40","mode":"manual","enabled":true,"owner":"请填写产品负责人","fallback":"人工关联迭代","evidence":"请填写观察证据"}, + {"id":"A01","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"人工上传并回写链接","evidence":"请填写观察证据"}, + {"id":"A02","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"人工维护更新内容历史","evidence":"请填写观察证据"}, + {"id":"A02B","mode":"not_supported","enabled":false,"owner":"请填写产品负责人","fallback":"人工维护需求说明","evidence":"请填写观察证据"}, + {"id":"A03","mode":"integration_bridge","enabled":false,"owner":"请填写项目管理员","fallback":"人工建需求和唯一主任务","evidence":"请填写观察证据"}, + {"id":"A04","mode":"integration_bridge","enabled":false,"owner":"请填写研发负责人","fallback":"人工拆分开发任务","evidence":"请填写观察证据"}, + {"id":"A05","mode":"integration_bridge","enabled":false,"owner":"请填写研发负责人","fallback":"人工核对MR并完成开发任务","evidence":"请填写观察证据"}, + {"id":"A06","mode":"integration_bridge","enabled":false,"owner":"请填写项目管理员","fallback":"人工建唯一测试任务","evidence":"请填写观察证据"}, + {"id":"A07","mode":"integration_bridge","enabled":false,"owner":"请填写测试负责人","fallback":"人工核对用例与缺陷闭环","evidence":"请填写观察证据"}, + {"id":"A08","mode":"integration_bridge","enabled":false,"owner":"请填写发布负责人","fallback":"人工建发版任务和更新说明","evidence":"请填写观察证据"}, + {"id":"A09","mode":"integration_bridge","enabled":false,"owner":"请填写流水线负责人","fallback":"人工回写发布成败","evidence":"请填写观察证据"}, + {"id":"A10","mode":"manual","enabled":true,"owner":"请填写产品负责人","fallback":"产品验收后人工关闭","evidence":"请填写观察证据"}, + {"id":"TC01","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"逐条执行并记录用例结果","evidence":"请填写观察证据"}, + {"id":"TC02","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"人工正式关联失败用例和缺陷","evidence":"请填写观察证据"}, + {"id":"TC03","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"复测后更新原用例状态","evidence":"请填写观察证据"}, + {"id":"DF01","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"已修复继续等待复测","evidence":"请填写观察证据"}, + {"id":"DF02","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"复测通过后关闭缺陷并通过用例","evidence":"请填写观察证据"}, + {"id":"DF03","mode":"manual","enabled":true,"owner":"请填写测试负责人","fallback":"复测失败后重开缺陷并保持用例失败","evidence":"请填写观察证据"}, + {"id":"CI01","mode":"manual","enabled":true,"owner":"请填写研发负责人","fallback":"人工核对全部前后端MR","evidence":"请填写观察证据"}, + {"id":"CI02","mode":"not_supported","enabled":false,"owner":"请填写流水线负责人","fallback":"使用test副本评审回调POC","evidence":"请填写观察证据"}, + {"id":"CI03","mode":"manual","enabled":true,"owner":"请填写发布负责人","fallback":"人工区分test、生产发布和验收","evidence":"请填写观察证据"}, + {"id":"L01","mode":"disabled_legacy","enabled":false,"owner":"请填写项目管理员","fallback":"保持发布完成等待验收","evidence":"请填写观察证据"}, + {"id":"L02","mode":"disabled_legacy","enabled":false,"owner":"请填写项目管理员","fallback":"保持发布完成等待验收","evidence":"请填写观察证据"} + ] + } + ] +} diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/assets/project-profile.template.json b/.cursor/skills/yunxiao-requirement-lifecycle/assets/project-profile.template.json new file mode 100644 index 0000000..3f6c25a --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/assets/project-profile.template.json @@ -0,0 +1,148 @@ +{ + "schema_version": 3, + "organization": "请填写组织名称", + "work_item_type": "产品类需求", + "lifecycle": [ + "待处理", + "已确认", + "分析中", + "分析完成", + "设计中", + "设计完成", + "待开发", + "开发中", + "开发完成", + "测试中", + "测试完成", + "完成发布", + "已完成" + ], + "requirement_labels": ["分析", "设计", "开发", "测试"], + "automation_policy": { + "require_current_state_condition": true, + "inspect_rule_order_and_cascades": true, + "allow_automatic_final_close_without_acceptance": false, + "zero_related_items_are_complete": false, + "development_start_event_candidates": ["添加关联分支", "正式关联代码资产"], + "require_code_assets_linked_to_requirement_and_development_task": true, + "stage_task_creation_idempotency_key_components": ["项目ID", "需求ID", "阶段类型"] + }, + "test_closure_policy": { + "accepted_case_states": ["已通过"], + "rejected_case_states": ["未执行", "待测试", "阻塞", "未通过"], + "defect_fixed_is_terminal": false, + "require_tester_retest": true + }, + "projects": [ + { + "name": "请填写项目名称", + "work_item_type": "产品类需求", + "release_status": "完成发布", + "final_status": "已完成", + "labels_observed": ["分析", "设计", "开发", "测试"], + "repositories": [ + { + "name": "请填写仓库名称", + "role": "frontend", + "integration_branch": "dev", + "codeup_integrated": true, + "required_for_mr_gate": true, + "evidence": "请填写仓库与需求正式关联的观察证据" + } + ], + "test_pipeline": { + "name": "请填写test流水线名称", + "environment": "test", + "is_clone_for_callback_poc": true, + "original_pipeline_unchanged": true, + "docker_success_callback_enabled": false, + "callback_security": { + "signed": false, + "timestamp_checked": false, + "replay_protected": false, + "idempotent_by_execution_id": false, + "failure_retry_tested": false + }, + "evidence": "请填写流水线观察证据" + }, + "release_evidence": { + "mode": "native_release_change", + "target_environment": "请填写目标发布环境", + "test_success_is_production_release": false, + "evidence": "请填写发布证据" + }, + "test_plan": { + "name": "请填写测试计划名称", + "requirement_formally_related": true, + "case_states_observed": ["未执行", "已通过", "未通过", "阻塞"], + "evidence": "请填写测试计划与用例状态证据" + }, + "defect_workflow": { + "fixed_status": "已修复", + "fixed_is_terminal": false, + "tester_retest_required": true, + "closed_status": "请填写缺陷真实关闭状态", + "reopen_status": "请填写缺陷复测失败后的状态", + "evidence": "请填写缺陷工作流证据" + }, + "rule_instances": [ + {"id": "R01", "mode": "manual", "actual_rule_name": "人工门禁:需求确认", "trigger": "负责人确认", "source_status": "待处理", "target_status": "已确认", "conditions": ["范围、负责人和验收口径明确"], "action": "人工变更状态为已确认", "order": 1, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R02", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "已确认", "target_status": "分析中", "conditions": ["当前状态=已确认", "需求自身标签包含分析"], "action": "变更状态为分析中", "order": 2, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R03", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联任务工作项状态变化", "source_status": "分析中", "target_status": "分析完成", "conditions": ["当前状态=分析中", "本阶段分析任务全部完成;若平台不能过滤阶段则记录替代策略"], "action": "变更状态为分析完成", "order": 3, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R04", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "分析完成", "target_status": "设计中", "conditions": ["当前状态=分析完成", "需求自身标签包含设计"], "action": "变更状态为设计中", "order": 4, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R05", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联任务工作项状态变化", "source_status": "设计中", "target_status": "设计完成", "conditions": ["当前状态=设计中", "本阶段设计任务全部完成;若平台不能过滤阶段则记录替代策略"], "action": "变更状态为设计完成", "order": 5, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R06", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "设计完成", "target_status": "待开发", "conditions": ["当前状态=设计完成", "需求自身标签包含开发"], "action": "变更状态为待开发", "order": 6, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R07", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联分支或正式关联代码资产", "source_status": "待开发", "target_status": "开发中", "conditions": ["当前状态=待开发", "仓库已集成", "代码资产正式关联需求"], "action": "变更状态为开发中", "order": 7, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R08", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联合并请求状态变化", "source_status": "开发中", "target_status": "开发完成", "conditions": ["当前状态=开发中", "全部相关前后端合并请求已合并"], "action": "变更状态为开发完成", "order": 8, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R09", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加关联任务工作项", "source_status": "开发完成", "target_status": "测试中", "conditions": ["当前状态=开发完成", "需求自身标签包含测试"], "action": "变更状态为测试中", "order": 9, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R10", "mode": "manual", "actual_rule_name": "人工门禁:测试验收", "trigger": "测试验收任务完成或获批回调", "source_status": "测试中", "target_status": "测试完成", "conditions": ["当前状态=测试中", "范围内用例全部通过", "失败用例已复测通过", "范围内缺陷已由测试复测并关闭"], "action": "变更状态为测试完成", "order": 10, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R11", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联发布变更完成或获批回调", "source_status": "测试完成", "target_status": "完成发布", "conditions": ["当前状态=测试完成", "目标环境发布证据满足组织口径"], "action": "变更状态为完成发布", "order": 11, "enabled": false, "has_current_state_condition": true, "evidence": "请填写观察证据"}, + {"id": "R12", "mode": "manual", "actual_rule_name": "人工门禁:业务验收", "trigger": "业务验收", "source_status": "完成发布", "target_status": "已完成", "conditions": ["当前状态=完成发布", "正式验收证据有效"], "action": "人工变更状态为已完成", "order": 12, "enabled": true, "has_current_state_condition": true, "evidence": "请填写观察证据"} + ], + "task_rule_instances": [ + {"id": "TK01", "mode": "manual", "actual_rule_name": "人工门禁:设计任务真实开工", "trigger": "负责人开始处理设计任务", "scope": "设计任务", "source_state": "待处理", "conditions": ["任务标签=设计", "任务正式关联需求"], "actions": ["设计任务变为处理中"], "order": 101, "enabled": true, "evidence": "请填写观察证据"}, + {"id": "TK02", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "设计任务状态变为已完成", "scope": "产品需求", "source_state": "设计中", "conditions": ["本阶段设计任务非零且全部完成"], "actions": ["需求变为设计完成"], "order": 102, "enabled": true, "evidence": "请填写观察证据"}, + {"id": "TK03", "mode": "integration_bridge", "actual_rule_name": "桥接:设计完成后创建开发任务", "trigger": "需求状态变为设计完成", "scope": "产品需求", "source_state": "设计完成", "conditions": ["不存在同需求同阶段未取消开发任务", "幂等键=项目ID+需求ID+开发"], "actions": ["创建并关联开发任务", "开发任务标签=开发", "开发任务状态=待处理"], "order": 103, "enabled": false, "evidence": "请填写观察证据"}, + {"id": "TK04", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "开发任务创建并正式关联需求", "scope": "产品需求", "source_state": "设计完成", "conditions": ["需求标签包含开发", "开发任务关系可见"], "actions": ["需求变为待开发"], "order": 104, "enabled": true, "evidence": "请填写观察证据"}, + {"id": "TK05", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "添加正式关联分支或代码资产", "scope": "产品需求和开发任务", "source_state": "需求=待开发;开发任务=待处理", "conditions": ["代码资产同时关联需求和开发任务", "仓库已接入项目"], "actions": ["需求变为开发中", "开发任务变为处理中"], "order": 105, "enabled": true, "evidence": "请填写观察证据"}, + {"id": "TK06", "mode": "native_rule", "actual_rule_name": "请填写云效规则显示名", "trigger": "关联合并请求状态变化", "scope": "产品需求和开发任务", "source_state": "需求=开发中;开发任务=处理中", "conditions": ["全部相关前后端MR已合并到真实dev/develop", "MR同时关联需求和开发任务"], "actions": ["需求变为开发完成", "开发任务变为已完成"], "order": 106, "enabled": true, "evidence": "请填写观察证据"}, + {"id": "TK07", "mode": "integration_bridge", "actual_rule_name": "桥接:test部署成功后创建测试任务", "trigger": "test流水线部署成功", "scope": "产品需求", "source_state": "开发完成", "conditions": ["流水线执行ID未处理", "不存在同需求同阶段未取消测试任务", "回调签名和防重放验证通过"], "actions": ["创建并关联测试任务", "测试任务变为处理中", "关联测试计划和用例", "需求变为测试中"], "order": 107, "enabled": false, "evidence": "请填写观察证据"}, + {"id": "TK08", "mode": "manual", "actual_rule_name": "人工门禁:测试任务验收闭环", "trigger": "测试验收完成", "scope": "产品需求和测试任务", "source_state": "需求=测试中;测试任务=处理中", "conditions": ["用例全部通过", "失败用例已复测", "范围内缺陷已由测试关闭"], "actions": ["测试任务变为已完成", "需求变为测试完成"], "order": 108, "enabled": true, "evidence": "请填写观察证据"} + ], + "control_coverage": [ + {"id": "R01", "mode": "manual", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工确认需求范围与验收口径"}, + {"id": "R02", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入分析中"}, + {"id": "R03", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工核对本阶段分析任务"}, + {"id": "R04", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入设计中"}, + {"id": "R05", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工核对本阶段设计任务"}, + {"id": "R06", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入待开发"}, + {"id": "R07", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "开发人员真实开工时人工开始开发"}, + {"id": "R08", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工核对全部相关MR"}, + {"id": "R09", "mode": "native_rule", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写负责人", "fallback": "人工进入测试中"}, + {"id": "R10", "mode": "manual", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写测试负责人", "fallback": "测试负责人验收任务"}, + {"id": "R11", "mode": "native_rule", "enabled": false, "evidence": "请填写观察证据", "owner": "请填写发布负责人", "fallback": "人工核对发布变更"}, + {"id": "R12", "mode": "manual", "enabled": true, "evidence": "请填写观察证据", "owner": "请填写产品负责人", "fallback": "产品或业务负责人验收"}, + {"id": "TK01", "mode": "manual", "enabled": true, "evidence": "请填写设计任务开工证据", "owner": "请填写设计负责人", "fallback": "负责人真实开工时人工改为处理中"}, + {"id": "TK02", "mode": "native_rule", "enabled": true, "evidence": "请填写设计任务完成汇总证据", "owner": "请填写产品负责人", "fallback": "人工核对非零设计任务全部完成"}, + {"id": "TK03", "mode": "integration_bridge", "enabled": false, "evidence": "请填写自动创建开发任务能力证据", "owner": "请填写项目管理员", "fallback": "从需求中人工新建并关联唯一开发任务"}, + {"id": "TK04", "mode": "native_rule", "enabled": true, "evidence": "请填写开发任务关联证据", "owner": "请填写项目管理员", "fallback": "人工核对关系后将需求改为待开发"}, + {"id": "TK05", "mode": "native_rule", "enabled": true, "evidence": "请填写需求和开发任务双对象开工证据", "owner": "请填写研发负责人", "fallback": "开发人员真实开工时人工更新任务和需求"}, + {"id": "TK06", "mode": "native_rule", "enabled": true, "evidence": "请填写需求和开发任务双对象完成证据", "owner": "请填写研发负责人", "fallback": "人工核对全部相关MR后完成任务和需求"}, + {"id": "TK07", "mode": "integration_bridge", "enabled": false, "evidence": "请填写test部署回调和自动创建测试任务证据", "owner": "请填写流水线负责人", "fallback": "test部署成功后人工创建并关联测试任务"}, + {"id": "TK08", "mode": "manual", "enabled": true, "evidence": "请填写测试任务与需求双闭环证据", "owner": "请填写测试负责人", "fallback": "测试负责人验收后同步完成测试任务和需求"}, + {"id": "TC01", "mode": "manual", "enabled": true, "evidence": "请填写用例执行状态证据", "owner": "请填写测试负责人", "fallback": "逐条执行并记录用例结果"}, + {"id": "TC02", "mode": "manual", "enabled": true, "evidence": "请填写失败用例关联缺陷证据", "owner": "请填写测试负责人", "fallback": "人工核对失败用例与缺陷正式关系"}, + {"id": "TC03", "mode": "manual", "enabled": true, "evidence": "请填写用例复测更新证据", "owner": "请填写测试负责人", "fallback": "复测后更新原用例状态"}, + {"id": "DF01", "mode": "manual", "enabled": true, "evidence": "请填写已修复交接证据", "owner": "请填写测试负责人", "fallback": "已修复继续等待测试复测"}, + {"id": "DF02", "mode": "manual", "enabled": true, "evidence": "请填写复测通过关闭证据", "owner": "请填写测试负责人", "fallback": "测试复测通过后关闭缺陷"}, + {"id": "DF03", "mode": "manual", "enabled": true, "evidence": "请填写复测失败重开证据", "owner": "请填写测试负责人", "fallback": "测试复测失败后重开缺陷"}, + {"id": "CI01", "mode": "native_rule", "enabled": true, "evidence": "请填写多仓库关联证据", "owner": "请填写研发负责人", "fallback": "人工核对全部前后端MR"}, + {"id": "CI02", "mode": "not_supported", "enabled": false, "evidence": "请填写流水线能力证据", "owner": "请填写流水线负责人", "fallback": "复制test流水线后评审回调POC"}, + {"id": "CI03", "mode": "manual", "enabled": true, "evidence": "请填写环境与发布证据", "owner": "请填写发布负责人", "fallback": "人工区分test部署、生产发布与业务验收"}, + {"id": "L01", "mode": "disabled_legacy", "enabled": false, "evidence": "请填写旧规则盘点证据", "owner": "请填写项目管理员", "fallback": "使用独立业务验收任务"}, + {"id": "L02", "mode": "disabled_legacy", "enabled": false, "evidence": "请填写旧规则盘点证据", "owner": "请填写项目管理员", "fallback": "保持发布完成状态等待业务验收"} + ], + "adjunct_automations": [] + } + ] +} diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/auto-stage-task.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/auto-stage-task.md new file mode 100644 index 0000000..82abe72 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/auto-stage-task.md @@ -0,0 +1,62 @@ +# 需求进阶段自动建同名任务(强制 · AI 桥接) + +当**产品类需求**推进到下列状态时,AI(`apply`)必须查重后创建/复用**与需求同名**的任务,并**正式关联**该需求。不得只改需求状态却不建任务。 + +## 触发状态与字段规则 + +| 需求进入状态 | 任务标题 | 任务标签(右侧基础字段) | 负责人 | 时间字段 | +|---|---|---|---|---| +| **分析中** | = 需求标题(同名) | `分析` | **需求创建人**姓名 | 沿用需求(见下) | +| **设计中** | = 需求标题(同名) | `设计` | **需求创建人**姓名 | 沿用需求(见下) | +| **待开发** | = 需求标题(同名) | `交付`(OneOS 主任务);若项目仍用阶段开发标签则用 `开发` | 固定 **何斐** | 沿用需求(见下) | + +### 时间沿用(「标签创建时间沿用需求的」) + +按平台能力尽量对齐,优先级: + +1. 任务 **计划开始时间** = 需求的计划开始时间;若需求无计划开始,则用需求 **创建时间** 的日期。 +2. 若平台允许写入任务创建时间:任务创建时间 = 需求创建时间。 +3. 若都不能改创建时间:仍写计划开始;并在任务描述首行注明:`创建时间沿用需求:YYYY-MM-DD HH:mm`。 + +### 正式关联(强制) + +- 使用云效**父子项或关联项**把任务挂到**该产品需求**。 +- 仅标题写需求编号、或只关联其他任务而不关联需求 → **不合格**,必须补关联后再回报成功。 +- 标题前缀(如 `【分析】`)**不是**关联;本规则默认标题与需求**完全同名**;若项目强制标题前缀,允许 `【分析】{需求标题}`,但仍须正式关联 + 正确标签。 + +## 幂等与查重 + +幂等键: + +```text +项目ID + 需求ID + 阶段标签(分析|设计|交付/开发) +``` + +执行顺序: + +1. 查询该需求下是否已有**未取消**、同阶段标签、同名(或同名前缀)任务。 +2. **已存在一条** → 复用:核对正式关联、标签、负责人(待开发须为何斐)、时间字段;缺则补齐;**不新建第二条**。 +3. **存在多条冲突** → 停止创建,列出任务编号,请人合并后再继续。 +4. **不存在** → 创建并正式关联,再改需求状态(若口令是「推进到某状态」且任务尚未建好,先建任务再改状态,避免有状态无任务)。 + +快轨直接到 **待开发**(跳过分析/设计):只建/复用 **待开发** 那一条同名任务(负责人何斐),不要为跳过的阶段补建分析/设计任务,除非用户明确要求。 + +## 与 OneOS 终态模型对齐 + +- `oneos_delivery`:待开发阶段的同名任务即唯一 **【交付】主任务**(标题优先与需求同名;标签=`交付`)。分析中/设计中另建同名阶段任务(标签分析/设计),**不**再额外建第二条交付主任务。 +- `stage_tasks`:分析/设计/开发阶段任务按上表;待开发对应标签=`开发` 的同名任务,负责人何斐(若项目配置了其他默认开发负责人,以用户当次口令为准,口令未写则何斐)。 + +## AI 回报模板(短) + +```text +需求:ONEOSP-xxx「标题」→ 状态:分析中/设计中/待开发 +任务:TASK-xxx「同名」;标签:…;负责人:…;计划开始:…;已正式关联需求 +查重:新建 / 复用 +``` + +## 负向(不得误做) + +- 需求仅到 `已确认` / `待处理`:不自动建分析/设计/待开发任务。 +- 不得把样式类口头变更当成建任务触发。 +- 不得在未查重时重复创建同阶段同名任务。 +- 不得把负责人设错:分析中/设计中≠何斐(除非创建人就是何斐);待开发必须何斐(除非用户当次明确改派)。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/codeup-integration.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/codeup-integration.md new file mode 100644 index 0000000..ac12a75 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/codeup-integration.md @@ -0,0 +1,60 @@ +# Codeup研发资产集成 + +## 目录 + +1. 仓库覆盖 +2. 分支策略 +3. 正式关联 +4. 提交与合并请求 +5. CI01多仓库门禁 +6. 验证场景 + +## 1. 仓库覆盖 + +前端、后端均可触发研发自动化,前提是仓库接入正确项目,分支/提交/MR正式关联需求,规则没有错误限定到单一仓库。 + +逐仓库记录:仓库名、角色、项目集成状态、Webhook、实际集成分支、是否纳入全部MR门禁和观察证据。 + +## 2. 分支策略 + +- 检查真实集成分支:存在`dev`则优先`dev`,否则使用存在的`develop`。 +- 从集成分支创建`feature/`或`fix/`。 +- 一个需求涉及多个仓库时,每个仓库可有一个同名活动分支。 +- 快速迭代仍使用短生命周期分支;合并后是否删除按仓库策略和用户授权处理。 +- 不直接推送保护分支,不为自动化方便创建不存在的`dev`/`develop`。 + +## 3. 正式关联 + +关联优先级: + +1. 从需求“代码”区域创建分支并回到需求确认资产数量。 +2. 在Codeup创建分支/MR时显式选择工作项;存在开发任务时同时关联需求和开发任务。 +3. 使用以工作项ID结尾的分支名,并在需求页确认自动关联。 +4. 提交说明关键字仅作兜底,必须回需求页验证。 + +需求编号、分支名或MR标题有关键字,不等同已正式关联。 + +## 4. 提交与合并请求 + +- 提交可带工作项ID增强追溯,但不判定开发完成。 +- MR目标必须是仓库真实集成分支。 +- MR显式关联需求和对应开发任务并完成评审。 +- 只有全部相关MR合并后才观察R08。 +- 首次提交时间不作为真实开发开始时间;人工“开始开发”为效能主口径。 + +## 5. CI01多仓库门禁 + +CI01要求所有相关前后端仓库的分支、提交和MR都正式关联需求与对应开发任务。只遗漏一个仓库或只关联需求,平台的“全部MR已合并”就可能只看到部分资产、提前完成需求,或完全无法完成开发任务。 + +如果无法可靠聚合所有仓库,保留人工核对门禁,不能声称R08已完全自动化。 + +## 6. 验证场景 + +| 场景 | 操作 | 期望 | +|---|---|---| +| 正式关联分支 | 从需求代码区创建真实仓库分支 | 异步进入开发中 | +| 非关联分支 | 仅本地创建/推送无关系分支 | 不触发R07 | +| 仅有提交 | 提交代码但不合并MR | 不进入开发完成 | +| 单仓库MR | 所有相关仓库只有一个MR合并 | 仍保持开发中 | +| 多仓库全部合并 | 所有相关MR正式关联并合并 | 进入开发完成 | +| 错误集成分支 | MR指向非真实集成分支 | 阻塞并修正,不伪造通过 | diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/lifecycle-rules.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/lifecycle-rules.md new file mode 100644 index 0000000..71904a5 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/lifecycle-rules.md @@ -0,0 +1,103 @@ +# 需求生命周期与项目规则 + +## 目录 + +1. 状态参数化 +2. R01–R12规则矩阵 +3. 阶段入口和阶段完成 +4. 开发开始与开发完成 +5. 标签、任务和关键字 +6. 规则实例与限制 + +## 1. 状态参数化 + +典型流程为: + +```text +待处理 → 已确认 → 分析中 → 分析完成 → 设计中 → 设计完成 → 待开发 +→ 开发中 → 开发完成 → 测试中 → 测试完成 → 完成发布/发布完成 → 已完成/已关闭 +``` + +逐项目读取真实状态。`完成发布`与`发布完成`、`已完成`与`已关闭`是候选映射,不能静默互换或创建近义重复状态。 + +## 2. R01–R12规则矩阵 + +| ID | 流转 | 触发事件 | 必要条件 | 动作 | 方式 | +|---|---|---|---|---|---| +| R01 | 待处理 → 已确认 | 负责人确认 | 范围、负责人、优先级、验收口径明确 | 改为已确认 | 人工 | +| R02 | 已确认 → 分析中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`分析` | 改为分析中 | 原生规则 | +| R03 | 分析中 → 分析完成 | 关联任务状态变化 | 当前状态;本阶段分析任务全部完成 | 改为分析完成 | 原生/条件化 | +| R04 | 分析完成 → 设计中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`设计` | 改为设计中 | 原生规则 | +| R05 | 设计中 → 设计完成 | 关联任务状态变化 | 当前状态;本阶段设计任务全部完成 | 改为设计完成 | 原生/条件化 | +| R06 | 设计完成 → 待开发 | 添加关联任务工作项 | 当前状态;需求自身标签包含`开发` | 改为待开发 | 原生规则 | +| R07 | 待开发 → 开发中 | 添加关联分支或正式关联代码资产 | 当前状态;仓库已集成;资产正式关联 | 改为开发中 | 自动兜底 | +| R08 | 开发中 → 开发完成 | 关联合并请求状态变化 | 当前状态;全部相关MR已合并 | 改为开发完成 | 原生规则 | +| R09 | 开发完成 → 测试中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`测试` | 改为测试中 | 原生规则 | +| R10 | 测试中 → 测试完成 | 测试验收任务或获批回调 | 当前状态;用例、失败复测、缺陷均闭环 | 改为测试完成 | 人工/桥接 | +| R11 | 测试完成 → 发布完成状态 | 发布变更完成或获批回调 | 当前状态;目标环境发布证据有效 | 改为真实发布状态 | 原生/桥接 | +| R12 | 发布完成状态 → 项目终态 | 业务验收 | 当前状态;验收证据有效 | 改为真实终态 | 默认人工 | + +R05–R10与阶段任务自身状态的双向联动见`task-lifecycle.md`;R10的用例和缺陷口径见`test-defect-closure.md`;R11、R12见`pipeline-release.md`;R07、R08的代码资产口径见`codeup-integration.md`。 + +## 3. 阶段入口和阶段完成 + +R02、R04、R06、R09同时要求: + +1. 从需求中“新建并关联”或“关联已有”任务工作项。 +2. 当前状态等于前一阶段完成状态。 +3. 需求自身包含目标阶段标签。 + +如果规则编辑器不能读取需求自身标签,标签只能用于筛选和报表;保留人工入口或使用当前状态+显式关联事件,不能声称标签已成为技术门槛。 + +R03、R05首选“本阶段任务全部完成”。若平台只能判断“全部关联任务工作项”,选择一种明确策略: + +- 阶段开始时才创建/关联本阶段任务。 +- 使用不同工作项类型,并确认规则能按类型过滤。 +- 使用父子需求拆分阶段。 +- 保留人工完成动作。 + +不得把零个关联任务当作全部完成。使用关联项状态变化触发,并执行零关联项负向测试。 + +## 4. 开发开始与开发完成 + +R07采用双轨口径: + +- 效能主口径:开发人员真实开工时执行“开始开发”。 +- 自动兜底:创建或正式关联需求分支/代码资产后进入`开发中`。 + +首次提交可能晚于真实开工,不能作为权威开始时间。自动规则必须要求当前状态=`待开发`。 + +R08推荐定义: + +```text +事件 = 关联合并请求状态变化 +且当前状态 = 开发中 +且全部相关合并请求 = 已合并 +则状态 = 开发完成 +``` + +提交活动不能证明开发完成。全部相关前端、后端MR都必须正式关联并合并到实际集成分支。 + +## 5. 标签、任务和关键字 + +`分析`、`设计`、`开发`、`测试`标签加在产品类需求自身。 + +“添加关联任务工作项”依赖云效正式关系,不依赖标题关键字。有效方式: + +- 在需求详情新建并关联任务。 +- 在需求详情关联已有任务。 +- 在任务详情选择该需求为父项/关联项,并在页面确认关系。 + +仅写需求编号、粘贴链接或添加同名标签都不能替代正式关联。编号仍建议出现在标题中,方便检索和审计。 + +## 6. 规则实例与限制 + +每个项目分别记录R01–R12的实际规则显示名/人工门禁、精确触发、源状态、目标状态、全部条件、动作、顺序、启用状态、执行账号和证据。 + +已知限制: + +- 规则通常异步执行,5–30秒内不要过早判失败。 +- “全部关联项”只能看到已集成并正式关联的资产。 +- 提前创建未来阶段任务可能阻塞“全部任务完成”。 +- 需求状态规则不会自动更新阶段任务状态;必须同时配置TK01–TK08。 +- 平台能力因模板、版本和权限不同,以当前UI和执行日志为准。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/live-operations.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/live-operations.md new file mode 100644 index 0000000..0c8271d --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/live-operations.md @@ -0,0 +1,133 @@ +# 线上实施、验证与治理 + +## 目录 + +1. 安全边界 +2. 项目盘点 +3. 规则应用 +4. 隔离验证 +5. 故障诊断 +6. 回滚 +7. 运行治理 + +## 1. 安全边界 + +- 只操作明确授权的组织、项目、工作项类型和测试资产。 +- 复用已登录会话,不记录账号、密码、Cookie、Token、OTP、私钥或签名材料。 +- 不修改现有prod流水线;回调实验只改test副本。 +- 不删除测试需求、分支、MR、流水线或规则,除非另有授权。 +- 保留无关规则和用户修改。 + +## 2. 项目盘点 + +先记录`flow_model=stage_tasks`或`flow_model=oneos_delivery`。发现两套规则同时启用时判定为迁移中,不按规则名称猜测主模型。 + +对每个项目独立记录: + +| 类别 | 必查内容 | +|---|---| +| 项目 | 精确项目名、项目ID、工作项类型 | +| 工作流 | 全部状态、顺序、发布状态、终态 | +| 阶段任务 | 设计/开发/测试任务类型、状态、标签、父子/关联关系、自动创建能力 | +| 标签 | `分析`、`设计`、`开发`、`测试` | +| 规则 | 名称、事件、条件、动作、顺序、启用、执行账号、后继规则 | +| Codeup | 前后端仓库、集成、Webhook、真实`dev/develop` | +| 流水线 | test/prod名称、环境、部署阶段、副本能力 | +| 发布 | 发布变更关系、目标环境、完成状态 | +| 测试 | 计划范围、用例状态枚举、需求关系 | +| 缺陷 | 已修复、复测、关闭态、重开态、权限 | + +两个项目只复制逻辑,不复制内部ID、仓库、分支、流水线、规则ID或观察证据。 + +OneOS终态迁移还要盘点:`待测试/发布中/发布失败`状态缺口、主任务身份方式、任务工作流方案A/B、原生规则能否过滤关联任务身份、Y/A控制负责人和幂等键。门禁未齐时保留可用旧规则,只停用会绕过验收或造成已确认误触发的规则。 + +## 3. 规则应用 + +1. 导出或截图现有规则和启用状态。 +2. 识别后继规则和可能的连锁路径。 +3. 创建缺失阶段标签。 +4. 一次只创建或修改一条规则。 +5. 设置精确触发、当前状态、需求标签、其他条件和目标动作。 +6. 保存后重新打开,核对全部字段、顺序、启用状态和执行账号。 +7. 把真实字段写回该项目`rule_instances`。 +8. 触发最小事件,等待5–30秒。 +9. 查看状态、研发资产、执行日志和二次流转。 +10. 记录证据后再处理下一条。 + +手工推进只能标记为“测试准备动作”,不能伪造规则通过。 + +## 4. 隔离验证 + +测试需求命名示例: + +```text +[自动化测试] 产品类需求生命周期验证-YYYYMMDD +``` + +| 用例 | 操作 | 期望 | 负向检查 | +|---|---|---|---| +| T01 | 已确认+分析标签+关联任务 | 分析中 | 无标签不流转 | +| T02 | 完成分析任务 | 分析完成 | 零任务不误触发 | +| T03 | 设计标签+关联任务 | 设计中 | 仅标题关键字不触发 | +| T04 | 完成设计任务 | 设计完成 | 未完成任务阻塞 | +| T04a | 设计完成后触发TK03 | 创建且只创建一个开发任务 | 重复事件不得重复创建 | +| T04b | 开发任务正式关联需求 | 需求待开发、任务待处理 | 仅标题编号不触发 | +| T05 | 开发标签+关联任务 | 待开发 | 错误当前状态不触发 | +| T06 | 正式关联真实仓库分支 | 开发中 | 非关联分支不触发 | +| T06a | 同一代码资产同时关联需求和开发任务 | 需求开发中、开发任务处理中 | 只关联需求不得声称任务已联动 | +| T07 | 前后端各关联MR,只合并一个 | 保持开发中 | 不得提前完成 | +| T08 | 全部相关MR合并 | 开发完成 | 仅提交不得完成 | +| T08a | 全部相关MR同时关联开发任务并合并 | 开发任务已完成 | 部分仓库MR未合并时阻塞 | +| T09 | 测试标签+关联测试任务 | 测试中 | 未来任务不应误阻塞 | +| T09a | test流水线部署成功触发TK07 | 创建且只创建一个测试任务并进入处理中 | 流水线失败或重复回调不得创建 | +| T10a | 执行全部用例并记录结果 | 每条有明确状态 | 未执行/阻塞/失败阻塞 | +| T10b | 失败用例关联缺陷,改为已修复 | 保持测试中 | 未复测不得完成 | +| T10c | 复测通过/失败 | 关闭并通过/重开并失败 | 用例缺陷状态一致 | +| T11 | 发布变更完成/获批回调 | 发布完成状态 | 重复回调不重复推进 | +| T12a | 进入发布完成后等待异步窗口 | 停留发布完成 | L01/L02不得误关闭 | +| T12b | 完成正式业务验收 | 项目终态 | test或发布不能替代验收 | + +结果只使用:`通过`、`异步通过`、`未触发`、`误触发`、`阻塞`。 + +## 5. 故障诊断 + +按顺序检查: + +1. 事件是否真实发生,而不是只有相似标题/标签。 +2. 当前状态是否精确匹配。 +3. 标签是否加在需求自身。 +4. 任务、分支、MR、发布变更是否正式关联。 +5. 仓库是否接入正确项目,Webhook是否有效。 +6. 全部相关MR是否合并到真实集成分支。 +7. 规则是否启用,执行账号是否有权限。 +8. 执行日志是未匹配、失败还是排队。 +9. 用例是否仍有未执行、阻塞或失败。 +10. 缺陷是否仅到已修复,尚未复测关闭。 +11. 目标状态是否触发后继/重复规则。 +12. 是否发生`测试完成 → 发布完成 → 终态`瞬时连锁。 +13. 是否等待合理异步窗口。 +14. 是否误用另一个项目的内部ID或证据。 +15. 缺陷与原用例在复测后的状态是否一致。 +16. 需求状态变化是否遗漏对应阶段任务状态,或任务变化是否遗漏需求汇总。 +17. 自动创建下一阶段任务是否具有需求ID+阶段类型幂等门禁。 +18. 项目是否混用R/TK阶段模型与Y/A终态模型。 +19. OneOS主任务是否唯一且可被规则可靠识别。 +20. 是否缺少`待测试`、`发布中`、`发布失败`或重试流转边。 + +## 6. 回滚 + +- 先禁用本次新增规则,再恢复修改前条件和动作。 +- 保留变更前截图/导出和规则ID。 +- 回调POC通过禁用副本回滚;删除需单独授权。 +- 分支/MR等测试资产按仓库策略清理;删除需单独授权。 +- 不覆盖与本次变更无关的规则。 + +## 7. 运行治理 + +- 每季度复核规则账号、权限、启用状态和失败日志。 +- 新增仓库时同步验证项目集成、Webhook和多仓库门禁。 +- 状态、标签或工作项类型重命名后逐条复核依赖规则。 +- 保护`master/main`和真实`dev/develop`。 +- 监控超过7天无活动的需求分支,未经授权不删除。 +- 分开统计自动流转时间与真实业务开始时间。 +- 持续检查回调签名、时间戳、防重放、幂等和失败重试。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-fast-path.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-fast-path.md new file mode 100644 index 0000000..be9a309 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-fast-path.md @@ -0,0 +1,117 @@ +# 统一运营管理平台 · 记录需求快路径 + +真相源运行时 ID:[assets/oneos-pc-runtime-ids.json](../assets/oneos-pc-runtime-ids.json) +验证样例:ONEOS-84「测试需求」(2026-07-21)。 + +**用户可复制完整口令(A/B/C 门禁 + 30 标签):** [oneos-pc-record-requirement-prompt.md](oneos-pc-record-requirement-prompt.md) + +本文件只服务**日常 `记录需求` apply**。全量审计/规则配置仍读 `live-operations.md` 等模块。 + +## 0. 执行门禁(强制) + +用户口令要求 Plan / 单选「优先级」「推进至」「标签」时: + +1. **必须**先拿到明确的 A(紧急/高/中/低)、B(分析中/设计中/设计完成/开发中)、**C(标签,可多选)**。 +2. 标签 **C** 只能从 [assets/oneos-pc-tag-catalog.md](../assets/oneos-pc-tag-catalog.md) / `tags.catalog` 点选;**禁止**按 `lines.ts` 或业务条线说明自动推断。 +3. 来源优先:`AskQuestion` 点选 → Plan 批准时写明的 A+B+C → 用户一句话点明。 +4. **禁止**:未选时默认「中 + 分析中」并继续建单;**禁止**未选标签时自作主张打标。 +5. 「批准/Implement 计划」≠ 已选 A+B+C;计划正文若仍是空 ○,必须先问清再执行。 + +多条需求名称时:每条或整批共用的优先级/推进至/标签仍须用户点选后再写云效。 + +## 1. 加载配方(禁止重探) + +对项目别名含「统一运营管理平台」时: + +1. 读取 `assets/oneos-pc-runtime-ids.json`。 +2. **禁止**再探测 create URL(不要用 `/workitem/create`)。 +3. **禁止**对优先级 ID 做猜测重试超过配方表。 + +## 2. 标准执行顺序(目标 1~3 分钟) + +```text +AutoPRD → API 建需求(含描述) → API 建阶段任务 → API 打标签(COVER) → 状态连跳 → 一次校验回报 +``` + +### 2.1 AutoPRD + +- 先跑 `$oneos-autoprd`(模块/原型来自口令或对话页)。 +- 描述 HTML 最小块:`【原型】` + `【目标】` + `【非目标】` + `【更新内容】`(无变更记录则「首版定稿」)。 + +### 2.2 API 建产品类需求 + +`POST`(失败再试一次 `PUT`) +`https://devops.aliyun.com/projex/api/workitem/workitem?_input_charset=utf-8` + +要点: + +- `workitemTypeIdentifier` **与** `workitemType` 都设为产品类需求 ID。 +- `document: { content: html, formatType: 'RICHTEXT' }` 创建时写入。 +- `fieldValueList` 带 `priority`、`assignedTo`、可选 `79`(计划开始,中国时区中午 epoch 字符串)。 +- 开发中:需求负责人用何斐 ID;分析中/设计中/设计完成:创建人 ID。 + +### 2.3 API 建同名阶段任务 + +同一 create 接口,`category=Task`,`parentIdentifier=需求ID`,标题与需求**完全同名**。 + +| 推进至 | 任务标签目标 | 负责人 | +|---|---|---| +| 分析中 | 分析 | 创建人 | +| 设计中 | 设计 | 创建人 | +| 开发中 / 待开发 | 开发或交付 | 何斐 | + +查重:同需求 + 同阶段标签 + 未取消 → 复用,不建第二条。详见 [auto-stage-task.md](auto-stage-task.md)。 + +### 2.4 状态连跳(开发中) + +从「待处理」到「开发中」UI 固定路径: + +```text +待处理 → 设计完成 → 待开发 → 开发中 +``` + +每跳点开状态按钮选目标;不要在「待处理」下拉里找「开发中」(没有)。 + +其它推进至: + +- 分析中 / 设计中 / 设计完成:待处理下拉通常可直达。 + +### 2.5 标签(选择器点选 + API) + +1. 用户已从 `tags.catalog` 点选标签名(见门禁 C);用 `tags.by_name` 得到 identifier。 +2. **优先 API**(已验证): + +```http +PATCH https://devops.aliyun.com/projex/api/workitem/workitem/{identifier}?_input_charset=utf-8 +Content-Type: application/json + +{ + "workitemIdentifier": "{identifier}", + "propertyKey": "tag", + "propertyValue": "{id1,id2}", + "operateType": "COVER" +} +``` + +3. API 失败 1 次 → UI 标签选择器勾选 → **确定**;仍失败 → **停止并请用户回复标签名**,禁止报「已打上」。 +4. **禁止**对照 `lines.ts` / 业务条线说明自动映射;刷新全量标签:`GET` `api.list_tags`(`spaceType=Space`)。 + +### 2.6 校验与回报(只做一次) + +核对:编号、链接、状态、优先级、负责人、计划开始、描述含原型、任务父项、标签。 +截图最多:建单后、终态后各一次。 + +## 3. 性能红线 + +| 允许 | 禁止 | +|---|---| +| 配方内 ID / URL | 每步 3+ 端点盲探 | +| create 带描述 | 默认先开富文本再粘贴 | +| 状态固定三跳 | React fiber 乱调 onChange(易白屏) | +| 标签失败问用户 | 标签失败仍声称成功 | +| API 失败 1 次改 UI | 同失败 payload 循环重试 | + +## 4. 浏览器会话 + +- 复用已登录 `devops.aliyun.com`;不记录 Cookie/Token。 +- 优先已打开的「统一运营管理平台」项目页;锁 tab 后执行,结束解锁。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-record-requirement-prompt.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-record-requirement-prompt.md new file mode 100644 index 0000000..0ee6812 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-pc-record-requirement-prompt.md @@ -0,0 +1,139 @@ +# 统一运营管理平台 · 记录需求到云效(完整提示词) + +复制下方「用户口令」整段发给 Agent;Agent 须先弹出 Plan / 单选卡让你点选 **A 优先级 + B 推进至 + C 标签**,**未点选前不得建单**。 + +--- + +## 用户口令(复制整段) + +```text +使用 $yunxiao-requirement-lifecycle + $oneos-autoprd +记录需求到云效 · 统一运营管理平台 + +【需求名】 +(填写 1 条或多条,例:车辆资产功能优化) + +【描述来源】 +$oneos-autoprd,原型/模块:(填写,例:oneos-h5-vehicle-assets) + +【已锁定】 +- 项目:统一运营管理平台 +- 计划开始:今日 +- 负责人:分析中 / 设计中 / 设计完成 → 创建人;开发中 → 何斐 + +【请你先 Plan 点选,未选完禁止写云效】 + +A. 优先级(单选) +○ 紧急 +○ 高 +○ 中 +○ 低 + +B. 推进至(单选) +○ 分析中(建同名「分析」任务,负责人=创建人) +○ 设计中(建同名「设计」任务,负责人=创建人) +○ 设计完成(只改状态,负责人=创建人;不强制建阶段任务) +○ 开发中(建同名开发/交付任务,负责人=何斐;状态连跳:待处理→设计完成→待开发→开发中) + +C. 标签(可多选;只能从下列 30 个云效标签中选,禁止自造、禁止按业务条线/lines.ts 自动推断) +○ 运维管理条线 +○ 报表中心 +○ 能源管理 +○ 维修站管理 +○ 充电站管理 +○ 还车应结款 +○ 交车应收款 +○ 加氢站管理 +○ 保险管理 +○ 合同管理 +○ 租赁账单 +○ 供应商管理 +○ 客户管理 +○ 还车任务 +○ 安全培训 +○ 备件管理 +○ 停车场管理 +○ 备车管理 +○ 异动管理 +○ 调拨管理 +○ 上牌管理 +○ 替换车管理 +○ 还车管理 +○ 交车管理 +○ 车辆管理 +○ 审批中心 +○ 故障管理 +○ 车辆年审 +○ 维修管理 +○ 工作台 + +【我确认 A+B+C 后你再执行】 +批准 / Implement 计划时,我会写明所选值,例如: +优先级:高;推进至:设计完成;标签:运维管理条线 + +【执行要求(Agent)】 +1. 读快路径:references/oneos-pc-fast-path.md + assets/oneos-pc-runtime-ids.json +2. 先 $oneos-autoprd 生成描述(【原型】【目标】【非目标】【更新内容】;无增量则「首版定稿」) +3. API 建产品类需求(POST /workitem/workitem,创建时写入 document) +4. 计划开始=今日;按 B 推进状态;按规则建/复用同名阶段任务 +5. 标签:用 tags.by_name 查 identifier,PATCH propertyKey=tag operateType=COVER;API 失败 1 次再 UI 兜底 +6. 一次校验回报:编号、链接、状态、优先级、负责人、计划开始、标签、描述、阶段任务 +7. 禁止默认「中+分析中」;禁止未选标签时自作主张打标;标签失败不得报「已成功」 +``` + +--- + +## 单条需求示例(填好即用) + +```text +使用 $yunxiao-requirement-lifecycle + $oneos-autoprd +记录需求到云效 · 统一运营管理平台 + +【需求名】 +测试需求3 + +【描述来源】 +$oneos-autoprd,原型/模块:oneos-h5-vehicle-assets + +【已锁定】 +- 项目:统一运营管理平台 +- 计划开始:今日 +- 负责人:分析中/设计中/设计完成=创建人;开发中=何斐 + +请先 Plan 让我点选 A 优先级、B 推进至、C 标签(30 项 catalog,禁止 lines.ts 推断)。 +我确认后再执行快路径建单。 +``` + +确认回复示例: + +```text +优先级:高;推进至:设计完成;标签:运维管理条线 +``` + +--- + +## Agent 执行清单(内部,勿省略) + +| 步 | 动作 | +|---|---| +| 0 | 门禁:A+B+C 已明确;Implement ≠ 已选 | +| 1 | `$oneos-autoprd` → HTML 描述 | +| 2 | `POST …/workitem/workitem` 建需求 + document | +| 3 | 字段:priority、assignedTo、79=今日 | +| 4 | 若 B∈{分析中,设计中,开发中} → 同名阶段任务(查重复用) | +| 5 | 状态推进至 B(开发中三跳) | +| 6 | `PATCH …/workitem/{id}` 打标签 COVER | +| 7 | 回报 ONEOS-xxx + 链接 + 核对表 | + +标签 API 形态: + +```json +{ + "workitemIdentifier": "{id}", + "propertyKey": "tag", + "propertyValue": "{identifier1},{identifier2}", + "operateType": "COVER" +} +``` + +参考:`assets/oneos-pc-tag-catalog.md`、`assets/oneos-pc-runtime-ids.json` → `tags.by_name` diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-terminal-flow.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-terminal-flow.md new file mode 100644 index 0000000..269f36a --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/oneos-terminal-flow.md @@ -0,0 +1,192 @@ +# OneOS终态交付模型与迁移规则 + +## 目录 + +1. 模型选择与边界 +2. 工作项和状态 +3. 原生Y规则 +4. AI与Webhook桥接 +5. 迁移就绪门禁 +6. 分阶段实施 +7. 验收与负向测试 +8. 已知风险和回滚 + +## 1. 模型选择与边界 + +OneOS终态模型以需求为阶段看板唯一真相: + +```text +迭代 +├─ 多个产品类需求 +├─ 按需求分包的测试计划 +└─ 一条【发版】任务 + +产品类需求 +└─ 一条【交付】主任务 + ├─ 多条【开发】任务 + └─ 一条【测试】任务 +``` + +每个项目只能选择一个主模型: + +- `stage_tasks`:分析、设计、开发、测试各建阶段任务,使用R/TK控制。 +- `oneos_delivery`:一条交付主任务镜像需求,多开发任务汇总,使用Y/A控制。 + +不得同时启用两套进场规则。若项目仍有分析/设计标签驱动的R规则,先记录为`legacy_active`,完成迁移就绪检查后再停用。 + +## 2. 工作项和状态 + +### 2.1 任务身份 + +| 任务 | 数量 | 必须关系 | 识别优先级 | +|---|---:|---|---| +| `【交付】`主任务 | 每需求1条 | 正式关联需求 | 独立工作项类型 > 任务标签`交付` > 标题前缀流程约束 | +| `【开发】`任务 | 每需求1..N条 | 正式关联需求和主任务 | 独立类型 > 标签`开发` > 标题前缀 | +| `【测试】`任务 | 每需求1条 | 正式关联需求和主任务 | 独立类型 > 标签`测试` > 标题前缀 | +| `【发版】`任务 | 每迭代1条 | 正式关联迭代和本批需求 | 独立类型 > 标签`发版` > 标题前缀 | + +标题关键字本身不构成正式关系。规则编辑器不能读取关联任务的类型、标签或标题时,不得声称原生规则能区分主任务和开发任务;使用桥接或人工门禁。 + +### 2.2 需求状态 + +目标状态顺序: + +```text +待处理 → 已确认 → 分析中 → 分析完成 → 设计中 → 设计完成 +→ 待开发 → 开发中 → 开发完成 → 待测试 → 测试中 → 测试完成 +→ 发布中 → 发布完成 / 发布失败 → 已关闭 +``` + +允许的快轨至少包括`已确认 → 待开发`和`设计完成 → 待开发`。`发布失败 → 发布中`用于重试。已有近义状态先映射,禁止静默创建`完成发布/发布完成`、`已完成/已关闭`等重复状态。 + +### 2.3 任务状态 + +- 主任务:首选与需求同名镜像状态。 +- 开发任务:`待处理 → 处理中 → 已完成`,另有取消态。 +- 测试任务:`待处理/待测试 → 测试中 → 已完成`。 +- 发版任务:`待处理 → 发布中 → 发布完成/发布失败`。 + +若任务工作流不能承载主任务全量状态,不配置Y02–Y16原生镜像;由桥接更新需求并记录主任务显示口径。 + +## 3. 原生Y规则 + +统一规则前缀:`[OneOS终态] Yxx ...`。保存后重新打开核对事件、条件、动作、顺序、启用状态和执行账号。 + +### 3.1 主任务镜像 + +| ID | 触发 | 条件 | 动作 | 默认方式 | +|---|---|---|---|---| +| Y01 | 新增正式关联主任务 | 主任务可可靠识别;需求为待处理/已确认 | 需求与主任务进入分析中或获批快轨 | 原生/桥接 | +| Y02 | 主任务到分析完成 | 正式关联唯一需求 | 需求到分析完成 | 原生 | +| Y03 | 主任务到设计中 | 正式关联唯一需求 | 需求到设计中 | 原生 | +| Y04 | 主任务到设计完成 | 正式关联唯一需求 | 需求到设计完成 | 原生 | +| Y05 | 主任务到待开发 | 正式关联唯一需求 | 需求到待开发;负责人按项目配置 | 原生/桥接 | +| Y06 | 主任务到开发中 | 正式关联唯一需求 | 需求到开发中 | 原生 | +| Y07 | 主任务到开发完成 | 正式关联唯一需求 | 需求到开发完成 | 原生 | +| Y08 | 主任务到待测试 | 正式关联唯一需求 | 需求到待测试 | 原生 | +| Y09 | 主任务到测试中 | 正式关联唯一需求 | 需求到测试中 | 原生 | +| Y10 | 主任务到测试完成 | Testhub门禁通过 | 需求到测试完成 | 桥接优先 | +| Y11 | 主任务到发布中 | 发版任务和迭代范围有效 | 需求到发布中 | 原生/桥接 | +| Y12 | 主任务到发布完成 | 生产发布证据有效 | 需求到发布完成 | 桥接 | +| Y13 | 主任务到发布失败 | 生产发布失败证据有效 | 需求到发布失败并记录原因 | 桥接 | +| Y14 | 主任务到已关闭 | 产品验收证据有效 | 需求到已关闭 | 默认人工 | +| Y15 | 需求状态变化 | 存在唯一主任务且无循环风险 | 主任务同名镜像 | 可选原生 | +| Y16 | 需求快轨到待开发 | 存在唯一主任务 | 主任务到待开发 | 原生/桥接 | + +每条双向镜像规则必须有防循环条件。平台无法区分“由本规则写入”的事件时,只保留一个权威方向。 + +### 3.2 开发任务汇总 + +| ID | 触发 | 条件 | 动作 | 默认方式 | +|---|---|---|---|---| +| Y20 | 新增第一条开发任务或正式关联开发分支 | 需求=待开发;开发任务数量≥1;任务已指派 | 需求和主任务到开发中;对应开发任务处理中 | 原生/桥接 | +| Y21 | 开发任务完成 | 非取消开发任务数量≥1且全部完成;全部相关MR已合并 | 需求和主任务到开发完成 | 桥接优先 | +| Y22 | 开发完成 | 同需求无未取消测试任务;幂等键未处理 | 创建并关联唯一测试任务,进入待测试 | 桥接 | + +现有的“任务添加分支且标签=开发,待处理→处理中”和“全部MR已合并且标签=开发,处理中→已完成”可作为Y20/Y21的任务侧子规则保留。需求侧“全部MR已合并”只能看到正式关联资产,必须执行多仓库负向测试。 + +### 3.3 测试、发布和迭代 + +| ID | 触发 | 条件 | 动作 | 默认方式 | +|---|---|---|---|---| +| Y30 | 创建测试任务 | 正式关联需求和主任务 | 需求/主任务到待测试 | 桥接 | +| Y31 | 首条用例执行或测试负责人开工 | 测试任务=待处理/待测试 | 测试任务、需求、主任务到测试中 | 原生/人工 | +| Y32 | 测试验收完成 | 本需求用例包全部通过且缺陷闭环 | 三个对象完成测试阶段 | 桥接/人工 | +| Y33 | 发版任务提交 | 发版任务正式关联迭代与本批需求 | 发版任务和本批需求到发布中 | 原生/桥接 | +| Y34 | 生产发布成功 | 流水线执行、环境、范围、签名和幂等均有效 | 到发布完成 | 桥接 | +| Y35 | 生产发布失败 | 同上;失败证据有效 | 到发布失败并记录原因 | 桥接 | +| Y40 | 需求到待开发 | 未关联迭代 | 通知产品负责人,不自动写入未知迭代 | 通知/人工 | + +test部署只能允许测试开始,不能触发Y34。发布完成后必须停留,等待产品/业务验收;禁止自动进入已关闭。 + +## 4. AI与Webhook桥接 + +| ID | 能力 | 最小门禁 | +|---|---|---| +| A01 | 发布原型/附件并回写需求描述 | 外部存储授权;链接可审计 | +| A02 | 生成并保留更新内容历史 | 不覆盖历史段;**OneOS 默认调用 `$oneos-autoprd` 第 10 章「功能变更记录」(自上次定稿以来);旧段挪到「更新内容·历史」** | +| A02B | 生成产品可读需求说明 | 不写实现机密;保留来源;**OneOS 默认调用 `$oneos-autoprd` 全文/交付口径写入「需求说明」** | +| A03 | 建需求和唯一主任务并快轨待开发 | 幂等查询;正式关系可见;**建单写描述前先完成 A02+A02B(AutoPRD)**;进入待开发时按 [auto-stage-task.md](auto-stage-task.md) 建/复用与需求同名任务,负责人何斐,时间沿用需求 | +| A03B | 需求进入分析中/设计中自动建同名阶段任务 | 标签分析/设计;负责人=需求创建人;正式关联;时间沿用需求;与 A03 同一套查重 | +| A04 | 拆分多条开发任务 | 负责人、范围和父子关系明确 | +| A05 | 按MR证据关闭开发任务 | 所有仓库、目标分支和关联关系完整 | +| A06 | 全部开发完成后建唯一测试任务 | 幂等键=`项目ID+需求ID+create_test_task` | +| A07 | 按Testhub结果完成测试 | 用例与缺陷口径满足TC/DF控制 | +| A08 | 按迭代创建发版任务和更新说明 | 正式关联迭代和本批需求 | +| A09 | 发布成败回写 | 验证环境、签名、时间戳、执行ID和范围 | +| A10 | 产品验收后关闭 | 明确验收人和证据;不得由发布成功替代 | + +所有桥接必须具备:最小权限服务账号、签名、时间戳、防重放、幂等、失败重试、审计日志和人工回退。不得把未部署的AI能力写成已自动化。 + +## 5. 迁移就绪门禁 + +只有全部满足时才停用旧R/TK进场规则: + +1. 需求状态已包含`待测试`、`发布中`、`发布失败`,并配置必要流转边。 +2. 主任务能被可靠识别;若只能靠标题前缀,已明确由桥接处理,不使用无过滤原生镜像。 +3. 主任务和开发/测试/发版任务的工作流已选定并验证。 +4. Y/A控制逐项指定`native_rule`、`integration_bridge`、`manual`或`not_supported`。 +5. A03、A06、A08、A09具备幂等键、负责人和回退方案。 +6. Codeup前后端仓库、真实`dev/develop`、正式关联和多仓库MR门禁已验证。 +7. Testhub按需求分包;未执行、阻塞、失败用例和仅“已修复”缺陷会阻塞测试完成。 +8. test、生产发布、发布变更和产品验收证据已分离。 +9. 已保存旧规则名称、条件、顺序、启用状态和执行账号证据。 +10. `L01`、`L02`已停用,发布完成不会瞬时关闭。 + +未通过门禁时:保留当前可用规则,只停用会绕过验收或造成误触发的规则,并输出迁移计划。 + +## 6. 分阶段实施 + +- P0:补齐工作流、唯一主任务、快轨待开发、负责人和旧规则备份。 +- P1:开发任务拆分、分支/MR双关联、多仓库完成门禁、唯一测试任务。 +- P2:迭代、Testhub按需求分包、用例和缺陷复测闭环。 +- P3:发版任务、生产流水线成败回写、发布失败重试、产品验收关闭。 + +每阶段先在隔离测试需求验证,再启用下一阶段。新规则不补触发历史事件;历史数据修正必须单独记录。 + +## 7. 验收与负向测试 + +| ID | 操作 | 期望 | 负向检查 | +|---|---|---|---| +| V01 | 创建需求和唯一主任务 | 正式关联且不重复 | 重放不得建第二条主任务 | +| V02 | 未建开发任务 | 保持待开发 | 主任务本身不得被当作开发任务 | +| V03 | 建两条开发任务 | 进入开发中 | 无正式关系或无负责人不触发 | +| V04 | 只完成一条开发任务/MR | 保持开发中 | 不得提前完成 | +| V05 | 所有开发任务和MR完成 | 开发完成并只建一条测试任务 | 重复回调不重复创建 | +| V06 | 同迭代其他需求未测完 | 本需求仍可独立完成测试 | 不按整个计划全绿判断 | +| V07 | 用例失败且缺陷仅已修复 | 保持测试中 | 未复测不得完成 | +| V08 | 复测通过/失败 | 用例通过+缺陷关闭 / 用例失败+缺陷重开 | 双方状态必须一致 | +| V09 | test部署成功 | 允许测试开始 | 不得发布完成 | +| V10 | 生产发布成功 | 发布完成并停留 | 不得自动已关闭 | +| V11 | 生产发布失败后重试 | 发布失败→发布中 | 原因和执行ID可审计 | +| V12 | 产品验收 | 已关闭 | 无验收证据不关闭 | + +## 8. 已知风险和回滚 + +- “全部关联任务完成”若无法按任务身份过滤,会把主任务或测试任务纳入,默认交桥接。 +- 仅标题前缀不能替代正式关系,也不能保证原生规则可过滤。 +- 需求和主任务双向镜像可能循环,只保留一个权威方向或增加来源防重。 +- 分支/MR只关联需求而未关联开发任务,会导致任务状态不更新。 +- 发布完成自动关闭会绕过产品验收;`L01`、`L02`默认停用。 + +回滚顺序:先禁用本次新增Y/A入口,恢复旧规则启用状态和顺序,再恢复状态流转边;保留已创建任务、关系和执行日志,删除任何资产需单独授权。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/pipeline-release.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/pipeline-release.md new file mode 100644 index 0000000..4290aca --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/pipeline-release.md @@ -0,0 +1,66 @@ +# 流水线、发布与最终验收 + +## 目录 + +1. 环境证据边界 +2. R11发布完成 +3. CI02回调POC +4. CI03证据分离 +5. R12最终验收 +6. L01–L02历史规则 +7. 连锁触发验证 + +## 1. 环境证据边界 + +分别记录以下事件,未经批准不得互相替代: + +- test环境部署成功。 +- 生产环境流水线发布成功。 +- 云效关联发布变更达到完成状态。 +- 产品/业务验收完成。 + +流水线成功不等于测试用例通过,test成功也不默认等于生产发布完成。 + +## 2. R11发布完成 + +优先使用“关联发布变更达到项目真实完成状态”作为R11证据。规则要求当前状态=`测试完成`,目标环境和发布证据满足组织口径。 + +如果组织明确把test部署定义为“完成发布”,必须记录该口径的批准证据;否则test回调只记录测试部署成功,不推进发布完成。 + +## 3. CI02回调POC + +1. 复制现有test流水线并使用明显POC名称。 +2. 保留原test与prod流水线不变。 +3. 只在副本Docker部署成功路径末尾添加回调。 +4. 普通变量传工作项ID、环境、流水线执行ID。 +5. 密钥变量保存认证材料,不写入仓库、命令输出或文档。 +6. 请求携带时间戳、签名和执行ID幂等键。 +7. 服务端校验需求当前状态,只允许预期流转执行一次。 +8. 测试成功、重复回调、错误签名、超时、部署失败和安全重试。 +9. POC通过后走推广评审,不直接修改prod。 + +## 4. CI03证据分离 + +CI03要求项目配置分别保存test、prod、发布变更和验收证据。任何桥接都必须记录实现方式、启用状态、负责人、证据和人工回退。 + +回调失败保留失败日志并可安全重试,不得伪造成功。同一执行ID重试不得多次推进需求。 + +## 5. R12最终验收 + +发布成功不等于业务验收。默认由产品/业务负责人确认后人工进入`已完成`或`已关闭`。只有可靠、可审计的验收任务才允许条件化自动关闭。 + +## 6. L01–L02历史规则 + +- `L01`:`发布完成 + 全部关联任务完成/关闭 → 项目终态`。默认停用或收紧为独立验收任务,避免早期阶段任务误满足。 +- `L02`:监听`测试完成 → 发布完成`状态路径并立即进入项目终态。默认停用,防止发布完成成为瞬时状态。 + +逐项目记录两条规则是否存在、是否启用、真实条件、顺序和证据;不存在也要有盘点证据。 + +## 7. 连锁触发验证 + +- 不按规则名称推断行为,逐条读取触发、条件、动作、顺序和执行账号。 +- 每个目标状态都检查后继规则和重复规则。 +- R11后至少观察两个时点:刚进入发布完成时,以及等待5–30秒后。 +- 未经验收自动进入终态时,记录实际后继规则并判定`误触发`。 +- 跨两级以上流转必须有独立负向测试,不能只验证最终状态。 + diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/report-template.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/report-template.md new file mode 100644 index 0000000..3bc1210 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/report-template.md @@ -0,0 +1,97 @@ +# 云效自动化执行报告模板 + +```markdown +# 云效产品需求全生命周期自动化执行报告 + +## 1. 基本信息 + +- 组织: +- 项目: +- 工作项类型: +- 流程模型:stage_tasks / oneos_delivery +- 迁移阶段(仅oneos_delivery):P0 / P1 / P2 / P3 +- 执行模式:audit / plan / apply / test / document +- 执行时间: +- 执行账号:仅记录显示名,不记录凭据 + +## 2. 观察到的当前配置 + +| 项目 | 生命周期 | 标签 | 仓库/集成分支 | test流水线 | 发布证据 | +|---|---|---|---|---|---| + +## 3. 规则变更 + +| 项目 | 规则 | 变更前 | 变更后 | 启用状态 | 复开核验 | +|---|---|---|---|---|---| + +### 3.0 项目级规则实例 + +| 项目 | 规则ID | 云效规则显示名/人工门禁 | 精确触发事件 | 源状态 | 必要条件 | 动作/目标状态 | 顺序 | 启用 | 观察证据 | +|---|---|---|---|---|---|---|---:|---|---| + +每个项目必须分别覆盖`R01`–`R12`和`TK01`–`TK08`。不得把一个项目的规则ID、显示名、顺序或证据复制为另一个项目的观察结果。 + +### 3.1 完整性台账 + +| 控制ID | 对象/流转 | 实现方式 | 启用状态 | 观察证据 | 负责人 | 回退方案 | 结论 | +|---|---|---|---|---|---|---|---| + +每个项目必须分别覆盖全部31项:`R01`–`R12`、`TK01`–`TK08`、`TC01`–`TC03`、`DF01`–`DF03`、`CI01`–`CI03`、`L01`–`L02`。不得把“规则编辑器不支持”写成“已完成”;应记录为`not_supported`并给出人工门禁或集成桥接方案。 + +`oneos_delivery`模型改用48项台账:`Y01`–`Y16`、`Y20`–`Y22`、`Y30`–`Y35`、`Y40`、`A01`、`A02`、`A02B`、`A03`–`A10`、`TC01`–`TC03`、`DF01`–`DF03`、`CI01`–`CI03`、`L01`–`L02`。 + +### 3.1a OneOS迁移就绪度 + +| 项目 | 目标状态缺口 | 主任务身份方式 | 原生规则能否过滤 | 任务工作流方案 | 桥接幂等/负责人 | 旧规则处置 | 结论 | +|---|---|---|---|---|---|---|---| + +旧R/TK规则只有在目标状态、任务身份、任务工作流、桥接和回滚证据全部就绪后才能停用。`L01`、`L02`不受此等待条件影响,应保持停用以保护产品验收。 + +### 3.2 规则顺序与连锁触发 + +| 前置规则 | 目标状态 | 后继规则 | 是否自动二次流转 | 是否绕过验收 | 处理结论 | +|---|---|---|---|---|---| + +## 4. 验证结果 + +| 用例 | 触发事件 | 期望状态 | 实际状态 | 结果分类 | 执行日志/证据 | +|---|---|---|---|---|---| + +## 5. 研发资产覆盖 + +| 仓库 | dev/develop | 分支关联 | 提交关联 | MR关联 | 全部合并判断 | +|---|---|---|---|---|---| + +## 6. 流水线与发布 + +| 流水线 | 环境 | 是否副本 | Docker部署 | 回调 | 是否影响prod | +|---|---|---|---|---|---| + +## 7. 测试计划与缺陷闭环 + +| 测试计划 | 用例总数 | 已通过 | 未执行/待测试 | 阻塞 | 未通过 | 结论 | +|---|---:|---:|---:|---:|---:|---| + +| 缺陷 | 关联用例/需求 | 修复状态 | 复测结果 | 最终状态 | 证据 | +|---|---|---|---|---|---| + +| 失败用例 | 关联缺陷 | 缺陷已修复后用例状态 | 复测通过后用例状态 | 复测失败后用例状态 | 证据 | +|---|---|---|---|---|---| + +## 8. 人工节点与剩余风险 + +- 真实开发开始时间: +- 业务验收: +- 发布完成是否被后继规则立即关闭: +- 全部关联任务范围: +- 多仓库漏关联风险: +- 其他: +- 范围外通用自动化(通知、指派、SLA、字段同步等): + +## 9. 回滚 + +- 禁用新增规则: +- 恢复旧规则: +- 停用回调POC: +- 测试资产清理(需授权): +``` diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/role-trigger-prompts.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/role-trigger-prompts.md new file mode 100644 index 0000000..71815ff --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/role-trigger-prompts.md @@ -0,0 +1,499 @@ +# 云效全流程角色与触发节点沟通话术 + +> 本文是异常处理和精细交接使用的详细版。日常操作优先读取`simple-role-commands.md`,使用“记录需求、开始分析、开始开发、提交测试、测试完成、发布成功、验收通过”等短口令。 + +## 目录 + +1. 使用规则 +2. 通用话术骨架 +3. 项目管理员与流程负责人 +4. 产品负责人 +5. 需求分析人员 +6. 设计人员 +7. 交付负责人 +8. 技术负责人 +9. 开发人员 +10. 代码评审人员 +11. 测试负责人和测试人员 +12. 缺陷修复人员 +13. 运维与发布负责人 +14. 业务验收人员 +15. 集成桥接负责人 +16. 异常和催办话术 +17. 节点交接清单 + +## 1. 使用规则 + +这套话术用于让不同角色把真实业务事件准确告诉Codex,再由Codex审计、规划、执行获批修改、验证或形成报告。默认使用`yunxiao-requirement-lifecycle`。 + +每次沟通至少提供: + +- 精确项目名和需求编号。 +- 当前角色、当前状态和刚刚发生的真实事件。 +- 关联任务、仓库、分支、MR、测试计划、缺陷或流水线执行ID;没有就明确写“无”。 +- 希望Codex采用的权限:`audit`、`plan`、`apply`、`test`或`document`。 +- 期望停在哪个状态,不要只说“继续流转”。 + +权限含义: + +- `audit`:只核查和报告,不修改云效。 +- `plan`:只输出修改方案和影响,不执行。 +- `apply`:只修改话术中明确列出的项目、工作项或规则。 +- `test`:使用隔离测试资产触发并收集证据。 +- `document`:只形成说明、记录或报告。 + +必须遵守: + +- 标题、需求编号、分支名或标签不能代替云效正式关联。 +- 首次提交不代表真实开始开发;人工“开始处理”是效能主口径,分支关联只是自动兜底。 +- `已修复`只表示交给测试复测,不是缺陷关闭。 +- test部署成功不等于测试通过,更不等于生产发布完成。 +- 发布成功后停留`发布完成`,必须有产品或业务验收才能进入`已关闭`。 +- 未明确授权时,不改生产流水线、不直接推送保护分支、不删除规则或测试资产。 +- 项目仍为`stage_tasks`时使用R/TK规则;只有迁移门禁通过后才使用完整`oneos_delivery`链路。 + +## 2. 通用话术骨架 + +### 2.1 请求执行 + +```text +使用 yunxiao-requirement-lifecycle。 +权限:【audit/plan/apply/test/document】 +项目:【精确项目名】 +流程模型:【未知/stage_tasks/oneos_delivery】 +我的角色:【角色】 +需求:【需求编号+标题】 +当前状态:【状态】 +刚发生的事件:【真实事件】 +相关资产:【任务/MR/分支/测试计划/缺陷/流水线执行ID;没有写无】 +期望结果:【目标状态或需要生成的资产】 +请先核查当前状态和正式关联,再执行授权范围内动作;输出实际动作、触发结果、证据、未完成项和下一责任人。不要用手工改状态冒充自动化通过。 +``` + +### 2.2 只核查为什么没有流转 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +项目:【项目名】,需求:【编号】,当前状态:【状态】。 +我已执行:【事件】,相关对象:【对象编号或链接】;等待了【秒数】仍未流转。 +请按事件、当前状态、标签位置、正式关联、规则启用、执行账号、执行日志、后继规则和异步窗口逐项诊断。不要直接替我改状态。 +``` + +### 2.3 请求修改规则 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +只允许修改项目【项目名】中的规则【精确规则名/控制ID】,目标是【目标】。 +修改前备份触发、条件、动作、顺序、启用状态和执行账号;一次只改一条,保存后重新打开核对,并用隔离需求做正向和负向测试。禁止修改生产流水线及无关规则。 +``` + +## 3. 项目管理员与流程负责人 + +### ADM01 项目首次审计或接管 + +触发:新项目接入、规则无人维护或准备复制到另一个项目。 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +请审计项目【项目名】,先判断是stage_tasks、oneos_delivery还是迁移中。盘点工作流状态、标签、工作项类型、全部自动化规则、执行账号、前后端仓库、dev/develop、Testhub、缺陷状态、test/prod流水线、发布证据和验收门禁。 +输出当前—目标差距、规则控制ID映射、风险、P0-P3实施顺序和不能自动化的人工门禁。不要修改线上配置。 +``` + +完成证据:项目画像、规则清单、模型判断、缺口和迁移阶段齐全。 + +### ADM02 迁移到OneOS终态模型 + +触发:准备从阶段任务切换到交付主任务模型。 + +```text +使用 yunxiao-requirement-lifecycle,权限=plan。 +项目【项目名】准备迁移到oneos_delivery。请检查待测试、发布中、发布失败状态,交付主任务识别方式,开发/测试/发版任务工作流,A03/A06/A08/A09幂等与负责人,多仓库MR门禁、Testhub分包、生产证据和L01/L02状态。 +只有十项迁移门禁全部通过才给出停用旧R/TK规则清单;否则保留现网主链,只列安全可做项和回滚方案。 +``` + +完成证据:迁移门禁逐项结论,而不是笼统“可以迁移”。 + +### ADM03 规则变更后回归 + +触发:新建、修改、启停任何生命周期规则。 + +```text +使用 yunxiao-requirement-lifecycle,权限=test。 +项目【项目名】刚修改规则【规则名/控制ID】。请创建或复用隔离测试需求,验证事件【事件】、源状态【状态】、目标状态【状态】,并增加错误状态、无正式关联、部分MR、重复回调或后继连锁等负向场景。 +结果只能记录为通过、异步通过、未触发、误触发或阻塞,并附执行日志和回滚入口。 +``` + +### ADM04 定期治理 + +触发:季度检查、状态/标签重命名、新增仓库或人员离职。 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +请对项目【项目名】执行运行治理检查:规则执行账号和权限、失败日志、状态/标签重命名影响、新增仓库Webhook、多仓库MR覆盖、超过7天无活动分支、回调签名/幂等/重试、L01/L02以及生产和验收证据分离。 +输出需要立即修复、计划修复和仅观察三类清单。 +``` + +## 4. 产品负责人 + +### PO01 新建并确认需求 + +触发:需求范围、负责人、优先级和验收标准已明确。 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需要建立需求【标题】。范围:【范围】;不包含:【排除项】;负责人:【姓名】;优先级:【级别】;目标迭代:【迭代】;验收标准:【标准】。 +请先查重,再创建或完善产品类需求。若项目为oneos_delivery,校验唯一【交付】主任务;若仍为stage_tasks,不要擅自创建主任务。完成后告诉我需求编号、正式关系、当前状态和下一责任人。 +``` + +### PO02 批准进入分析或快轨待开发 + +触发:需求确认后决定正常分析,或低风险小改走快轨。 + +正常路径: + +```text +项目【项目名】需求【编号】已确认,批准进入分析。使用 yunxiao-requirement-lifecycle,权限=apply。请核查范围、负责人和验收标准齐全,再按当前流程模型触发分析入口;不要跳过缺失信息。 +``` + +快轨路径: + +```text +项目【项目名】需求【编号】批准快轨到待开发,批准人【姓名】,原因【原因】,验收标准【标准】。使用 yunxiao-requirement-lifecycle,权限=apply。请确认快轨边存在、唯一交付主任务已同步且没有循环风险;缺少任一条件就停止并报告。 +``` + +### PO03 需求范围变更 + +触发:开发或测试中修改范围。 + +```text +使用 yunxiao-requirement-lifecycle,权限=plan。 +项目【项目名】需求【编号】当前【状态】,拟变更:【新增/删除内容】;原因:【原因】;受影响资产:【开发任务/MR/用例/缺陷/发布范围】。 +请先做影响分析,列出需要重开或新增的任务、MR、用例和验收项,以及是否应回退需求状态。未经我确认不要直接改状态或删除已有证据。 +``` + +### PO04 产品验收通过并关闭 + +触发:生产发布完成,业务验收通过。 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需求【编号】当前为发布完成。验收人【姓名】,验收时间【时间】,验收环境【生产】,验收结果【通过】,证据【链接/附件/记录】。 +请核查生产发布证据与需求范围一致,并确认不存在未关闭验收项;满足后执行已关闭并回报审计证据。不得用流水线成功替代本次验收。 +``` + +### PO05 验收不通过 + +```text +使用 yunxiao-requirement-lifecycle,权限=plan。 +项目【项目名】需求【编号】生产验收不通过。问题:【问题】;证据:【证据】;影响:【影响】。 +请创建或关联可追踪缺陷/改进任务,建议应回到的真实状态和责任人;未经批准不要直接关闭需求或覆盖原发布记录。 +``` + +## 5. 需求分析人员 + +### BA01 开始分析 + +```text +项目【项目名】需求【编号】由我开始分析,实际开始时间【时间】。使用 yunxiao-requirement-lifecycle,权限=apply。请核查当前状态和我的关联任务,将分析任务改为处理中并确认需求进入分析中;如果项目为oneos_delivery,则按主任务权威方向同步,避免双向循环。 +``` + +### BA02 分析完成 + +```text +项目【项目名】需求【编号】分析已完成。产物:【需求说明/流程/字段/边界链接】;未决项:【无或列表】;验收口径:【口径】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请先检查分析任务非零且全部完成、产物可访问、未决项已处理,再推进分析完成;回报触发的是原生规则、桥接还是人工门禁。 +``` + +## 6. 设计人员 + +### UX01 开始设计 + +```text +项目【项目名】需求【编号】开始设计,设计负责人【姓名】,原型地址【地址】,实际开始时间【时间】。使用 yunxiao-requirement-lifecycle,权限=apply。请核查正式关联设计任务和当前状态,再更新设计任务为处理中并确认需求进入设计中。 +``` + +### UX02 设计完成并交接开发 + +```text +项目【项目名】需求【编号】设计完成。原型版本【版本】;设计稿【链接】;交互说明【链接】;响应式范围【范围】;已确认人【姓名】;遗留项【无或列表】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请核查设计任务和交付物后完成设计阶段,并检查是否只创建一组正确的开发任务。不要因为标题含“开发”就视为已正式关联。 +``` + +完成证据:设计任务已完成、需求到设计完成;后继开发任务创建成功后才到待开发。 + +## 7. 交付负责人 + +### DL01 创建或核查唯一交付主任务 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需求【编号】需要建立【交付】主任务,负责人【姓名】。请先按正式关系和交付标签查重;不存在才创建,存在一条则复用,存在多条则停止并列出冲突。主任务必须正式关联需求,不能只靠标题前缀。 +``` + +### DL02 阶段同步异常 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +项目【项目名】需求【编号】状态【需求状态】,交付主任务【任务编号】状态【任务状态】,两者不一致。 +请判断权威方向、检查Y01-Y16、来源防重和执行日志,说明应补偿哪个对象。不要同时双向写入造成循环。 +``` + +## 8. 技术负责人 + +### TL01 拆分开发任务 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需求【编号】已到待开发。请按以下范围拆分开发任务:前端【范围/负责人/仓库】,后端【范围/负责人/仓库】,其他【范围】。每条任务必须正式关联需求和交付主任务、标签=开发、状态=待处理,并记录目标dev/develop分支。查重后创建,禁止重复任务。 +``` + +### TL02 仅前端或仅后端需求 + +```text +项目【项目名】需求【编号】只涉及【前端/后端】,不涉及【另一端】。使用 yunxiao-requirement-lifecycle,权限=apply。请只创建实际需要的开发任务,并把不涉及的仓库明确记为不适用;不要用缺少另一端MR阻塞,也不要把未盘点仓库默认为已完成。 +``` + +### TL03 开发完成门禁核查 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +项目【项目名】需求【编号】准备判定开发完成。相关仓库:【列表】;开发任务:【列表】;MR:【列表及目标分支】。 +请确认所有非取消开发任务完成、所有相关MR正式双关联并合并到真实dev/develop;任一仓库未完成都保持开发中。输出缺口,不要手工改成开发完成。 +``` + +## 9. 开发人员 + +### DEV01 真实开始开发并创建分支 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +我开始处理项目【项目名】需求【编号】的开发任务【任务编号】,真实开始时间【时间】,仓库【仓库名】,基线【dev/develop】,拟建分支【feature/编号或fix/编号】。 +请先确认基线真实存在且任务为待处理,再从基线创建或指导创建分支,并确保分支同时正式关联需求和开发任务。完成后核查需求=开发中、任务=处理中;不要用首次提交时间覆盖真实开始时间。 +``` + +### DEV02 提交代码并创建MR + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需求【编号】,开发任务【任务编号】,仓库【仓库】,分支【分支】,目标分支【dev/develop】。改动摘要:【摘要】;本地验证:【命令和结果】。 +请先拉取并检查冲突,再按仓库规范提交、推送并创建MR;MR必须正式关联需求和开发任务。不要直接推保护分支,不要因为提交成功就把开发标为完成。 +``` + +### DEV03 多仓库部分完成 + +```text +项目【项目名】需求【编号】目前仅仓库【仓库A】的MR【编号】已合并,仓库【仓库B】仍在开发。使用 yunxiao-requirement-lifecycle,权限=audit。请确认需求和交付主任务仍保持开发中,并检查是否存在“部分MR导致提前完成”的误触发规则。 +``` + +## 10. 代码评审人员 + +### REV01 评审要求修改 + +```text +项目【项目名】MR【编号】评审不通过,需要修改:【问题列表】。使用 yunxiao-requirement-lifecycle,权限=document。请把评审结论关联到需求【编号】和开发任务【编号】,确认MR保持未合并、开发任务保持处理中、需求保持开发中。 +``` + +### REV02 MR合并后核查 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +项目【项目名】MR【编号】已合并到【dev/develop】,关联需求【编号】、开发任务【编号】。请等待异步窗口后核查任务状态;再汇总同需求所有仓库和MR。只有全部相关任务和MR完成才允许需求进入开发完成,并确认测试任务只创建一条。 +``` + +## 11. 测试负责人和测试人员 + +### QA01 建立测试任务和用例范围 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需求【编号】开发已完成,目标迭代【迭代】,测试负责人【姓名】。请查重后创建或复用唯一【测试】任务,并将测试计划按需求编号建立独立用例包。范围:【功能/接口/权限/兼容/回归】;不适用:【列表】。正式关联需求、主任务和测试计划后进入待测试。 +``` + +### QA02 test部署成功,开始测试 + +```text +项目【项目名】需求【编号】的test部署成功,流水线【名称】,执行ID【ID】,环境【test】,部署时间【时间】。使用 yunxiao-requirement-lifecycle,权限=apply。请校验执行ID、范围和重复回调,再允许测试任务和需求进入测试中。明确记录:本事件不能推进发布完成。 +``` + +### QA03 用例失败并提交缺陷 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需求【编号】,测试计划【计划】,用例【用例ID】执行失败。环境【环境】;前置条件【条件】;步骤【步骤】;实际结果【实际】;期望结果【期望】;证据【截图/日志】;严重程度【级别】。 +请查重后创建或关联缺陷,并把缺陷与原用例、需求正式关联。保持用例未通过、测试任务和需求测试中;不要把创建缺陷当作测试完成。 +``` + +### QA04 缺陷复测通过 + +```text +项目【项目名】缺陷【编号】已在环境【环境】复测,原用例【用例ID】重新执行通过,证据【证据】,复测人【姓名】,时间【时间】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请将原用例更新为通过,并把缺陷进入项目真实关闭态;随后重新核查本需求是否仍有未执行、阻塞、失败用例或未关闭缺陷。 +``` + +### QA05 缺陷复测失败 + +```text +项目【项目名】缺陷【编号】复测失败,原用例【用例ID】仍未通过。实际结果【结果】;证据【证据】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请重开缺陷或转回项目真实处理中状态,保持原用例未通过、需求测试中,并通知原修复负责人。不要保留“已修复/已关闭”。 +``` + +### QA06 测试验收完成 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】需求【编号】申请完成测试。测试计划【计划】;用例包【范围】;执行统计【通过/失败/阻塞/未执行】;缺陷清单【编号+状态】;测试负责人【姓名】。 +请按TC01-TC03和DF01-DF03核查。只有全部范围用例已执行并通过、失败用例已复测、缺陷由测试关闭时,才完成测试任务并推进需求测试完成;否则列出阻塞项。 +``` + +## 12. 缺陷修复人员 + +### BUG01 接收缺陷并开始修复 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +我接收项目【项目名】缺陷【编号】,关联需求【编号】、原用例【ID】,开始修复时间【时间】,仓库【仓库】,拟用分支【fix/缺陷编号】。 +请核查缺陷、需求和用例的正式关联,再把缺陷转入真实处理中状态;分支和MR同时关联缺陷、需求及对应开发任务。 +``` + +### BUG02 修复完成交给测试 + +```text +项目【项目名】缺陷【编号】已修复。分支【分支】;MR【编号】;合并目标【dev/develop】;验证结果【结果】;部署环境【test/尚未部署】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请核查代码和部署证据后把缺陷改为已修复并交给测试复测。不要关闭缺陷,不要把原失败用例改为通过。 +``` + +## 13. 运维与发布负责人 + +### OPS01 test部署回报 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +项目【项目名】test流水线【名称】执行【成功/失败】,执行ID【ID】,提交/MR范围【范围】,时间【时间】。 +请核查本次执行关联的需求和幂等记录。成功只允许测试开始;失败保持原状态并记录原因。不要触发生产发布完成。 +``` + +### OPS02 创建迭代发版任务 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】迭代【迭代名/ID】准备发版,计划窗口【时间】,负责人【姓名】,本批需求【编号列表】,生产流水线【名称】。 +请查重后创建或复用唯一【发版】任务,正式关联迭代和本批需求,并生成发布范围与回滚项。需求未测试完成或范围不一致时停止,不进入发布中。 +``` + +### OPS03 开始生产发布 + +```text +项目【项目名】发版任务【编号】批准开始生产发布。审批人【姓名】;窗口【时间】;生产流水线【名称】;本批需求【列表】;回滚方案【链接】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请核查全部需求测试完成、范围一致和审批证据,再进入发布中。只操作明确的生产发布,不修改流水线定义。 +``` + +### OPS04 生产发布成功 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】生产流水线【名称】执行成功,执行ID【ID】,环境【prod】,完成时间【时间】,发布范围【需求/MR/版本】,证据【链接】。 +请验证签名、时间戳、执行ID幂等和范围,推进发版任务及本批需求到发布完成并停留,通知产品验收。禁止自动进入已关闭。 +``` + +### OPS05 生产发布失败或重试 + +失败: + +```text +项目【项目名】生产流水线【名称】执行失败,执行ID【ID】,失败阶段【阶段】,原因【原因】,影响需求【列表】,回滚结果【结果】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请记录失败证据并推进到发布失败;不得伪造成功或关闭需求。 +``` + +重试: + +```text +项目【项目名】发版任务【编号】批准从发布失败重试,批准人【姓名】,新执行ID【ID】,已修正原因【说明】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请核查发布失败→发布中流转边、幂等键和范围后开始重试,保留原失败记录。 +``` + +## 14. 业务验收人员 + +### ACC01 验收通过 + +```text +我是业务验收人【姓名】。项目【项目名】需求【编号】已在生产环境按验收项【列表】验证通过,时间【时间】,证据【链接/附件】。 +使用 yunxiao-requirement-lifecycle,权限=apply。请核查身份、发布完成状态和证据范围后关闭需求,并保留验收记录。 +``` + +### ACC02 验收不通过 + +```text +我是业务验收人【姓名】。项目【项目名】需求【编号】生产验收不通过,失败项【列表】,实际结果【结果】,证据【证据】。 +使用 yunxiao-requirement-lifecycle,权限=plan。请建立可追踪问题清单,分析应回到测试、开发还是重新发布,并指定下一责任人;不要关闭需求。 +``` + +## 15. 集成桥接负责人 + +### INT01 回调未生效或重复 + +```text +使用 yunxiao-requirement-lifecycle,权限=audit。 +桥接控制【A03/A06/A08/A09/其他】,项目【项目名】,需求【编号】,事件ID【ID】,时间戳【时间】,回调结果【未生效/重复/失败】,日志摘要【摘要】。 +请核查签名、时间窗、防重放、幂等键、当前状态、权限、重试和审计日志。不要绕过校验直接补写状态;给出安全重试或人工回退方案。 +``` + +### INT02 幂等补偿 + +```text +使用 yunxiao-requirement-lifecycle,权限=apply。 +项目【项目名】控制【控制ID】需要补偿,原事件ID【ID】,期望对象【任务/状态】,当前对象【实际】。已确认原操作未成功且不存在重复对象。 +请再次查询幂等键和正式关系,只补偿缺失动作;完成后输出新旧事件关联、对象ID和重复检查证据。 +``` + +## 16. 异常和催办话术 + +### 状态长时间不动 + +```text +项目【项目名】需求/任务【编号】在【状态】停留【时长】,最后真实事件【事件】,负责人【姓名】,关联资产【列表】。使用 yunxiao-requirement-lifecycle,权限=audit。请判断是业务未完成、自动化未触发还是异步失败,并给出下一责任人和最小恢复动作;不要直接跳状态。 +``` + +### 状态被提前推进 + +```text +项目【项目名】需求【编号】从【源状态】意外进入【目标状态】,时间【时间】。使用 yunxiao-requirement-lifecycle,权限=audit。请检查后继规则、全部关联项口径、部分MR、未复测缺陷、test/prod混用和L01/L02,定位真实触发规则并给出回滚建议。未经确认不要回退状态。 +``` + +### 任务重复创建 + +```text +项目【项目名】需求【编号】出现重复【交付/开发/测试】任务:【任务列表】。使用 yunxiao-requirement-lifecycle,权限=audit。请检查幂等键、正式关系和重复回调,确定保留项及合并/取消方案。未经授权不要删除任务。 +``` + +## 17. 节点交接清单 + +每次角色交接都让Codex确认以下内容: + +1. 项目、需求、任务和流程模型准确。 +2. 源状态与目标状态明确。 +3. 事件真实发生,且正式关联可见。 +4. 当前责任人和下一责任人明确。 +5. 交付物、代码、用例、缺陷或发布证据可访问。 +6. 正向条件全部满足,负向阻塞项为零。 +7. 自动化结果与人工准备动作分开记录。 +8. 异步等待后重新核查状态和执行日志。 +9. 没有误触发后继规则或重复创建。 +10. 回滚入口、人工回退和未完成事项已记录。 + +Codex的节点回报统一使用: + +```text +项目: +流程模型: +工作项: +源状态 → 实际状态: +本次真实事件: +执行动作: +触发方式:原生规则/桥接/人工门禁/未触发 +验证结果:通过/异步通过/未触发/误触发/阻塞 +证据: +未完成项: +下一责任角色: +允许的下一动作: +回滚入口: +``` diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/simple-role-commands.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/simple-role-commands.md new file mode 100644 index 0000000..b94c7b6 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/simple-role-commands.md @@ -0,0 +1,417 @@ +# 云效日常口语化操作口令 + +## 1. 先说一次项目 + +每次新会话先输入: + +```text +项目:统一运营管理平台PC端 +``` + +之后直接说口令即可。项目没有明确、同一编号可能属于多个项目或用户切换项目时,Codex必须先问清项目,不能猜。 + +口令可以自然表达,不要求标点完全一致。例如: + +- `记录需求:增加车辆批量导出` +- `帮我记录一个需求,增加车辆批量导出` + +两句话含义相同。 + +## 2. 产品和需求 + +### 记录需求 + +产品输入: + +```text +记录需求:增加车辆批量导出 +``` + +Codex执行:查重后在当前项目创建产品类需求,初始状态为`待处理`,返回需求编号。只有标题也可以先记录;云效必填字段缺失时只追问缺少的字段。 + +**带模块/原型的记录(OneOS):** 若口令含模块名或原型(如「记录需求:保险采购」),创建前**先调用 `$oneos-autoprd`**,把「需求说明」写入描述;若用户同时定稿,一并写入「更新内容」。 + +**统一运营管理平台 · 快路径(强制):** 项目为「统一运营管理平台 / PC 端」时,先读 [oneos-pc-fast-path.md](oneos-pc-fast-path.md) 与 [oneos-pc-runtime-ids.json](../assets/oneos-pc-runtime-ids.json)。用已验证 `POST /workitem/workitem` 建单(创建时带 `document`),不要探测 `/workitem/create`。推进至「开发中」时状态连跳:`待处理 → 设计完成 → 待开发 → 开发中`。 + +**优先级 / 推进至 Plan 单选门禁(强制):** 用户要求 Plan 模式单选,或口令含「优先级」「推进至」需点选时: + +1. 先用 `AskQuestion`(可用时)或 Plan 确认拿到 A(紧急/高/中/低)与 B(分析中/设计中/设计完成/开发中)。 +2. **禁止**未选时默认「中 + 分析中」并继续建单。 +3. 「批准 / Implement 计划」本身不等于已选 A+B;计划里仍是空选项时必须先问清。 +4. 标签须用户从云效标签 catalog(`oneos-pc-tag-catalog.md`)点选;禁止按业务条线说明/`lines.ts` 自动推断。点选后 API 打标,失败则停并请用户回复标签名,禁止假装成功。 + +**完整可复制提示词(推荐):** 统一运营管理平台建单优先用 [oneos-pc-record-requirement-prompt.md](oneos-pc-record-requirement-prompt.md)——含 `$yunxiao-requirement-lifecycle` + `$oneos-autoprd`、A/B/C 三门禁与 30 项标签枚举。复制该文件「用户口令」整段发给 Agent;确认后再执行快路径。 + +简短版(填需求名与模块后复制): + +```text +使用 $yunxiao-requirement-lifecycle + $oneos-autoprd +记录需求到云效 · 统一运营管理平台 + +【需求名】(填写) +【描述来源】$oneos-autoprd,原型/模块:(填写) + +请先 Plan 让我点选 A 优先级、B 推进至、C 标签(30 项 catalog,见 oneos-pc-record-requirement-prompt.md;禁止 lines.ts 推断)。 +我确认后再执行快路径建单。 +``` + +确认回复示例:`优先级:高;推进至:设计完成;标签:运维管理条线` + +### 确认需求 + +产品输入: + +```text +确认需求:ONEOSP-123 +``` + +Codex执行:检查范围、负责人、优先级和验收标准,齐全后把需求改为`已确认`。 + +`确认需求`默认不创建分析任务。确认和真实开始分析是两个事件,防止开始时间失真。由分析人员下一步输入`开始分析`。 + +### 需求定稿(原型 + AutoPRD) + +产品输入: + +```text +保险采购需求定稿 +``` + +或: + +```text +需求定稿:ONEOSP-123;模块=保险采购 +``` + +Codex执行: + +1. 调用 `$oneos-autoprd` 定稿流程:更新原型 `.spec/requirements-prd.md` 第 10 章「功能变更记录」与 `autoprd-baseline.json`(只记功能/逻辑,不记样式/UI/表结构)。 +2. 若已有云效需求编号:刷新描述中的「需求说明」与「更新内容」;旧更新内容 →「更新内容·历史」。 +3. 若口令要求进待开发/发对象存储:继续走下方「快速进入开发」或 OneOS 快轨口令,不得跳过 AutoPRD。 + +### 快速进入开发 + +产品输入: + +```text +快速开发:ONEOSP-123;原因=小改动;负责人=张三;范围=只改后端 +``` + +Codex执行:检查项目是否允许快轨、验收标准是否明确,再跳过不需要的分析/设计阶段并建立必要开发任务。条件不齐时不跳状态,只提示缺少什么。 + +**OneOS 模块已定型进待开发(推荐):** + +```text +「保险采购」需求已经确定:先 AutoPRD 写需求说明与更新内容;发布对象存储并写入描述; +推进到待开发;自动建与需求同名的【交付】任务并正式关联;负责人何斐。 +``` + +执行前必须先跑 `$oneos-autoprd`(含定稿变更记录若用户刚定稿)。进入待开发时**必须**执行 [auto-stage-task.md](auto-stage-task.md)(同名任务、标签交付、负责人何斐、时间沿用需求);已有唯一交付任务则复用并改负责人为何斐,禁止建第二条。 + +### 需求变更 + +产品输入: + +```text +变更需求:ONEOSP-123;增加导出时间筛选 +``` + +Codex执行:先分析受影响的任务、MR、用例、缺陷和发布范围,给出应回到哪个阶段;产品确认后再修改,不直接覆盖历史说明。涉及原型功能变更时,同步 `$oneos-autoprd` 更新 PRD;用户再说「定稿」时写入第 10 章。 + +## 3. 分析和设计 + +### 开始分析 + +分析人员输入: + +```text +开始分析:ONEOSP-123;负责人=李四 +``` + +Codex执行: + +1. 将需求推进到`分析中`(或确认已在分析中)。 +2. **按 [auto-stage-task.md](auto-stage-task.md)**:查重后创建/复用与需求**同名**任务;标签=`分析`;正式关联需求;负责人默认=需求**创建人**(口令写了负责人则用口令);计划开始/创建时间沿用需求。 +3. 任务进入`处理中`(或项目约定的开工态)。 +4. `stage_tasks`:需求标签含`分析`。`oneos_delivery`:不因此再建第二条【交付】主任务。 + +### 分析完成 + +分析人员输入: + +```text +分析完成:ONEOSP-123;说明=https://... +``` + +Codex执行:检查分析产物和未决项;完成对应任务或主任务阶段,核查需求进入`分析完成`。 + +### 开始设计 + +设计人员输入: + +```text +开始设计:ONEOSP-123;负责人=王五 +``` + +Codex执行: + +1. 将需求推进到`设计中`。 +2. **按 auto-stage-task**:同名任务;标签=`设计`;正式关联;负责人默认=需求**创建人**(口令可覆盖);时间沿用需求。 +3. 任务开工态同上。 +4. `oneos_delivery`:不同时新建交付主任务(交付主任务在待开发时建/复用)。 + +### 设计完成 + +设计人员输入: + +```text +设计完成:ONEOSP-123;原型=https://... +``` + +Codex执行:检查原型或设计说明,完成设计任务/主任务阶段,核查需求进入`设计完成`。 + +如果项目已经部署并验证自动建开发任务的桥接,而且负责人、仓库和范围明确,Codex继续查重并创建开发任务;否则停在`设计完成`,提示技术负责人输入`安排开发`。不得创建无负责人或重复任务。 + +**进入待开发时(含快轨):** 必须再跑 auto-stage-task:同名任务、标签`交付`(或`开发`)、负责人**何斐**、正式关联、时间沿用需求。 + +## 4. 开发 + +### 安排开发 + +技术负责人输入: + +```text +安排开发:ONEOSP-123;前端=张三/ln-one-os-web;后端=李四/ln-cloud +``` + +只改一端时可以说: + +```text +安排开发:ONEOSP-123;只改后端=李四/ln-cloud +``` + +Codex执行:按实际范围创建一条或多条`【开发】`任务,正式关联需求;`oneos_delivery`还要关联交付主任务。任务初始为`待处理`,需求进入`待开发`。明确“不涉及”的仓库不会成为完成门禁。 + +### 开始开发 + +开发人员输入: + +```text +开始开发:ONEOSP-123;任务=TASK-456;仓库=ln-cloud +``` + +Codex执行:记录真实开始时间,确认真实基线`dev`或`develop`,创建`feature/ONEOSP-123`并同时正式关联需求和开发任务。任务进入`处理中`,需求进入`开发中`。 + +首次提交只作兜底,不覆盖这次真实开始时间。 + +### 提交代码 + +开发人员输入: + +```text +提交代码:ONEOSP-123;任务=TASK-456;仓库=ln-cloud +``` + +Codex执行:检查改动和测试,先拉取并处理冲突,再提交、推送并创建合并到真实`dev/develop`的MR;MR同时关联需求和开发任务。提交成功不等于开发完成。 + +### 开发完成 + +开发人员或技术负责人输入: + +```text +开发完成:ONEOSP-123 +``` + +Codex执行:汇总该需求所有非取消开发任务、前后端仓库和正式关联MR。只有全部任务完成、全部相关MR已合并,才确认需求进入`开发完成`;少一个仓库也不提前完成。 + +条件满足后,如果项目已配置并验证唯一测试任务桥接,则查重创建测试任务;否则提示测试负责人输入`提交测试`。 + +## 5. 测试和缺陷 + +### 提交测试 + +测试负责人或开发负责人输入: + +```text +提交测试:ONEOSP-123;test流水线=oneos-web-test +``` + +Codex执行:确认开发完成,触发或核查明确的test流水线。部署成功后,查重创建并正式关联唯一测试任务和需求用例包,进入`待测试/测试中`的项目真实状态。 + +test成功只允许开始测试,不能变成发布完成。 + +### 开始测试 + +测试人员输入: + +```text +开始测试:ONEOSP-123;负责人=赵六 +``` + +Codex执行:核查test部署和测试任务,把测试任务改为`测试中/处理中`,需求进入`测试中`。 + +### 发现Bug + +测试人员输入: + +```text +发现Bug:ONEOSP-123;用例=CASE-12;现象=车辆来源显示0;期望=显示中文来源 +``` + +Codex执行:查重后创建缺陷,正式关联需求、原失败用例和必要开发任务;用例保持未通过,需求保持`测试中`。 + +### Bug修复完成 + +开发人员输入: + +```text +Bug修复完成:BUG-123;MR=456;已部署test +``` + +Codex执行:核查代码、MR和test部署后,把缺陷改为`已修复`并交给测试复测。不会关闭缺陷,也不会把原用例直接改成通过。 + +### 复测通过 + +测试人员输入: + +```text +复测通过:BUG-123;用例=CASE-12 +``` + +Codex执行:重新记录原用例为通过,把缺陷改为项目真实关闭态,再检查该需求是否还有失败、阻塞、未执行用例或未关闭缺陷。 + +### 复测失败 + +测试人员输入: + +```text +复测失败:BUG-123;用例=CASE-12;现象=仍显示0 +``` + +Codex执行:原用例保持未通过,缺陷重开或回到处理中,并通知修复负责人;需求保持`测试中`。 + +### 测试完成 + +测试负责人输入: + +```text +测试完成:ONEOSP-123 +``` + +Codex执行:检查本需求用例包,不按整个迭代一刀切。只有没有未执行、阻塞、失败用例,且所有缺陷都经过测试复测关闭,才完成测试任务并推进需求到`测试完成`。 + +## 6. 发布和验收 + +### 准备发布 + +运维或项目负责人输入: + +```text +准备发布:迭代V1.3;需求=ONEOSP-123,ONEOSP-124;窗口=今晚22:00 +``` + +Codex执行:检查本批需求均测试完成,查重创建或复用唯一`【发版】`任务,正式关联迭代和需求,生成发布范围及回滚清单。 + +### 开始发布 + +运维输入: + +```text +开始发布:发版任务=TASK-900;prod流水线=oneos-prod;审批人=王经理 +``` + +Codex执行:核查审批、本批范围、生产流水线和回滚方案后进入`发布中`。这句话授权执行明确的本次生产发布,但不授权修改生产流水线定义。 + +### 发布成功 + +运维输入: + +```text +发布成功:执行ID=PIPE-789;发版任务=TASK-900 +``` + +Codex执行:到生产流水线核查环境、执行ID、范围和成功证据,确认后推进本批需求到`发布完成`并停留,等待验收。只说“成功”但没有生产证据时不改状态。 + +### 发布失败 + +运维输入: + +```text +发布失败:执行ID=PIPE-789;原因=镜像拉取失败 +``` + +Codex执行:核查失败证据,记录原因并进入项目真实`发布失败`状态;项目尚无该状态时记录阻塞并给出人工处置,不创建近义重复状态。 + +### 验收通过 + +产品或业务输入: + +```text +验收通过:ONEOSP-123;验收人=王经理;证据=https://... +``` + +Codex执行:核查生产发布完成和验收证据后,把需求改为`已关闭`。发布成功不能代替本句验收。 + +### 验收不通过 + +产品或业务输入: + +```text +验收不通过:ONEOSP-123;问题=导出字段缺失 +``` + +Codex执行:创建或关联可追踪问题,判断应回到开发、测试还是重新发布,先给出处理建议;不关闭需求,不覆盖原发布记录。 + +## 7. 随时可用的查询口令 + +```text +查状态:ONEOSP-123 +下一步:ONEOSP-123 +为什么没流转:ONEOSP-123 +查重复任务:ONEOSP-123 +查关联代码:ONEOSP-123 +查测试闭环:ONEOSP-123 +给我回滚方案:刚才那条规则 +``` + +这些口令默认只查询或出方案,不修改云效。 + +## 8. Codex的简短回报格式 + +处理成功: + +```text +已处理:ONEOSP-123 +状态:已确认 → 分析中 +任务:已创建并关联分析任务 TASK-456 +下一步:分析人员输入“分析完成:ONEOSP-123;说明=链接” +``` + +条件不足: + +```text +暂未处理:ONEOSP-123 +缺少:开发负责人 +请补充:安排开发:ONEOSP-123;只改后端=负责人/仓库 +``` + +自动化未触发: + +```text +未触发:ONEOSP-123 仍为待开发 +原因:分支只写了需求编号,没有在云效正式关联开发任务 +下一步:我可以补正式关联,然后再核查一次 +``` + +## 9. Codex执行口令的规则 + +1. `记录、确认、开始、完成、提交、修复、复测、准备发布、发布、验收`属于当前项目和明确工作项的执行授权。 +2. `查、看看、为什么、下一步、给方案`只允许读取或规划。 +3. 写操作前必须确认当前项目;项目不清楚只问项目名。 +4. 其他必要字段缺失时只追问最少信息,不发送长表单。 +5. 先查重、再创建;任务幂等键使用项目ID、需求ID和阶段类型。 +6. 项目使用`stage_tasks`还是`oneos_delivery`由Codex识别,同事不需要记规则编号。 +7. 当前平台或桥接不支持的动作要明确说“需要人工一步”,不能伪装成已自动化。 +8. 自动规则执行后等待5–30秒,再回看状态、正式关联和执行日志。 +9. 不用手工改最终状态冒充自动化通过。 +10. 不修改生产流水线定义,不删除规则、分支、MR、任务或测试资产,除非用户单独明确授权。 diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/task-lifecycle.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/task-lifecycle.md new file mode 100644 index 0000000..7432685 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/task-lifecycle.md @@ -0,0 +1,87 @@ +# 阶段任务生命周期与自动创建 + +## 目录 + +1. 目标链路 +2. TK01–TK08规则 +3. 关系与代码资产 +4. 原生规则和桥接边界 +5. 验证与回滚 + +## 1. 目标链路 + +```text +需求设计中 + 设计任务处理中 +→ 设计任务已完成 +→ 需求设计完成 +→ 创建并关联开发任务 +→ 需求待开发 + 开发任务待处理 +→ 正式关联需求分支 +→ 需求开发中 + 开发任务处理中 +→ 全部相关MR合并到真实dev/develop +→ 需求开发完成 + 开发任务已完成 +→ test流水线部署成功 +→ 创建并关联测试任务 +→ 需求测试中 + 测试任务处理中 +→ 用例全部通过且缺陷复测关闭 +→ 需求测试完成 + 测试任务已完成 +``` + +分支只能合并到集成分支,不能“合并到测试环境”。test环境由流水线部署;部署成功用于允许测试开始,不用于证明生产发布。 + +## 2. TK01–TK08规则 + +| ID | 触发 | 必要条件 | 动作 | 推荐实现 | +|---|---|---|---|---| +| TK01 | 需求进入设计中 | 已正式关联设计任务;任务标签=`设计` | 设计任务保持待处理,负责人真实开工后人工改为处理中 | 默认+人工 | +| TK02 | 设计任务变为已完成 | 需求当前状态=设计中;本阶段设计任务非零且全部完成 | 需求改为设计完成 | 原生规则 | +| TK03 | 需求变为设计完成 | 不存在同需求、同阶段、未取消的开发任务 | 创建并正式关联开发任务,标签=`开发`,状态=待处理 | API/Webhook桥接 | +| TK04 | 开发任务创建并关联成功 | 需求当前状态=设计完成;开发任务关系可见 | 需求改为待开发 | 原生规则 | +| TK05 | 正式关联需求分支/代码资产 | 需求=待开发;开发任务=待处理;资产同时关联需求和开发任务 | 需求改为开发中;开发任务改为处理中 | 原生规则 | +| TK06 | 关联合并请求状态变化 | 需求=开发中;开发任务=处理中;全部相关前后端MR已合并到真实集成分支 | 需求改为开发完成;开发任务改为已完成 | 原生规则 | +| TK07 | test流水线部署成功 | 需求=开发完成;执行ID未处理;不存在同需求、同阶段、未取消的测试任务 | 创建并关联测试任务,关联测试计划/用例,任务改为处理中,需求改为测试中 | API/Webhook桥接 | +| TK08 | 测试验收完成 | 用例全部通过;失败用例已复测;范围内缺陷均由测试关闭 | 测试任务改为已完成;需求改为测试完成 | 桥接/人工门禁 | + +## 3. 关系与代码资产 + +- 设计、开发、测试任务必须使用云效正式父子项或关联项关系,不依赖标题关键字。 +- 开发分支和MR必须同时关联产品需求与对应开发任务;只关联需求不能可靠更新开发任务。 +- 一个需求涉及多个仓库时,全部相关前端、后端MR都纳入TK06门禁。 +- 首次提交可能晚于真实开工。任务`处理中`的权威时间是负责人执行“开始处理”;分支关联仅作自动兜底。 + +## 4. 原生规则和桥接边界 + +优先使用云效原生规则处理状态变化、父子项、关联项和Codeup对象。若当前规则编辑器没有“创建工作项”动作,TK03和TK07必须标记为`integration_bridge`或`not_supported`,不得声称已经自动创建。 + +桥接创建任务时使用幂等键: + +```text +项目ID + 需求ID + 阶段类型 +``` + +重复回调先查询是否存在未取消的同阶段任务;存在则复用并校验正式关系,不重复创建。流水线回调还要校验签名、时间戳、执行ID、防重放和失败重试。 + +## 5. 验证与回滚 + +- 正向验证每个TK控制,并检查需求和任务两个对象的状态。 +- 负向验证零任务、错误标签、错误当前状态、未正式关联分支、只合并部分MR、流水线失败和重复回调。 +- 新规则不会补触发历史事件。历史任务仅在核对MR/流水线证据后人工修正,并标记为测试准备或数据修复。 +- 回滚时先禁用新增规则或桥接入口,不删除已有任务和关系;恢复前一状态需要单独业务确认。 + +## 6. 进阶段自动建同名任务(与 OneOS / 口令共用) + +完整规则见 [auto-stage-task.md](auto-stage-task.md)。摘要: + +| 需求状态 | 同名任务标签 | 负责人 | +|---|---|---| +| 分析中 | `分析` | 需求创建人 | +| 设计中 | `设计` | 需求创建人 | +| 待开发 | `交付` 或 `开发` | 何斐 | + +必须正式关联需求;计划开始/创建时间沿用需求;同阶段幂等查重。 + +当创建或修复**分析 / 设计 / 交付(开发)**任务时: + +1. **任务标签(右侧基础字段)**:分析 → `分析`;设计 → `设计`;待开发主任务 → `交付`(或 `开发`)。Title 同名或前缀 alone 不够。 +2. **正式关联需求**:每条阶段任务必须链接到**产品需求**(父子或关联项)。只关联其他任务不算完成。 +3. 创建/修复后先核对标签与可见需求关系,再报告成功。 \ No newline at end of file diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/references/test-defect-closure.md b/.cursor/skills/yunxiao-requirement-lifecycle/references/test-defect-closure.md new file mode 100644 index 0000000..dcf7b3c --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/references/test-defect-closure.md @@ -0,0 +1,62 @@ +# 测试用例与缺陷复测闭环 + +## 目录 + +1. 测试完成门槛 +2. TC01–TC03用例控制 +3. DF01–DF03缺陷控制 +4. 实现方式 +5. 验证场景 + +## 1. 测试完成门槛 + +R10只有同时满足以下条件才能完成: + +1. 范围内用例均已执行并有明确结果。 +2. 不存在`未执行/待测试`、`阻塞`或`未通过`用例;获批暂缓/不适用项记录批准人与原因。 +3. 每个未通过用例均正式创建或关联缺陷。 +4. 修复后重新执行原用例并通过。 +5. 范围内缺陷均由测试复测并进入项目真实关闭态。 +6. 测试计划或测试验收任务由测试负责人确认。 + +流水线成功只能证明部署/自动检查成功,不能替代用例结果和缺陷复测。 + +## 2. TC01–TC03用例控制 + +| ID | 控制 | 完成口径 | +|---|---|---| +| TC01 | 用例执行 | 每条范围内用例有真实执行结果;未执行、待测试、阻塞、未通过均阻塞测试完成 | +| TC02 | 失败用例关联缺陷 | 每条未通过用例正式创建或关联缺陷;仅写编号或链接不算关联 | +| TC03 | 用例复测更新 | 修复后重新执行原用例;通过才改为已通过,失败保持未通过 | + +## 3. DF01–DF03缺陷控制 + +| ID | 控制 | 完成口径 | +|---|---|---| +| DF01 | 已修复交接 | `已修复`只表示开发交给测试复测,不是终态,不解除测试门禁 | +| DF02 | 复测通过 | 缺陷进入项目真实关闭态,原失败用例同时更新为已通过 | +| DF03 | 复测失败 | 缺陷进入项目真实重开/处理中状态,原用例保持未通过 | + +不得关闭缺陷却不更新原用例,也不得在缺陷仅为`已修复`时把用例标为通过。 + +## 4. 实现方式 + +先检查当前规则编辑器是否真的暴露Testhub执行用例和缺陷聚合条件: + +- 支持时:使用原生聚合规则,并保存条件和执行日志证据。 +- 不支持时:使用测试验收任务/人工门禁。 +- 需要全自动时:单独评审Testhub OpenAPI/Webhook桥接,要求签名、幂等、失败重试和审计记录。 + +不能把`not_supported`写成“已自动化”;必须指定负责人和回退方案。 + +## 5. 验证场景 + +| 场景 | 操作 | 期望 | 负向检查 | +|---|---|---|---| +| 用例未执行 | 保留一条未执行用例 | 保持测试中 | 不得完成测试 | +| 用例失败 | 记录未通过并正式关联缺陷 | 保持测试中 | 仅建缺陷不得完成 | +| 缺陷已修复 | 开发改为已修复 | 等待测试复测 | 不得关闭或通过用例 | +| 复测通过 | 重跑原用例并通过 | 用例通过、缺陷关闭 | 两者状态必须一致 | +| 复测失败 | 重跑仍失败 | 用例未通过、缺陷重开 | 不得保持已修复/关闭 | +| 全部闭环 | 全部用例通过且缺陷关闭 | 允许R10推进 | 检查是否误触发后继规则 | + diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/scripts/oneos_profile_tool.py b/.cursor/skills/yunxiao-requirement-lifecycle/scripts/oneos_profile_tool.py new file mode 100644 index 0000000..c1849b6 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/scripts/oneos_profile_tool.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Validate and render a OneOS delivery-model Yunxiao project profile.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + + +TARGET_LIFECYCLE = [ + "待处理", "已确认", "分析中", "分析完成", "设计中", "设计完成", + "待开发", "开发中", "开发完成", "待测试", "测试中", "测试完成", + "发布中", "发布完成", "发布失败", "已关闭", +] +REQUIRED_TASK_LABELS = {"交付", "开发", "测试", "发版"} +REQUIRED_CONTROL_IDS = { + *(f"Y{index:02d}" for index in range(1, 17)), + "Y20", "Y21", "Y22", + *(f"Y{index:02d}" for index in range(30, 36)), + "Y40", + "A01", "A02", "A02B", *(f"A{index:02d}" for index in range(3, 11)), + *(f"TC{index:02d}" for index in range(1, 4)), + *(f"DF{index:02d}" for index in range(1, 4)), + *(f"CI{index:02d}" for index in range(1, 4)), + "L01", "L02", +} +VALID_MODES = { + "manual", "native_rule", "integration_bridge", "disabled_legacy", "not_supported" +} +VALID_PHASES = {"P0", "P1", "P2", "P3"} +VALID_TASK_WORKFLOW_MODES = {"A", "B"} +VALID_TASK_IDENTITY_MODES = {"work_item_type", "task_label", "title_prefix_process_control"} +VALID_LEGACY_DISPOSITIONS = {"keep_until_ready", "disable", "replace", "already_disabled"} +SENSITIVE_FRAGMENTS = ( + "password", "passwd", "secret", "token", "cookie", "credential", + "private_key", "otp", "密码", "密钥", +) + + +def is_text(value: Any) -> bool: + return isinstance(value, str) and bool(value.strip()) + + +def require_text(container: dict[str, Any], key: str, label: str, errors: list[str]) -> None: + if not is_text(container.get(key)): + errors.append(f"{label}.{key} 不能为空") + + +def walk_sensitive_keys(value: Any, prefix: str = "") -> list[str]: + found: list[str] = [] + if isinstance(value, dict): + for key, nested in value.items(): + current = f"{prefix}.{key}" if prefix else str(key) + if any(fragment in str(key).lower() for fragment in SENSITIVE_FRAGMENTS): + found.append(current) + found.extend(walk_sensitive_keys(nested, current)) + elif isinstance(value, list): + for index, nested in enumerate(value): + found.extend(walk_sensitive_keys(nested, f"{prefix}[{index}]")) + return found + + +def validate_controls(controls: Any, label: str, errors: list[str]) -> None: + if not isinstance(controls, list): + errors.append(f"{label} 必须是数组") + return + seen: set[str] = set() + for index, control in enumerate(controls): + item_label = f"{label}[{index}]" + if not isinstance(control, dict): + errors.append(f"{item_label} 必须是对象") + continue + control_id = control.get("id") + if not is_text(control_id): + errors.append(f"{item_label}.id 不能为空") + continue + if control_id in seen: + errors.append(f"{label} 控制项重复:{control_id}") + seen.add(control_id) + if control_id not in REQUIRED_CONTROL_IDS: + errors.append(f"{label} 未知控制项:{control_id}") + mode = control.get("mode") + if mode not in VALID_MODES: + errors.append(f"{item_label}.mode 不合法") + if not isinstance(control.get("enabled"), bool): + errors.append(f"{item_label}.enabled 必须是布尔值") + if mode in {"disabled_legacy", "not_supported"} and control.get("enabled") is not False: + errors.append(f"{item_label} 的 {mode} 模式要求 enabled=false") + if control_id in {"L01", "L02"}: + if mode != "disabled_legacy" or control.get("enabled") is not False: + errors.append(f"{item_label} 必须停用,防止发布完成绕过验收") + for key in ("owner", "fallback", "evidence"): + require_text(control, key, item_label, errors) + missing = sorted(REQUIRED_CONTROL_IDS - seen) + if missing: + errors.append(f"{label} 缺少控制项:" + "、".join(missing)) + + +def validate_project(project: Any, index: int, errors: list[str]) -> None: + label = f"projects[{index}]" + if not isinstance(project, dict): + errors.append(f"{label} 必须是对象") + return + require_text(project, "name", label, errors) + if project.get("migration_phase") not in VALID_PHASES: + errors.append(f"{label}.migration_phase 必须是 P0/P1/P2/P3") + statuses = project.get("requirement_statuses_observed") + if not isinstance(statuses, list) or not statuses or not all(is_text(v) for v in statuses): + errors.append(f"{label}.requirement_statuses_observed 必须是非空文本数组") + if project.get("task_workflow_mode") not in VALID_TASK_WORKFLOW_MODES: + errors.append(f"{label}.task_workflow_mode 必须是 A 或 B") + if project.get("task_identity_mode") not in VALID_TASK_IDENTITY_MODES: + errors.append(f"{label}.task_identity_mode 不合法") + if not isinstance(project.get("native_rule_can_filter_related_task_identity"), bool): + errors.append(f"{label}.native_rule_can_filter_related_task_identity 必须是布尔值") + for key in ( + "main_task_prefix", "development_task_prefix", "test_task_prefix", "release_task_prefix" + ): + require_text(project, key, label, errors) + + repositories = project.get("repositories") + if not isinstance(repositories, list) or not repositories: + errors.append(f"{label}.repositories 至少包含一个仓库") + else: + for repo_index, repo in enumerate(repositories): + repo_label = f"{label}.repositories[{repo_index}]" + if not isinstance(repo, dict): + errors.append(f"{repo_label} 必须是对象") + continue + for key in ("name", "role", "integration_branch", "evidence"): + require_text(repo, key, repo_label, errors) + if repo.get("integration_branch") not in {"dev", "develop"}: + errors.append(f"{repo_label}.integration_branch 只能是实际存在的 dev/develop") + for key in ("codeup_integrated", "required_for_mr_gate"): + if not isinstance(repo.get(key), bool): + errors.append(f"{repo_label}.{key} 必须是布尔值") + + test_plan = project.get("test_plan") + if not isinstance(test_plan, dict): + errors.append(f"{label}.test_plan 必须是对象") + else: + require_text(test_plan, "name", f"{label}.test_plan", errors) + require_text(test_plan, "evidence", f"{label}.test_plan", errors) + for key, required in ( + ("iteration_formally_related", True), + ("cases_partitioned_by_requirement", True), + ("fixed_defect_is_terminal", False), + ("tester_retest_required", True), + ): + if test_plan.get(key) is not required: + errors.append(f"{label}.test_plan.{key} 必须为 {str(required).lower()}") + + release = project.get("release") + if not isinstance(release, dict): + errors.append(f"{label}.release 必须是对象") + else: + require_text(release, "evidence", f"{label}.release", errors) + for key in ( + "release_task_formally_related_to_iteration", + "production_evidence_separate_from_test", + "acceptance_required_after_release", + "production_pipeline_unchanged", + ): + if release.get(key) is not True: + errors.append(f"{label}.release.{key} 必须为 true") + + legacy_rules = project.get("legacy_rules") + if not isinstance(legacy_rules, list): + errors.append(f"{label}.legacy_rules 必须是数组") + else: + for legacy_index, rule in enumerate(legacy_rules): + item_label = f"{label}.legacy_rules[{legacy_index}]" + if not isinstance(rule, dict): + errors.append(f"{item_label} 必须是对象") + continue + for key in ("name", "reason", "evidence"): + require_text(rule, key, item_label, errors) + if rule.get("disposition") not in VALID_LEGACY_DISPOSITIONS: + errors.append(f"{item_label}.disposition 不合法") + if not isinstance(rule.get("enabled"), bool): + errors.append(f"{item_label}.enabled 必须是布尔值") + + validate_controls(project.get("controls"), f"{label}.controls", errors) + + +def validate(data: dict[str, Any]) -> list[str]: + errors: list[str] = [] + if data.get("schema_version") != 1: + errors.append("schema_version 必须为 1") + if data.get("flow_model") != "oneos_delivery": + errors.append("flow_model 必须为 oneos_delivery") + for key in ("organization", "work_item_type"): + require_text(data, key, "profile", errors) + if data.get("target_lifecycle") != TARGET_LIFECYCLE: + errors.append("target_lifecycle 必须与OneOS终态16状态顺序一致") + labels = data.get("task_labels") + if not isinstance(labels, list) or not REQUIRED_TASK_LABELS.issubset(set(labels)): + errors.append("task_labels 必须包含:交付、开发、测试、发版") + policies = data.get("policies") + if not isinstance(policies, dict): + errors.append("policies 必须是对象") + else: + true_keys = ( + "require_formal_relations", "require_current_state_condition", + "require_nonzero_development_tasks", + "require_signed_timestamped_replay_protected_callbacks", + ) + false_keys = ("allow_automatic_final_close_without_acceptance", "test_success_is_production_release") + for key in true_keys: + if policies.get(key) is not True: + errors.append(f"policies.{key} 必须为 true") + for key in false_keys: + if policies.get(key) is not False: + errors.append(f"policies.{key} 必须为 false") + parts = policies.get("bridge_idempotency_key_components") + if not isinstance(parts, list) or not parts or not all(is_text(v) for v in parts): + errors.append("policies.bridge_idempotency_key_components 不能为空") + projects = data.get("projects") + if not isinstance(projects, list) or not projects: + errors.append("projects 至少包含一个项目") + else: + for index, project in enumerate(projects): + validate_project(project, index, errors) + sensitive = walk_sensitive_keys(data) + if sensitive: + errors.append("配置中禁止出现敏感字段:" + "、".join(sensitive)) + if "请填写" in json.dumps(data, ensure_ascii=False): + errors.append("仍存在“请填写”占位符") + return errors + + +def render(data: dict[str, Any]) -> str: + lines = [ + "# OneOS云效终态交付模型执行计划", "", + f"- 组织:{data['organization']}", + f"- 工作项类型:{data['work_item_type']}", + f"- 项目数:{len(data['projects'])}", + "- 完整性口径:每项目48项Y/A/测试/缺陷/集成/旧规则控制。", + "- 安全边界:不修改生产流水线;发布完成后保留产品验收门禁。", "", + ] + for project in data["projects"]: + missing_statuses = [s for s in TARGET_LIFECYCLE if s not in project["requirement_statuses_observed"]] + lines.extend([ + f"## 项目:{project['name']}", "", + f"- 迁移阶段:{project['migration_phase']}", + f"- 任务工作流方案:{project['task_workflow_mode']}", + f"- 任务身份方式:{project['task_identity_mode']}", + f"- 原生规则可过滤关联任务身份:{'是' if project['native_rule_can_filter_related_task_identity'] else '否'}", + f"- 缺少目标状态:{'、'.join(missing_statuses) if missing_statuses else '无'}", "", + "### 旧规则处置", "", + "| 规则 | 处置 | 启用 | 原因 | 证据 |", "|---|---|---|---|---|", + ]) + for rule in project["legacy_rules"]: + lines.append( + f"| {rule['name']} | {rule['disposition']} | {'是' if rule['enabled'] else '否'} | " + f"{rule['reason']} | {rule['evidence']} |" + ) + lines.extend([ + "", "### 48项控制台账", "", + "| ID | 方式 | 启用 | 负责人 | 回退方案 | 证据 |", "|---|---|---|---|---|---|", + ]) + for control in sorted(project["controls"], key=lambda item: item["id"]): + lines.append( + f"| {control['id']} | {control['mode']} | {'是' if control['enabled'] else '否'} | " + f"{control['owner']} | {control['fallback']} | {control['evidence']} |" + ) + lines.extend([ + "", "### 迁移门禁", "", + "- [ ] 目标状态与流转边已补齐。", + "- [ ] 主任务可可靠识别或已明确交桥接。", + "- [ ] 多仓库需求与开发任务均正式关联分支/MR。", + "- [ ] Testhub按需求分包且缺陷复测闭环。", + "- [ ] test、生产发布和产品验收证据分离。", + "- [ ] L01、L02已停用,旧规则已有备份和回滚。", "", + ]) + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + validate_parser = subparsers.add_parser("validate") + validate_parser.add_argument("profile", type=Path) + render_parser = subparsers.add_parser("render") + render_parser.add_argument("profile", type=Path) + render_parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + try: + with args.profile.open("r", encoding="utf-8-sig") as handle: + data = json.load(handle) + if not isinstance(data, dict): + raise ValueError("配置根节点必须是JSON对象") + errors = validate(data) + except (OSError, json.JSONDecodeError, ValueError) as error: + print(f"配置读取失败:{error}", file=sys.stderr) + return 2 + if errors: + for error in errors: + print(f"[错误] {error}", file=sys.stderr) + return 1 + if args.command == "validate": + print(f"[通过] OneOS配置有效:{len(data['projects'])}个项目,每项目48项控制。") + return 0 + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(render(data), encoding="utf-8") + print(f"[完成] 已生成执行计划:{args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.cursor/skills/yunxiao-requirement-lifecycle/scripts/profile_tool.py b/.cursor/skills/yunxiao-requirement-lifecycle/scripts/profile_tool.py new file mode 100644 index 0000000..5738bf3 --- /dev/null +++ b/.cursor/skills/yunxiao-requirement-lifecycle/scripts/profile_tool.py @@ -0,0 +1,536 @@ +#!/usr/bin/env python3 +"""Validate a project-scoped Yunxiao lifecycle profile and render an execution plan.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + + +REQUIRED_LABELS = {"分析", "设计", "开发", "测试"} +REQUIRED_RULE_IDS = {f"R{index:02d}" for index in range(1, 13)} +REQUIRED_TASK_RULE_IDS = {f"TK{index:02d}" for index in range(1, 9)} +REQUIRED_CONTROL_IDS = { + *REQUIRED_RULE_IDS, + *REQUIRED_TASK_RULE_IDS, + *(f"TC{index:02d}" for index in range(1, 4)), + *(f"DF{index:02d}" for index in range(1, 4)), + *(f"CI{index:02d}" for index in range(1, 4)), + "L01", + "L02", +} +VALID_MODES = { + "manual", + "native_rule", + "integration_bridge", + "disabled_legacy", + "not_supported", +} +VALID_RELEASE_EVIDENCE_MODES = { + "native_release_change", + "pipeline_callback", + "manual_release_confirmation", +} +SENSITIVE_FRAGMENTS = ( + "password", + "passwd", + "secret", + "token", + "cookie", + "credential", + "private_key", + "otp", + "密码", + "密钥", +) + + +def load_profile(path: Path) -> dict[str, Any]: + with path.open("r", encoding="utf-8-sig") as handle: + data = json.load(handle) + if not isinstance(data, dict): + raise ValueError("配置根节点必须是 JSON 对象") + return data + + +def is_text(value: Any) -> bool: + return isinstance(value, str) and bool(value.strip()) + + +def require_text(container: dict[str, Any], key: str, label: str, errors: list[str]) -> None: + if not is_text(container.get(key)): + errors.append(f"{label}.{key} 不能为空") + + +def walk_sensitive_keys(value: Any, prefix: str = "") -> list[str]: + found: list[str] = [] + if isinstance(value, dict): + for key, nested in value.items(): + current = f"{prefix}.{key}" if prefix else str(key) + lower = str(key).lower() + if any(fragment in lower for fragment in SENSITIVE_FRAGMENTS): + found.append(current) + found.extend(walk_sensitive_keys(nested, current)) + elif isinstance(value, list): + for index, nested in enumerate(value): + found.extend(walk_sensitive_keys(nested, f"{prefix}[{index}]")) + return found + + +def validate_control_list( + controls: Any, label: str, errors: list[str] +) -> None: + if not isinstance(controls, list): + errors.append(f"{label} 必须是数组") + return + seen: set[str] = set() + for index, control in enumerate(controls): + item_label = f"{label}[{index}]" + if not isinstance(control, dict): + errors.append(f"{item_label} 必须是对象") + continue + control_id = control.get("id") + if not is_text(control_id): + errors.append(f"{item_label}.id 不能为空") + continue + if control_id in seen: + errors.append(f"{label} 控制项重复:{control_id}") + seen.add(control_id) + if control_id not in REQUIRED_CONTROL_IDS: + errors.append(f"{label} 未知控制项:{control_id}") + mode = control.get("mode") + if mode not in VALID_MODES: + errors.append(f"{item_label}.mode 不合法") + if not isinstance(control.get("enabled"), bool): + errors.append(f"{item_label}.enabled 必须是布尔值") + for key in ("evidence", "owner", "fallback"): + require_text(control, key, item_label, errors) + if mode in {"disabled_legacy", "not_supported"} and control.get("enabled") is not False: + errors.append(f"{item_label} 的 {mode} 模式要求 enabled=false") + if control_id in {"L01", "L02"} and mode not in { + "disabled_legacy", + "native_rule", + "manual", + }: + errors.append(f"{item_label} 必须明确为停用旧规则、获批原生规则或人工处置") + missing = sorted(REQUIRED_CONTROL_IDS - seen) + if missing: + errors.append(f"{label} 缺少控制项:" + "、".join(missing)) + + +def validate_rule_instances( + rules: Any, lifecycle: list[str], label: str, errors: list[str] +) -> None: + if not isinstance(rules, list): + errors.append(f"{label} 必须是数组") + return + seen: set[str] = set() + orders: set[int] = set() + expected = { + f"R{index + 1:02d}": (lifecycle[index], lifecycle[index + 1]) + for index in range(12) + } + for index, rule in enumerate(rules): + item_label = f"{label}[{index}]" + if not isinstance(rule, dict): + errors.append(f"{item_label} 必须是对象") + continue + rule_id = rule.get("id") + if not is_text(rule_id): + errors.append(f"{item_label}.id 不能为空") + continue + if rule_id in seen: + errors.append(f"{label} 规则实例重复:{rule_id}") + seen.add(rule_id) + if rule_id not in REQUIRED_RULE_IDS: + errors.append(f"{label} 未知规则实例:{rule_id}") + mode = rule.get("mode") + if mode not in {"manual", "native_rule", "integration_bridge", "not_supported"}: + errors.append(f"{item_label}.mode 不合法") + for key in ("actual_rule_name", "trigger", "source_status", "target_status", "action", "evidence"): + require_text(rule, key, item_label, errors) + conditions = rule.get("conditions") + if not isinstance(conditions, list) or not conditions or not all(is_text(v) for v in conditions): + errors.append(f"{item_label}.conditions 必须是非空文本数组") + order = rule.get("order") + if not isinstance(order, int) or order < 1: + errors.append(f"{item_label}.order 必须是正整数") + elif order in orders: + errors.append(f"{label} 规则顺序重复:{order}") + else: + orders.add(order) + if not isinstance(rule.get("enabled"), bool): + errors.append(f"{item_label}.enabled 必须是布尔值") + if rule.get("has_current_state_condition") is not True: + errors.append(f"{item_label}.has_current_state_condition 必须为 true") + if rule_id in expected: + expected_source, expected_target = expected[rule_id] + if rule.get("source_status") != expected_source: + errors.append(f"{item_label}.source_status 应为 {expected_source}") + if rule.get("target_status") != expected_target: + errors.append(f"{item_label}.target_status 应为 {expected_target}") + missing = sorted(REQUIRED_RULE_IDS - seen) + if missing: + errors.append(f"{label} 缺少规则实例:" + "、".join(missing)) + + +def validate_task_rule_instances(rules: Any, label: str, errors: list[str]) -> None: + if not isinstance(rules, list): + errors.append(f"{label} 必须是数组") + return + seen: set[str] = set() + orders: set[int] = set() + for index, rule in enumerate(rules): + item_label = f"{label}[{index}]" + if not isinstance(rule, dict): + errors.append(f"{item_label} 必须是对象") + continue + rule_id = rule.get("id") + if not is_text(rule_id): + errors.append(f"{item_label}.id 不能为空") + continue + if rule_id in seen: + errors.append(f"{label} 规则实例重复:{rule_id}") + seen.add(rule_id) + if rule_id not in REQUIRED_TASK_RULE_IDS: + errors.append(f"{label} 未知任务规则实例:{rule_id}") + if rule.get("mode") not in {"manual", "native_rule", "integration_bridge", "not_supported"}: + errors.append(f"{item_label}.mode 不合法") + for key in ("actual_rule_name", "trigger", "scope", "source_state", "evidence"): + require_text(rule, key, item_label, errors) + for key in ("conditions", "actions"): + values = rule.get(key) + if not isinstance(values, list) or not values or not all(is_text(value) for value in values): + errors.append(f"{item_label}.{key} 必须是非空文本数组") + order = rule.get("order") + if not isinstance(order, int) or order < 1: + errors.append(f"{item_label}.order 必须是正整数") + elif order in orders: + errors.append(f"{label} 规则顺序重复:{order}") + else: + orders.add(order) + if not isinstance(rule.get("enabled"), bool): + errors.append(f"{item_label}.enabled 必须是布尔值") + missing = sorted(REQUIRED_TASK_RULE_IDS - seen) + if missing: + errors.append(f"{label} 缺少任务规则实例:" + "、".join(missing)) + + +def validate_project( + project: Any, index: int, lifecycle: list[str], errors: list[str] +) -> None: + label = f"projects[{index}]" + if not isinstance(project, dict): + errors.append(f"{label} 必须是对象") + return + for key in ("name", "work_item_type", "release_status", "final_status"): + require_text(project, key, label, errors) + if project.get("release_status") != lifecycle[11]: + errors.append(f"{label}.release_status 必须与 lifecycle[11] 一致") + if project.get("final_status") != lifecycle[12]: + errors.append(f"{label}.final_status 必须与 lifecycle[12] 一致") + + labels = project.get("labels_observed") + if not isinstance(labels, list) or not REQUIRED_LABELS.issubset(set(labels)): + errors.append(f"{label}.labels_observed 必须包含:分析、设计、开发、测试") + + repositories = project.get("repositories") + if not isinstance(repositories, list) or not repositories: + errors.append(f"{label}.repositories 至少包含一个仓库") + else: + for repo_index, repo in enumerate(repositories): + repo_label = f"{label}.repositories[{repo_index}]" + if not isinstance(repo, dict): + errors.append(f"{repo_label} 必须是对象") + continue + for key in ("name", "role", "integration_branch", "evidence"): + require_text(repo, key, repo_label, errors) + if repo.get("integration_branch") not in {"dev", "develop"}: + errors.append(f"{repo_label}.integration_branch 只能是实际存在的 dev/develop") + for key in ("codeup_integrated", "required_for_mr_gate"): + if not isinstance(repo.get(key), bool): + errors.append(f"{repo_label}.{key} 必须是布尔值") + + pipeline = project.get("test_pipeline") + if not isinstance(pipeline, dict): + errors.append(f"{label}.test_pipeline 必须是对象") + else: + for key in ("name", "environment", "evidence"): + require_text(pipeline, key, f"{label}.test_pipeline", errors) + for key in ( + "is_clone_for_callback_poc", + "original_pipeline_unchanged", + "docker_success_callback_enabled", + ): + if not isinstance(pipeline.get(key), bool): + errors.append(f"{label}.test_pipeline.{key} 必须是布尔值") + security = pipeline.get("callback_security") + if not isinstance(security, dict): + errors.append(f"{label}.test_pipeline.callback_security 必须是对象") + else: + security_keys = ( + "signed", + "timestamp_checked", + "replay_protected", + "idempotent_by_execution_id", + "failure_retry_tested", + ) + for key in security_keys: + if not isinstance(security.get(key), bool): + errors.append(f"{label}.test_pipeline.callback_security.{key} 必须是布尔值") + if pipeline.get("docker_success_callback_enabled") is True: + if pipeline.get("is_clone_for_callback_poc") is not True: + errors.append(f"{label} 启用回调时必须使用test流水线副本") + if pipeline.get("original_pipeline_unchanged") is not True: + errors.append(f"{label} 启用回调时必须保持原流水线不变") + if any(security.get(key) is not True for key in security_keys): + errors.append(f"{label} 启用回调时必须完成全部安全与重试验证") + + release = project.get("release_evidence") + if not isinstance(release, dict): + errors.append(f"{label}.release_evidence 必须是对象") + else: + if release.get("mode") not in VALID_RELEASE_EVIDENCE_MODES: + errors.append(f"{label}.release_evidence.mode 不合法") + for key in ("target_environment", "evidence"): + require_text(release, key, f"{label}.release_evidence", errors) + if release.get("test_success_is_production_release") is not False: + errors.append(f"{label}.release_evidence.test_success_is_production_release 必须为 false") + + test_plan = project.get("test_plan") + if not isinstance(test_plan, dict): + errors.append(f"{label}.test_plan 必须是对象") + else: + for key in ("name", "evidence"): + require_text(test_plan, key, f"{label}.test_plan", errors) + if not isinstance(test_plan.get("requirement_formally_related"), bool): + errors.append(f"{label}.test_plan.requirement_formally_related 必须是布尔值") + states = test_plan.get("case_states_observed") + if not isinstance(states, list) or not states or not all(is_text(v) for v in states): + errors.append(f"{label}.test_plan.case_states_observed 必须是非空文本数组") + + defect = project.get("defect_workflow") + if not isinstance(defect, dict): + errors.append(f"{label}.defect_workflow 必须是对象") + else: + for key in ("fixed_status", "closed_status", "reopen_status", "evidence"): + require_text(defect, key, f"{label}.defect_workflow", errors) + if defect.get("fixed_is_terminal") is not False: + errors.append(f"{label}.defect_workflow.fixed_is_terminal 必须为 false") + if defect.get("tester_retest_required") is not True: + errors.append(f"{label}.defect_workflow.tester_retest_required 必须为 true") + + validate_rule_instances(project.get("rule_instances"), lifecycle, f"{label}.rule_instances", errors) + validate_task_rule_instances( + project.get("task_rule_instances"), f"{label}.task_rule_instances", errors + ) + validate_control_list(project.get("control_coverage"), f"{label}.control_coverage", errors) + if not isinstance(project.get("adjunct_automations"), list): + errors.append(f"{label}.adjunct_automations 必须是数组,可为空") + + +def validate(data: dict[str, Any]) -> list[str]: + errors: list[str] = [] + if data.get("schema_version") != 3: + errors.append("schema_version 必须为 3;旧版台账不包含阶段任务闭环") + for key in ("organization", "work_item_type"): + require_text(data, key, "profile", errors) + + lifecycle = data.get("lifecycle") + if not isinstance(lifecycle, list) or len(lifecycle) != 13 or not all(is_text(v) for v in lifecycle): + errors.append("lifecycle 必须包含 13 个非空有序状态") + lifecycle = [str(index) for index in range(13)] + elif len(set(lifecycle)) != len(lifecycle): + errors.append("lifecycle 状态名称不能重复") + + labels = data.get("requirement_labels") + if not isinstance(labels, list) or not REQUIRED_LABELS.issubset(set(labels)): + errors.append("requirement_labels 必须包含:分析、设计、开发、测试") + + policy = data.get("automation_policy") + if not isinstance(policy, dict): + errors.append("automation_policy 必须是对象") + else: + for key in ("require_current_state_condition", "inspect_rule_order_and_cascades"): + if policy.get(key) is not True: + errors.append(f"automation_policy.{key} 必须为 true") + for key in ("allow_automatic_final_close_without_acceptance", "zero_related_items_are_complete"): + if policy.get(key) is not False: + errors.append(f"automation_policy.{key} 必须为 false") + events = policy.get("development_start_event_candidates") + if not isinstance(events, list) or not events or not all(is_text(v) for v in events): + errors.append("automation_policy.development_start_event_candidates 不能为空") + if policy.get("require_code_assets_linked_to_requirement_and_development_task") is not True: + errors.append( + "automation_policy.require_code_assets_linked_to_requirement_and_development_task 必须为 true" + ) + idempotency = policy.get("stage_task_creation_idempotency_key_components") + if not isinstance(idempotency, list) or not idempotency or not all(is_text(v) for v in idempotency): + errors.append("automation_policy.stage_task_creation_idempotency_key_components 不能为空") + + test_policy = data.get("test_closure_policy") + if not isinstance(test_policy, dict): + errors.append("test_closure_policy 必须是对象") + else: + if test_policy.get("defect_fixed_is_terminal") is not False: + errors.append("test_closure_policy.defect_fixed_is_terminal 必须为 false") + if test_policy.get("require_tester_retest") is not True: + errors.append("test_closure_policy.require_tester_retest 必须为 true") + for key in ("accepted_case_states", "rejected_case_states"): + values = test_policy.get(key) + if not isinstance(values, list) or not values or not all(is_text(v) for v in values): + errors.append(f"test_closure_policy.{key} 不能为空") + + projects = data.get("projects") + if not isinstance(projects, list) or not projects: + errors.append("projects 至少包含一个项目") + else: + names: set[str] = set() + for index, project in enumerate(projects): + validate_project(project, index, lifecycle, errors) + if isinstance(project, dict) and is_text(project.get("name")): + name = project["name"] + if name in names: + errors.append(f"项目名称重复:{name}") + names.add(name) + + sensitive = walk_sensitive_keys(data) + if sensitive: + errors.append("配置中禁止出现敏感字段:" + "、".join(sensitive)) + if "请填写" in json.dumps(data, ensure_ascii=False): + errors.append("仍存在“请填写”占位符") + return errors + + +def render(data: dict[str, Any]) -> str: + lines = [ + "# 云效需求生命周期自动化执行计划", + "", + f"- 组织:{data['organization']}", + f"- 工作项类型:{data['work_item_type']}", + f"- 项目数:{len(data['projects'])}", + "- 完整性口径:每项目12个需求规则实例、8个任务规则实例、31个闭环控制项。", + "- 安全边界:不修改现有生产流水线;不记录任何凭据或密钥。", + "", + ] + for project in data["projects"]: + lines.extend([ + f"## 项目:{project['name']}", + "", + f"- 发布状态:{project['release_status']}", + f"- 最终状态:{project['final_status']}", + f"- test流水线:{project['test_pipeline']['name']}", + f"- 发布证据方式:{project['release_evidence']['mode']}", + "", + "### 仓库覆盖", + "", + "| 仓库 | 角色 | 集成分支 | Codeup已集成 | 纳入全部MR门禁 | 证据 |", + "|---|---|---|---|---|---|", + ]) + for repo in project["repositories"]: + lines.append( + f"| {repo['name']} | {repo['role']} | {repo['integration_branch']} | " + f"{'是' if repo['codeup_integrated'] else '否'} | " + f"{'是' if repo['required_for_mr_gate'] else '否'} | {repo['evidence']} |" + ) + lines.extend([ + "", + "### R01–R12实际规则实例", + "", + "| ID | 实现 | 实际规则/门禁 | 触发 | 流转 | 条件 | 顺序 | 启用 | 证据 |", + "|---|---|---|---|---|---|---:|---|---|", + ]) + for rule in sorted(project["rule_instances"], key=lambda item: item["order"]): + conditions = ";".join(rule["conditions"]) + lines.append( + f"| {rule['id']} | {rule['mode']} | {rule['actual_rule_name']} | {rule['trigger']} | " + f"{rule['source_status']} → {rule['target_status']} | {conditions} | {rule['order']} | " + f"{'是' if rule['enabled'] else '否'} | {rule['evidence']} |" + ) + lines.extend([ + "", + "### TK01–TK08任务规则实例", + "", + "| ID | 实现 | 实际规则/门禁 | 触发 | 作用域 | 源状态 | 条件 | 动作 | 顺序 | 启用 | 证据 |", + "|---|---|---|---|---|---|---|---|---:|---|---|", + ]) + for rule in sorted(project["task_rule_instances"], key=lambda item: item["order"]): + conditions = ";".join(rule["conditions"]) + actions = ";".join(rule["actions"]) + lines.append( + f"| {rule['id']} | {rule['mode']} | {rule['actual_rule_name']} | {rule['trigger']} | " + f"{rule['scope']} | {rule['source_state']} | {conditions} | {actions} | {rule['order']} | " + f"{'是' if rule['enabled'] else '否'} | {rule['evidence']} |" + ) + lines.extend([ + "", + "### 31项完整性台账", + "", + "| 控制ID | 实现方式 | 启用 | 观察证据 | 负责人 | 回退方案 |", + "|---|---|---|---|---|---|", + ]) + for control in sorted(project["control_coverage"], key=lambda item: item["id"]): + lines.append( + f"| {control['id']} | {control['mode']} | {'是' if control['enabled'] else '否'} | " + f"{control['evidence']} | {control['owner']} | {control['fallback']} |" + ) + adjunct = project["adjunct_automations"] + lines.extend([ + "", + "### 范围外通用自动化盘点", + "", + "- " + (";".join(map(str, adjunct)) if adjunct else "未纳入;如需通知、自动指派、SLA、字段同步等,应单独授权和设计。"), + "", + ]) + lines.extend([ + "## 执行检查", + "", + "- [ ] 两个或多个项目的规则实例、控制台账和证据已分别填写。", + "- [ ] 已逐条读取真实触发配置、规则顺序、启用状态和执行账号。", + "- [ ] 已检查后继规则、重复规则和跨阶段连锁流转。", + "- [ ] 已验证失败用例、缺陷已修复、复测通过/失败时双方状态一致。", + "- [ ] 已逐仓库确认真实dev/develop分支、正式关联和全部MR门禁。", + "- [ ] 已区分test部署、生产发布、发布变更和业务验收。", + "- [ ] 已执行正向、负向、异步、多仓库、失败重试和旧规则连锁测试。", + "", + ]) + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + validate_parser = subparsers.add_parser("validate", help="验证项目配置") + validate_parser.add_argument("profile", type=Path) + render_parser = subparsers.add_parser("render", help="生成 Markdown 执行计划") + render_parser.add_argument("profile", type=Path) + render_parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + + try: + data = load_profile(args.profile) + errors = validate(data) + except (OSError, json.JSONDecodeError, ValueError) as error: + print(f"配置读取失败:{error}", file=sys.stderr) + return 2 + if errors: + for error in errors: + print(f"[错误] {error}", file=sys.stderr) + return 1 + if args.command == "validate": + print( + f"[通过] 配置有效:{len(data['projects'])} 个项目;" + "每项目12个需求规则实例、8个任务规则实例、31个闭环控制项。" + ) + return 0 + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(render(data), encoding="utf-8") + print(f"[完成] 已生成执行计划:{args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/AGENTS.md b/AGENTS.md index 084b079..80969ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,7 @@ | 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` | | 画布 | `src/prototypes//canvas.excalidraw`、`canvas-assets/` | 原型画布和画布素材 | | 业务逻辑规格 | `src/prototypes//.spec/.md` | `rules/business-logic-documentation-guide.md` | +| AutoPRD + 标注目录 | `.spec/requirements-prd.md` + `annotation-source.json`;定稿写第 10 章功能变更 | skill `oneos-autoprd`;规则 `.cursor/rules/oneos-autoprd-sync.mdc` | | 对象存储发布 URL | `.axhub/make/axhub.config.json` → `cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` | | 原型标注布局 | `src/common/prototype-annotation-host.tsx` | `rules/prototype-annotation-layout-guide.md` | diff --git a/docs/superpowers/plans/2026-07-20-h5-vehicle-assets.md b/docs/superpowers/plans/2026-07-20-h5-vehicle-assets.md new file mode 100644 index 0000000..be0f1ba --- /dev/null +++ b/docs/superpowers/plans/2026-07-20-h5-vehicle-assets.md @@ -0,0 +1,65 @@ +# 车辆资产 H5 Implementation Plan + +> **For agentic workers:** Execute task-by-task. Checkboxes track progress. Prefer inline execution in this repo (React prototype, no separate unit-test harness). + +**Goal:** 新建独立 H5 原型 `oneos-h5-vehicle-assets`:只读查车 + KPI/异常筛选 + 详情多 Tab,口径对齐 PC `vehicle-management`。 + +**Architecture:** PhoneShell 手机壳 + 列表/详情双视图;数据与 KPI 规则复用 PC `vehicles.json` 与 `utils/vehicle.ts`(及各记录 utils);UI 不复用 `vm-page` 宽表 DOM。 + +**Tech Stack:** React + TypeScript、本地 JSON 种子、Axhub annotation、`nav:sync` 注册导航。 + +--- + +## File map + +| Path | Responsibility | +|------|----------------| +| `src/prototypes/oneos-h5-vehicle-assets/index.tsx` | 入口:列表/详情状态、筛选、分页 | +| `styles/index.css` | H5 样式(ONE-OS 绿) | +| `components/PhoneShell.tsx` | 手机预览壳 | +| `components/VehicleList.tsx` | 搜索、KPI、筛选抽屉、卡片 | +| `components/VehicleCard.tsx` | 单车卡片 | +| `components/FilterSheet.tsx` | 证照/保险状态抽屉 | +| `components/VehicleDetail.tsx` | 详情顶栏 + Tab | +| `components/detailTabs.tsx` | 11 个只读 Tab 内容 | +| `utils/filter.ts` | 列表过滤封装 | +| `.spec/requirements-prd.md` | AutoPRD | +| `annotation-source.json` | 标注目录 | +| `nav-menu.json` + `nav:sync` | 导航注册 | + +--- + +### Task 1: Scaffold + list shell + +- [ ] Create prototype files with PhoneShell, list layout, import vehicles +- [ ] Wire KPI via `matchKpi` / `countKpi` / `isLicenseExpired` +- [ ] Search + license/insurance filter + cards + page size 20 + +### Task 2: Detail multi-tab readonly + +- [ ] Detail view with 11 tabs matching PC `DETAIL_TABS` +- [ ] Basic/model/license as field grids; record tabs via PC utils filtered by plate +- [ ] No edit/action buttons + +### Task 3: Nav + PRD + annotation + +- [ ] Add nav item under 车辆资产 folder as「车辆资产(H5)」 +- [ ] Full AutoPRD + annotation-source PRD node +- [ ] Run `npm run nav:sync -- --prototype oneos-h5-vehicle-assets` + +### Task 4: Smoke check + +- [ ] Open `/prototypes/oneos-h5-vehicle-assets` and verify list → detail → tabs + +--- + +## Spec coverage + +| Spec item | Task | +|-----------|------| +| Independent H5 prototype | 1 | +| Search + KPI + anomaly filter + cards | 1 | +| Detail 11 tabs readonly | 2 | +| No edit including admin | 1–2 | +| Nav + PRD | 3 | +| KPI/anomaly口径 = PC | 1 (shared utils) | diff --git a/docs/superpowers/specs/2026-07-20-h5-vehicle-assets-design.md b/docs/superpowers/specs/2026-07-20-h5-vehicle-assets-design.md new file mode 100644 index 0000000..68508e5 --- /dev/null +++ b/docs/superpowers/specs/2026-07-20-h5-vehicle-assets-design.md @@ -0,0 +1,189 @@ +# 车辆资产 H5 适配 · 设计规格 + +| 项 | 说明 | +|---|---| +| 日期 | 2026-07-20 | +| 状态 | 已确认(2026-07-20) | +| 原型 ID | `oneos-h5-vehicle-assets` | +| 关联 PC | `vehicle-management`(车辆资产) | +| 参考 H5 | `oneos-h5-h2-order`(PhoneShell、交互密度) | + +--- + +## 1. 背景与目标 + +App 除推送外全部接入 H5。PC「车辆资产」为宽表中后台,不适合同一页响应式塞进手机。采用**独立 H5 原型**,口径与 PC 对齐,交互按手机重做。 + +### 成功标准 + +1. App / 手机浏览器打开 H5 地址,不进入 PC 宽表页。 +2. KPI 分类与证照/保险异常口径与 PC 一致。 +3. 详情多 Tab 可浏览,**全程无编辑入口**(含 Admin)。 +4. 按车牌搜索可定位车辆。 + +--- + +## 2. 已确认产品决策 + +| 决策 | 结论 | +|---|---| +| 落地方式 | 独立原型 `oneos-h5-vehicle-assets`(方案 1) | +| 角色 | 全角色同一套界面 | +| 编辑 | **手机端不做编辑**(含 Admin;改数仅 PC) | +| 异常 | 只看:可筛、详情标红/提示;不催办、不跳转业务处理页 | +| 列表 | 搜索 + 横滑 KPI + 筛选抽屉 + 车辆卡片(对齐 PC 信息量) | +| 详情 | 多 Tab 对齐 PC,只读 | + +--- + +## 3. 范围 + +### 3.1 第一期做 + +| 模块 | 内容 | +|---|---| +| 列表 | 顶栏标题「车辆资产」+ 车牌搜索;KPI 横滑;证照/保险等筛选;卡片列表;**触底异步加载更多**(无翻页、不展示总辆数) | +| 详情 | 返回 + 车牌(透明顶栏贴一体背景);浮起白卡摘要(营运状态、标签、里程、证照/保险异常);横向 Tab + 只读内容;**无**底部悬浮 Tab | +| 数据 | 复用 PC `vehicles.json` 及关联记录 JSON;KPI/异常规则与 PC 同源工具函数(可抽共享或复制后标注同源) | +| 壳层 | PhoneShell 桌面预览;真机/WebView 全屏内容区 | +| 导航 | 注册到原型导航「运维管理」下,与 PC 车辆资产并列区分(如「车辆资产(H5)」) | + +### 3.2 第一期不做 + +- 任意字段编辑、运维负责人修改、运营城市修改 +- 批量导入 / 导出真实能力(不做入口,或仅隐藏) +- 催办、抄送、跳转证照管理/保险采购等业务页(不做;外链 Toast 占位也不做,避免误导) +- 在租车型占比弹窗(PC 尾部 KPI;H5 第一期可不做,避免干扰主路径) +- PC 页响应式改造 + +--- + +## 4. 信息架构 + +```text +列表 List +├── 顶栏:标题 + 搜索(车牌) +├── KPI 横滑条(点选切换分类,计数与 PC 一致) +├── 筛选入口 → 底部抽屉(证照状态、保险状态;可扩展运营状态等) +└── 车辆卡片列表 → 进入详情 + +详情 Detail +├── 顶栏:返回 + 车牌号(透明,贴一体渐变底;无底栏) +├── 摘要卡:车牌 / 品牌型号 / 营运状态 / 来源·城市·项目标签 / 里程 / 证照·保险异常 +└── Tab(横滑):与 PC DETAIL_TABS 一致 + 基本信息 / 型号参数 / 证照信息 / 保险记录 / 租赁记录 / + 事故记录 / 故障记录 / 违章记录 / 异动记录 / 调拨记录 / 年审记录 +``` + +--- + +## 5. KPI 与异常口径(与 PC 一致) + +| KPI | 口径 | +|---|---| +| 所有营运车辆 | 非「非运营车辆」且非「退出运营」 | +| 租赁车辆 | `operateStatus === '租赁'` | +| 物流车辆 | `operateStatus === '物流'` | +| 库存车辆 | `operateStatus` 为「可运营」或「待运营」 | +| 非运营车辆 | `vehicleLedgerType === '非运营车辆'` | +| 退出运营车辆 | `operateStatus === '退出运营'` | + +| 异常筛选 | 口径 | +|---|---| +| 证照异常 | `inspectExpire` 早于今天 | +| 保险异常 | `insuranceStatus === '异常'` | + +筛选与 KPI **叠加**:先 KPI,再筛选条件。 + +--- + +## 6. 列表卡片字段(建议) + +Boutique 横卡布局:左侧信息、右侧状态区(无车辆照片)。至少展示: + +- 车牌(主标题) +- 品牌·型号 +- 营运状态标签(语义色) +- 证照/保险异常角标(有异常时显示) +- 车辆来源 / 运营城市 / 项目或「车辆在库」标签 +- 里程(有数据时,右侧次要数字) + +点击整卡进入详情。 + +### 6.1 详情摘要卡(与列表同语义) + +浮起白卡(同列表卡片圆角/阴影),**不加**车辆实景大图: + +- 车牌(大标题)+ 品牌·型号 +- 营运状态(圆点 + 与列表一致的文案) +- 车辆来源 / 运营城市 / 项目或备车等上下文标签 +- 里程(有数据时) +- 证照/保险异常角标(有异常时) + +Tab 选中态对齐 Chip(主色字 + 浅底),内容区为白卡 Section / 记录列表;字段 label 12 / value 15。 + +--- + +## 7. 视觉与设计基底 + +| 项 | 约定 | +|---|---| +| 交互壳 | 对齐 `oneos-h5-h2-order` PhoneShell 结构(桌面预览);真机全屏内容区 | +| 主题 | **SaaS Boutique App 壳**:对齐参考图 A(顶栏左标题 + 铃铛/加号、KPI 图标卡、筛选项行、实景横卡、底部五 Tab) | +| 色彩 | Primary `#007AFF`;页面灰 `#F2F3F5`;卡片白底;语义绿/橙/红 | +| 风格 | 一体式浅蓝渐变底(列表与详情共用;顶栏透明不分色带);KPI 图标卡 + Chip;列表/详情浮起白卡(无车辆照片) | +| 字体 | **苹方 PingFang SC**(回退 Hiragino Sans GB / 微软雅黑 / system) | +| 字号阶梯 | 标题 22 / 详情车牌 26 / KPI 数 20 / 列表车牌 17 / 正文 15 / 车型与 Chip 12 / 标签与底栏 11 | +| 字重 | 标题/车牌 800(系统会映射为 Semibold/Medium);区块标题 700;正文 400–500;标签 600 | +| 壳层约定 | **列表**底部为悬浮胶囊 Tab(毛玻璃);「工作台/任务/消息/我的」Toast 占位;本页「车辆」选中。**详情**不挂底栏,仅顶栏返回 + 车牌 | +| 触控 | ≥ 44×44px(Chip/图标区可视觉更紧,点击热区保留);`safe-area-inset` | +| 动效 | ≤ 240ms;尊重 `prefers-reduced-motion` | + +--- + +## 8. 技术约定(实现阶段) + +| 项 | 约定 | +|---|---| +| 目录 | `src/prototypes/oneos-h5-vehicle-assets/` | +| 入口 | `index.tsx`;样式独立 `styles/index.css` | +| 标注 | `annotation-source.json` + `.spec/requirements-prd.md`(实现时按 oneos-autoprd 同步) | +| 与 PC 关系 | 逻辑口径同源;UI 组件不直接复用 `vm-page` 宽表 DOM | +| App 打开 | 固定 H5 URL(发布后 `{baseUrl}/oneos-h5-vehicle-assets/index.html` 形态以云发布规则为准) | + +--- + +## 9. 用户故事(摘要) + +1. **作为**任意登录用户,**我想**在手机上按车牌找到车,**以便**外勤快速核对车辆信息。 +2. **作为**运维/业务,**我想**用 KPI 与证照/保险异常筛选缩小范围,**以便**发现需跟进车辆(跟进在 PC 或其他模块完成)。 +3. **作为**任意用户,**我想**在详情里切换各业务 Tab 只读浏览,**以便**不回 PC 也能看全档案摘要与记录。 + +--- + +## 10. 验收清单 + +- [ ] 独立原型可在导航打开,桌面 PhoneShell / 窄屏可用 +- [ ] KPI 六类计数与同数据下 PC 一致 +- [ ] 证照异常、保险异常筛选结果与 PC 规则一致 +- [ ] 搜索车牌可过滤列表 +- [ ] 列表无总辆数、无翻页;触底异步加载更多 +- [ ] 列表进详情:一体渐变底连续,无白顶栏色带割裂;摘要为浮起白卡(状态/标签/里程/异常与列表同语义) +- [ ] 详情无底部悬浮 Tab;顶栏仅返回 + 车牌 +- [ ] 详情 11 个 Tab 均可进入且只读,无编辑按钮 +- [ ] 无导入/导出/修改运维负责人等 PC 写操作入口 + +--- + +## 11. 修订记录 + +| 版本 | 日期 | 说明 | +|---|---|---| +| v0.7 | 2026-07-21 | 列表改为触底异步加载;移除翻页与总辆数展示 | +| v0.6 | 2026-07-21 | 对齐参考图 A 结构:App 顶栏/底栏、KPI 图标卡、筛选项行、实景横卡;写操作仍 Toast 占位 | +| v0.5 | 2026-07-21 | 视觉切换至 SaaS Boutique(Electric Blue + KPI 摘要卡 + 横卡列表);产品范围不变 | +| v0.5 | 2026-07-21 | H5 按参考图标注尺寸落地(16/8/12/44 栅格) | +| v0.4 | 2026-07-20 | 手机交互增强:粘性工具栏、safe-area、系统返回、空态引导 | +| v0.3 | 2026-07-20 | 视觉切换至 `src/themes/apple`(Action Blue + utility card) | +| v0.2 | 2026-07-20 | 视觉基底补充:Premium SaaS Mobile 质感(品牌绿不变) | +| v0.1 | 2026-07-20 | 需求访谈确认后首版设计规格 | diff --git a/docs/superpowers/specs/2026-07-21-h5-mileage-task-card-design.md b/docs/superpowers/specs/2026-07-21-h5-mileage-task-card-design.md new file mode 100644 index 0000000..b30f560 --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-h5-mileage-task-card-design.md @@ -0,0 +1,16 @@ +# H5 车辆卡片 · 里程任务区设计 + +| 项 | 说明 | +|---|---| +| 日期 | 2026-07-21 | +| 状态 | 已确认 | +| 原型 | `oneos-h5-vehicle-assets` | +| 规格全文 | `src/prototypes/oneos-h5-vehicle-assets/.spec/mileage-task.md` | + +## 结论摘要 + +- 列表每张车卡在标签行下展示「里程任务」区块(全宽)。 +- 有任务:标题 + 完成百分比 + 进度条 +「剩余 xx km · 预计 xx 天内完成」。 +- 无任务:标题 +「暂无里程任务」。 +- 预计天数 = 剩余里程 ÷ 近 7 个活跃日(日里程 > 0)平均日里程,向上取整;无活跃日则「暂无法预计」。 +- 业务来源:采购合同关联批次车辆里程要求,经任务工单下发(原型用本地演示种子,未接真实 API)。 diff --git a/docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.md b/docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.md new file mode 100644 index 0000000..9e87e42 --- /dev/null +++ b/docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.md @@ -0,0 +1,110 @@ +# 车辆资产 UI · 边距与尺寸标注(估算) + +> 来源:参考截图标注 +> 基准宽:**390pt**(iPhone 常见逻辑宽) +> 方法:按屏内容像素测量后换算,并按 **8pt 栅格**圆整 +> 标注图:`docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.png` + +## 1. 页面骨架 + +| 元素 | 尺寸 / 边距 | 备注 | +|------|-------------|------| +| 内容区宽度 | **390** | 逻辑宽 | +| 左右页边距 | **16** | 搜索、KPI、列表、Chip 统一 | +| 主内容可用宽 | **358** | 390 − 16×2 | +| 区块纵向间距 | **12–16** | 搜索→KPI、Chip→列表 | + +## 2. 顶栏 + +| 元素 | 尺寸 | 备注 | +|------|------|------| +| 标题行高 | **40** | 「车辆资产」+ 右侧操作 | +| 标题字号 | **≈22–24 / Semibold** | SF Pro Display 观感 | +| 通知铃 / 加号 | **36×36** | 触控建议扩到 44 | +| 右侧图标间距 | **8** | | + +## 3. 搜索栏 + +| 元素 | 尺寸 | 备注 | +|------|------|------| +| 高度 | **44** | 满足触控下限 | +| 宽度 | **358** | 满宽 − 页边距 | +| 圆角 | **12** | | +| 左右内边距 | **12** | 左放大镜 / 右扫码 | +| 占位字号 | **15–17** | | + +## 4. KPI 横滑卡片 + +| 元素 | 尺寸 | 备注 | +|------|------|------| +| 卡片宽 × 高 | **90 × 100** | 约露出 4 张 | +| 卡片间距 | **8** | | +| 圆角 | **12–16** | | +| 主数字 | **≈20–22 / Bold** | tabular-nums | +| 分类标题 | **12** | | +| 底部分页点 | **Ø6 / 间距 6** | | + +## 5. 筛选 Chip 行 + +| 元素 | 尺寸 | 备注 | +|------|------|------| +| Chip 高 | **32** | 若偏难点可提到 36 | +| Chip 宽 | **≈78–90** | 随文案 | +| Chip 间距 | **8** | | +| 圆角 | **8** | | +| 字号 | **13–14** | 选中态浅蓝底 | + +## 6. 车辆列表卡片 + +| 元素 | 尺寸 | 备注 | +|------|------|------| +| 卡片宽 × 高 | **358 × 118** | | +| 卡片圆角 | **16** | | +| 卡片垂直间距 | **12** | | +| 卡片内边距 | **12** | 四边一致 | +| 车辆缩略图 | **100 × 74** | 圆角 ≈8 | +| 图文间距 | **12** | 缩略图 ↔ 文案 | +| 车牌字号 | **≈17–18 / Semibold** | | +| 车型副文 | **13–14** | 次要灰 | +| Tag 高 | **≈22** | 圆角 6 | +| Tag 间距 | **6–8** | | +| 右侧状态区 | **≈72 宽** | 状态点 + 里程 + 异常标 | +| 右箭头 | **12–16** | | + +## 7. 底栏 + +| 元素 | 尺寸 | 备注 | +|------|------|------| +| 底栏总高 | **≈83** | 含 Home Indicator ≈34 | +| 可点区域高 | **≈49** | | +| Tab 等分 | **5** | | +| 图标 | **24×24** | | +| 文案 | **10** | | +| 中心「工作台」 | **⌀56** | 略浮起 | +| 消息红点 | **≈16–18** | | + +## 8. 推荐实现 Token(可直接写 CSS) + +```css +--page-pad: 16px; +--gap-sm: 8px; +--gap-md: 12px; +--gap-lg: 16px; +--radius-chip: 8px; +--radius-control: 12px; +--radius-card: 16px; +--search-h: 44px; +--chip-h: 32px; +--kpi-w: 90px; +--kpi-h: 100px; +--thumb-w: 100px; +--thumb-h: 74px; +--card-pad: 12px; +--touch-min: 44px; +``` + +## 9. 说明 + +- 数值为**视觉估算**,不是设计工具导出的精确标注。 +- 若要以设计稿为准,请用 Figma / Sketch 再量一次关键项(页边距、卡片高、缩略图)。 +- 手机实现时:**触控热区 ≥ 44×44**,即使视觉图标只有 24/36。 diff --git a/docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.png b/docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.png new file mode 100644 index 0000000..41b8451 Binary files /dev/null and b/docs/superpowers/specs/2026-07-21-vehicle-assets-ui-measure.png differ diff --git a/src/common/oneos-app-shell/OneOsAppShell.tsx b/src/common/oneos-app-shell/OneOsAppShell.tsx new file mode 100644 index 0000000..20bd998 --- /dev/null +++ b/src/common/oneos-app-shell/OneOsAppShell.tsx @@ -0,0 +1,413 @@ +import './oneos-app-shell.css'; + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + ChevronDown, + ChevronRight, + LayoutDashboard, + Menu, + Moon, + Sun, + PanelLeftClose, + RefreshCw, + Search, + Maximize, + X, + Sparkles, + type LucideIcon, + FolderOpen, + FileText, + Truck, + Shield, + Wallet, + Fuel, + BatteryCharging, + Users, + Workflow, + Settings, + ClipboardList, + BarChart3, + Wrench, +} from 'lucide-react'; +import navMenuJson from '../../prototypes/oneos-prototype-nav/nav-menu.json'; +import { + buildShellMenuFromNav, + collectOpenKeys, + findMenuPath, + hrefPathname, + type ShellMenuItem, +} from './nav-from-prototypes'; +import { + readStoredOneOsTheme, + setOneOsTheme, + type OneOsTheme, +} from './theme'; +import { ShellNoticeCenter } from './ShellNoticeCenter'; +import { + isNoticesSync, + postNoticeAction, + type ShellNoticeItem, +} from './notice-bridge'; + +const AVATAR_URL = 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp'; + +/** 演示壳自身,不在内容区嵌套打开 */ +export const PROTOTYPE_DEMO_HREF = '/prototypes/oneos-prototype-demo'; + +function iconForLabel(label: string): LucideIcon { + if (/原型演示|工作台/.test(label)) return LayoutDashboard; + if (/审批/.test(label)) return ClipboardList; + if (/车辆|运维|资产/.test(label)) return Truck; + if (/安全/.test(label)) return Shield; + if (/财务|应收|收款|应结/.test(label)) return Wallet; + if (/加氢|氢/.test(label)) return Fuel; + if (/充电/.test(label)) return BatteryCharging; + if (/用户|客户|机构|角色|部门/.test(label)) return Users; + if (/工作流|流程/.test(label)) return Workflow; + if (/系统|菜单|字典|参数|日志/.test(label)) return Settings; + if (/BI|统计|分析|台账|明细|盈亏|回款/.test(label)) return BarChart3; + if (/合同|业务|保险|供应商/.test(label)) return FileText; + if (/任务|工单|调度/.test(label)) return Wrench; + if (/配置|模板/.test(label)) return FolderOpen; + return FolderOpen; +} + +export type ShellTab = { + href: string; + title: string; +}; + +export type OneOsAppShellProps = { + children: React.ReactNode; + activeHref?: string; + pageTitle?: string; + breadcrumb?: string[]; + /** 自定义导航:用于演示壳在内容区切换;不传则整页跳转 */ + onNavigate?: (item: ShellMenuItem) => void; + /** 多页签(演示壳) */ + tabs?: ShellTab[]; + onTabSelect?: (href: string) => void; + onTabClose?: (href: string) => void; + onRefresh?: () => void; + brandHref?: string; + /** 原型演示:顶栏「版本更新」图标,打开更新日志 */ + onOpenReleaseNotes?: () => void; + /** 是否有未读版本更新(显示角标) */ + releaseNotesUnread?: boolean; +}; + +export function OneOsAppShell({ + children, + activeHref = PROTOTYPE_DEMO_HREF, + pageTitle = '原型演示', + breadcrumb, + onNavigate: onNavigateProp, + tabs, + onTabSelect, + onTabClose, + onRefresh, + brandHref = PROTOTYPE_DEMO_HREF, + onOpenReleaseNotes, + releaseNotesUnread = false, +}: OneOsAppShellProps) { + const menu = useMemo( + () => buildShellMenuFromNav(navMenuJson as Parameters[0]), + [], + ); + const path = useMemo(() => findMenuPath(menu, activeHref) || [], [menu, activeHref]); + const [collapsed, setCollapsed] = useState(false); + const [openKeys, setOpenKeys] = useState(() => collectOpenKeys(path)); + const [theme, setTheme] = useState(() => readStoredOneOsTheme()); + const [shellNotices, setShellNotices] = useState([]); + const [shellUnread, setShellUnread] = useState(0); + + useEffect(() => { + setOneOsTheme(theme); + }, [theme]); + + useEffect(() => { + const onMsg = (e: MessageEvent) => { + if (!isNoticesSync(e.data)) return; + setShellNotices(e.data.notices); + setShellUnread(e.data.unreadCount); + }; + window.addEventListener('message', onMsg); + return () => window.removeEventListener('message', onMsg); + }, []); + + useEffect(() => { + setShellNotices([]); + setShellUnread(0); + }, [activeHref]); + + const frameWindow = useCallback((): Window | null => { + if (typeof document === 'undefined') return null; + const frame = document.querySelector('.oneos-shell-frame'); + return frame?.contentWindow || null; + }, []); + + const onNoticeRead = useCallback( + (notice: ShellNoticeItem) => { + setShellNotices((prev) => + prev.map((n) => (n.id === notice.id ? { ...n, read: true } : n)), + ); + setShellUnread((c) => Math.max(0, c - (notice.read ? 0 : 1))); + const win = frameWindow(); + if (win) postNoticeAction(win, 'read', notice.id); + }, + [frameWindow], + ); + + const onNoticeOpen = useCallback( + (notice: ShellNoticeItem) => { + const win = frameWindow(); + if (win) postNoticeAction(win, 'open', notice.id); + }, + [frameWindow], + ); + + const onNoticeHandle = useCallback( + (notice: ShellNoticeItem) => { + setShellNotices((prev) => + prev.map((n) => (n.id === notice.id ? { ...n, read: true } : n)), + ); + setShellUnread((c) => Math.max(0, c - (notice.read ? 0 : 1))); + const win = frameWindow(); + if (win) postNoticeAction(win, 'handle', notice.id); + }, + [frameWindow], + ); + + useEffect(() => { + setOpenKeys((prev) => { + const next = collectOpenKeys(path); + return Array.from(new Set([...prev, ...next])); + }); + }, [path]); + + const toggleTheme = useCallback(() => { + setTheme((prev) => { + const next: OneOsTheme = prev === 'dark' ? 'light' : 'dark'; + setOneOsTheme(next); + return next; + }); + }, []); + + const crumbs = breadcrumb?.length + ? breadcrumb + : path.length + ? path.map((p) => p.label) + : ['OneOS', pageTitle]; + + const selectedKey = path[path.length - 1]?.key; + const showTabs = Array.isArray(tabs); + + const handleNavigate = useCallback( + (item: ShellMenuItem) => { + if (onNavigateProp) { + onNavigateProp(item); + return; + } + if (item.href && typeof window !== 'undefined') { + window.location.href = item.href; + } + }, + [onNavigateProp], + ); + + const toggleOpen = useCallback((key: string) => { + setOpenKeys((prev) => (prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key])); + }, []); + + const renderItems = (items: ShellMenuItem[], level = 0): React.ReactNode => + items.map((item) => { + const hasChildren = !!item.children?.length; + const opened = openKeys.includes(item.key); + const selected = + item.key === selectedKey || + (!!item.href && hrefPathname(item.href) === hrefPathname(activeHref)); + const Icon = iconForLabel(item.label); + + return ( +
  • + + {!collapsed && hasChildren && opened && ( +
      {renderItems(item.children!, level + 1)}
    + )} +
  • + ); + }); + + return ( +
    + + +
    +
    +
    +
    + + + +
    +
    + + {onOpenReleaseNotes ? ( + + ) : null} + + + + +
    +
    + + {showTabs && tabs!.length > 0 && ( +
    + {tabs!.map((tab) => { + const active = hrefPathname(tab.href) === hrefPathname(activeHref); + return ( +
    + + {onTabClose && ( + + )} +
    + ); + })} +
    + )} +
    + +
    {children}
    +
    +
    + ); +} + +export default OneOsAppShell; diff --git a/src/common/oneos-app-shell/README.md b/src/common/oneos-app-shell/README.md new file mode 100644 index 0000000..a62aae4 --- /dev/null +++ b/src/common/oneos-app-shell/README.md @@ -0,0 +1,12 @@ +# 羚牛 OneOS 应用外壳 + +> 实现:`src/common/oneos-app-shell/` +> 宿主页:`/prototypes/oneos-prototype-demo`(原型演示) + +## 用法 + +- **带外壳浏览所有原型**:打开「原型演示」,从左侧菜单点选;内容区 iframe 嵌入。 +- **单独打开某原型**:访问 `/prototypes/`,无侧栏顶栏。 +- **浅色 / 暗色**:顶栏月亮/太阳按钮切换;暗色参照原型导航页风格。对 OneOS 目录下原型生效(含直链),**不包括「旧 ONEOS」**。 + +菜单数据:`oneos-prototype-nav/nav-menu.json`。 diff --git a/src/common/oneos-app-shell/ShellNoticeCenter.tsx b/src/common/oneos-app-shell/ShellNoticeCenter.tsx new file mode 100644 index 0000000..fdd853b --- /dev/null +++ b/src/common/oneos-app-shell/ShellNoticeCenter.tsx @@ -0,0 +1,364 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Bell, BellOff, X } from 'lucide-react'; +import type { ShellNoticeItem } from './notice-bridge'; + +/** 气泡/标签:x条新通知,请尽快处理(阿拉伯数字) */ +function formatNewMessageTip(count: number): string { + return `${count}条新通知,请尽快处理`; +} + +function compareNoticePriority(a: ShellNoticeItem, b: ShellNoticeItem): number { + const aUrge = a.type === '催办提醒' ? 0 : 1; + const bUrge = b.type === '催办提醒' ? 0 : 1; + if (aUrge !== bUrge) return aUrge - bUrge; + return b.time.localeCompare(a.time); +} + +type AllTab = 'unread' | 'read'; + +type ShellNoticeCenterProps = { + notices: ShellNoticeItem[]; + unreadCount: number; + onRead: (notice: ShellNoticeItem) => void; + onOpen: (notice: ShellNoticeItem) => void; + onHandle: (notice: ShellNoticeItem) => void; +}; + +function NoticeListItem({ + notice, + showHandle, + showSummary = false, + onSelect, + onHandle, +}: { + notice: ShellNoticeItem; + showHandle: boolean; + showSummary?: boolean; + onSelect: (notice: ShellNoticeItem) => void; + onHandle: (notice: ShellNoticeItem) => void; +}) { + const canHandle = !!(notice.href || notice.taskId); + return ( +
  • + + {showHandle && canHandle ? ( + + ) : null} +
  • + ); +} + +/** 顶栏铃铛 + 新消息闪烁提示 + 下拉通知中心(对齐工作台 NoticePanel) */ +export function ShellNoticeCenter({ + notices, + unreadCount, + onRead, + onOpen: _onOpen, + onHandle, +}: ShellNoticeCenterProps) { + const [open, setOpen] = useState(false); + const [allOpen, setAllOpen] = useState(false); + const [allTab, setAllTab] = useState('unread'); + const [detailNotice, setDetailNotice] = useState(null); + const wrapRef = useRef(null); + const allModalRef = useRef(null); + const [portalRoot, setPortalRoot] = useState(null); + + useEffect(() => { + const shell = wrapRef.current?.closest('.oneos-shell') as HTMLElement | null; + setPortalRoot(shell || document.body); + }, []); + + const pool = useMemo( + () => notices.filter((n) => !n.read).slice().sort(compareNoticePriority), + [notices], + ); + const unreadList = pool; + const readList = useMemo( + () => notices.filter((n) => n.read).slice().sort(compareNoticePriority), + [notices], + ); + const allTabList = allTab === 'unread' ? unreadList : readList; + + const closeAll = () => { + setOpen(false); + setAllOpen(false); + setDetailNotice(null); + }; + + useEffect(() => { + if (!open && !detailNotice && !allOpen) return; + const onDoc = (e: MouseEvent) => { + const target = e.target as Node; + if (allOpen) { + if (allModalRef.current && !allModalRef.current.contains(target)) { + setAllOpen(false); + } + return; + } + if (!wrapRef.current?.contains(target)) closeAll(); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key !== 'Escape') return; + if (detailNotice) setDetailNotice(null); + else if (allOpen) setAllOpen(false); + else setOpen(false); + }; + document.addEventListener('mousedown', onDoc); + document.addEventListener('keydown', onKey); + return () => { + document.removeEventListener('mousedown', onDoc); + document.removeEventListener('keydown', onKey); + }; + }, [open, detailNotice, allOpen]); + + useEffect(() => { + if (!allOpen) return; + const lockEl = portalRoot || document.body; + const prev = lockEl.style.overflow; + lockEl.style.overflow = 'hidden'; + return () => { + lockEl.style.overflow = prev; + }; + }, [allOpen, portalRoot]); + + const handleSelect = (notice: ShellNoticeItem) => { + onRead(notice); + setDetailNotice(notice); + setOpen(false); + setAllOpen(false); + }; + + const handleAction = (notice: ShellNoticeItem) => { + onRead(notice); + onHandle(notice); + closeAll(); + }; + + return ( +
    + {unreadCount > 0 && !open && !detailNotice && !allOpen ? ( + + {formatNewMessageTip(unreadCount)} + + ) : null} + + + {open && !detailNotice && !allOpen ? ( +
    +
    +

    + 通知中心 + {unreadCount > 0 ? ( + {formatNewMessageTip(unreadCount)} + ) : null} +

    + +
    + +
    + {pool.length ? ( +
      + {pool.map((notice) => ( + + ))} +
    + ) : ( +
    + +

    暂无新消息

    +

    + 当前通知均已读完 +
    + 有新催办或业务提醒时会显示在这里 +

    +
    + )} +
    +
    + ) : null} + + {allOpen && portalRoot + ? createPortal( +
    +
    +
    +

    全部通知

    + +
    +
    + + +
    +
    + {allTabList.length ? ( +
      + {allTabList.map((notice) => ( + + ))} +
    + ) : ( +
    + +

    + {allTab === 'unread' ? '暂无未读通知' : '暂无已读通知'} +

    +

    + {allTab === 'unread' + ? '当前通知均已读完,有新催办或业务提醒时会显示在这里' + : '已读通知会显示在这里'} +

    +
    + )} +
    +
    +
    , + portalRoot, + ) + : null} + + {detailNotice ? ( +
    +
    +

    + {detailNotice.type === '催办提醒' ? ( + 催办提醒 + ) : null} + {detailNotice.bizTag} + {detailNotice.title} +

    + +
    +
    + {detailNotice.time} +

    {detailNotice.detail || detailNotice.title}

    + {detailNotice.summary ? ( +

    {detailNotice.summary}

    + ) : null} +
    +
    + + {(detailNotice.href || detailNotice.taskId) && ( + + )} +
    +
    + ) : null} +
    + ); +} diff --git a/src/common/oneos-app-shell/index.ts b/src/common/oneos-app-shell/index.ts new file mode 100644 index 0000000..182609e --- /dev/null +++ b/src/common/oneos-app-shell/index.ts @@ -0,0 +1,50 @@ +export { OneOsAppShell, PROTOTYPE_DEMO_HREF } from './OneOsAppShell'; +export type { OneOsAppShellProps, ShellTab } from './OneOsAppShell'; +export { ShellNoticeCenter } from './ShellNoticeCenter'; +export { + ONEOS_NOTICE_ACTION, + ONEOS_NOTICES_SYNC, + isNoticeAction, + isNoticesSync, + postNoticeAction, + postNoticesSync, + type NoticeActionPayload, + type NoticesSyncPayload, + type ShellNoticeItem, +} from './notice-bridge'; +export { + ONEOS_SHELL_NAV, + isShellNav, + requestShellNav, + type ShellNavPayload, +} from './nav-bridge'; +export { + buildShellMenuFromNav, + findMenuPath, + hrefPathname, + itemKeyToHref, + type ShellMenuItem, +} from './nav-from-prototypes'; +export { + ONEOS_RELEASE_DEMO_ACTION, + RELEASE_SEEN_STORAGE_KEY, + SHELL_CURRENT_RELEASE_VERSION, + clearAllReleaseSeen, + hasUnseenRelease, + isReleaseDemoAction, + postReleaseDemoAction, + postReleaseDemoOpen, + postReleaseDemoReset, + type ReleaseDemoAction, + type ReleaseDemoActionPayload, +} from './release-demo-bridge'; +export { + bootstrapOneOsTheme, + broadcastOneOsTheme, + isLegacyOneOsPrototype, + LEGACY_ONEOS_PROTO_IDS, + readStoredOneOsTheme, + setOneOsTheme, + toggleOneOsTheme, + type OneOsTheme, +} from './theme'; diff --git a/src/common/oneos-app-shell/nav-bridge.ts b/src/common/oneos-app-shell/nav-bridge.ts new file mode 100644 index 0000000..741571b --- /dev/null +++ b/src/common/oneos-app-shell/nav-bridge.ts @@ -0,0 +1,29 @@ +/** iframe 内页面 ↔ 外壳侧栏/页签导航 postMessage 协议 */ + +export const ONEOS_SHELL_NAV = 'ONEOS_SHELL_NAV'; + +export type ShellNavPayload = { + type: typeof ONEOS_SHELL_NAV; + href: string; + title?: string; +}; + +export function isShellNav(data: unknown): data is ShellNavPayload { + return ( + !!data && + typeof data === 'object' && + (data as ShellNavPayload).type === ONEOS_SHELL_NAV && + typeof (data as ShellNavPayload).href === 'string' && + (data as ShellNavPayload).href.startsWith('/prototypes/') + ); +} + +/** 在 iframe 内跳转时优先通知外壳;直链打开则本页跳转 */ +export function requestShellNav(href: string, title?: string): boolean { + if (typeof window === 'undefined' || !href.startsWith('/prototypes/')) return false; + if (window.parent && window.parent !== window) { + window.parent.postMessage({ type: ONEOS_SHELL_NAV, href, title } satisfies ShellNavPayload, '*'); + return true; + } + return false; +} diff --git a/src/common/oneos-app-shell/nav-from-prototypes.ts b/src/common/oneos-app-shell/nav-from-prototypes.ts new file mode 100644 index 0000000..3dd5af2 --- /dev/null +++ b/src/common/oneos-app-shell/nav-from-prototypes.ts @@ -0,0 +1,104 @@ +/** + * 将 oneos-prototype-nav/nav-menu.json 转为羚牛 OneOS 侧栏菜单树 + */ + +export type NavMenuNode = { + id: string; + kind: 'folder' | 'item'; + title: string; + itemKey?: string; + children?: NavMenuNode[]; +}; + +export type ShellMenuItem = { + key: string; + label: string; + href?: string; + children?: ShellMenuItem[]; +}; + +type NavMenuFile = { + prototypes?: NavMenuNode[]; +}; + +export function itemKeyToHref(itemKey?: string): string | undefined { + if (!itemKey) return undefined; + if (itemKey.startsWith('/prototypes/')) return itemKey; + if (itemKey.startsWith('prototypes/')) return `/${itemKey}`; + return `/prototypes/${itemKey}`; +} + +function convertNode(node: NavMenuNode): ShellMenuItem { + if (node.kind === 'item') { + return { + key: node.id || node.itemKey || node.title, + label: node.title, + href: itemKeyToHref(node.itemKey), + }; + } + return { + key: node.id || node.title, + label: node.title, + children: (node.children || []).map(convertNode), + }; +} + +/** 取「OneOS」根目录下的子树作为侧栏;若无则用整棵 prototypes */ +export function buildShellMenuFromNav( + nav: NavMenuFile, + options?: { excludeHrefs?: string[] }, +): ShellMenuItem[] { + const roots = nav.prototypes || []; + const oneos = roots.find((n) => n.kind === 'folder' && /oneos/i.test(n.title)); + const source = oneos?.children?.length ? oneos.children : roots; + const exclude = new Set( + (options?.excludeHrefs ?? ['/prototypes/oneos-prototype-demo']).map((h) => h.replace(/\/$/, '')), + ); + + function filterTree(items: ShellMenuItem[]): ShellMenuItem[] { + return items + .map((it) => { + if (it.href && exclude.has(it.href.replace(/\/$/, ''))) return null; + if (it.children?.length) { + const children = filterTree(it.children); + if (!it.href && children.length === 0) return null; + return { ...it, children }; + } + return it; + }) + .filter(Boolean) as ShellMenuItem[]; + } + + return filterTree(source.map(convertNode)); +} + +/** 去掉 query/hash 与尾部斜杠,供菜单高亮与路径匹配 */ +export function hrefPathname(href: string): string { + return href.split(/[?#]/)[0]!.replace(/\/$/, '') || href; +} + +export function findMenuPath( + items: ShellMenuItem[], + activeHref: string, + trail: ShellMenuItem[] = [], +): ShellMenuItem[] | null { + const activePath = hrefPathname(activeHref); + for (const item of items) { + const next = [...trail, item]; + if (item.href) { + const itemPath = hrefPathname(item.href); + if (activePath === itemPath || activePath.endsWith(itemPath)) { + return next; + } + } + if (item.children?.length) { + const hit = findMenuPath(item.children, activeHref, next); + if (hit) return hit; + } + } + return null; +} + +export function collectOpenKeys(path: ShellMenuItem[]): string[] { + return path.filter((n) => n.children?.length).map((n) => n.key); +} diff --git a/src/common/oneos-app-shell/notice-bridge.ts b/src/common/oneos-app-shell/notice-bridge.ts new file mode 100644 index 0000000..0e34a95 --- /dev/null +++ b/src/common/oneos-app-shell/notice-bridge.ts @@ -0,0 +1,60 @@ +/** 工作台 iframe ↔ 外壳顶栏通知中心 postMessage 协议 */ + +export const ONEOS_NOTICES_SYNC = 'ONEOS_NOTICES_SYNC'; +export const ONEOS_NOTICE_ACTION = 'ONEOS_NOTICE_ACTION'; + +export type ShellNoticeItem = { + id: string; + type: string; + bizTag: string; + title: string; + summary: string; + detail: string; + time: string; + read: boolean; + href?: string; + taskId?: string; +}; + +export type NoticesSyncPayload = { + type: typeof ONEOS_NOTICES_SYNC; + unreadCount: number; + notices: ShellNoticeItem[]; + source?: string; +}; + +export type NoticeActionPayload = { + type: typeof ONEOS_NOTICE_ACTION; + action: 'read' | 'handle' | 'open'; + noticeId: string; +}; + +export function isNoticesSync(data: unknown): data is NoticesSyncPayload { + return ( + !!data && + typeof data === 'object' && + (data as NoticesSyncPayload).type === ONEOS_NOTICES_SYNC && + Array.isArray((data as NoticesSyncPayload).notices) + ); +} + +export function isNoticeAction(data: unknown): data is NoticeActionPayload { + return ( + !!data && + typeof data === 'object' && + (data as NoticeActionPayload).type === ONEOS_NOTICE_ACTION && + typeof (data as NoticeActionPayload).noticeId === 'string' + ); +} + +export function postNoticesSync(target: Window, payload: Omit) { + target.postMessage({ type: ONEOS_NOTICES_SYNC, ...payload }, '*'); +} + +export function postNoticeAction( + target: Window, + action: NoticeActionPayload['action'], + noticeId: string, +) { + target.postMessage({ type: ONEOS_NOTICE_ACTION, action, noticeId }, '*'); +} diff --git a/src/common/oneos-app-shell/oneos-app-shell.css b/src/common/oneos-app-shell/oneos-app-shell.css new file mode 100644 index 0000000..cbe84ab --- /dev/null +++ b/src/common/oneos-app-shell/oneos-app-shell.css @@ -0,0 +1,1564 @@ +/** + * 羚牛 OneOS 应用壳(侧栏 + 顶栏) + * Enterprise SaaS:精致侧栏、玻璃顶栏、流畅微交互 + */ + +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&display=swap'); + +.oneos-shell { + --oneos-aside-w: 232px; + --oneos-aside-w-collapsed: 68px; + --oneos-header-h: 52px; + --oneos-tabs-h: 40px; + --oneos-primary: #32a06e; + --oneos-primary-soft: #3fb87c; + --oneos-ink: #1e293b; + --oneos-ink-soft: rgba(30, 41, 59, 0.88); + --oneos-muted: #64748b; + --oneos-border: #e2e8f0; + --oneos-bg: #f1f5f9; + --oneos-surface: #ffffff; + --oneos-aside-bg: linear-gradient(180deg, #ffffff 0%, #f8fafc 48%, #f1f5f9 100%); + --oneos-ease: cubic-bezier(0.22, 1, 0.36, 1); + --oneos-shadow-aside: 4px 0 24px rgba(15, 23, 42, 0.04); + --oneos-shadow-chrome: 0 1px 0 rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.03); + + display: flex; + width: 100%; + min-height: 100dvh; + height: 100dvh; + overflow: hidden; + background: var(--oneos-bg); + color: var(--oneos-ink); + font-family: + 'Plus Jakarta Sans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'PingFang SC', + 'Microsoft YaHei', + sans-serif; + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* 暗色:参照原型导航页(VoltAgent 深色画布) */ +.oneos-shell[data-oneos-theme='dark'], +html[data-oneos-theme='dark'] .oneos-shell { + --oneos-primary: #00d992; + --oneos-primary-soft: #2fd6a1; + --oneos-ink: #f2f2f2; + --oneos-ink-soft: rgba(242, 242, 242, 0.88); + --oneos-muted: #8b949e; + --oneos-border: #3d3a39; + --oneos-bg: #101010; + --oneos-surface: #1a1a1a; + --oneos-aside-bg: linear-gradient(180deg, #121212 0%, #101010 48%, #0c0c0c 100%); + --oneos-shadow-aside: 4px 0 24px rgba(0, 0, 0, 0.35); + --oneos-shadow-chrome: 0 1px 0 rgba(255, 255, 255, 0.04), 0 8px 24px rgba(0, 0, 0, 0.25); + color-scheme: dark; +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-brand { + background: linear-gradient(180deg, rgba(26, 26, 26, 0.95) 0%, rgba(16, 16, 16, 0.6) 100%); +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-brand__text { + background: linear-gradient(135deg, #f2f2f2 0%, #00d992 55%, #2fd6a1 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-icon-btn:hover { + background: color-mix(in srgb, var(--oneos-primary) 12%, #1a1a1a); +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-search { + background: linear-gradient(180deg, #1a1a1a 0%, #161616 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-search:focus-within { + background: #1a1a1a; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--oneos-primary) 18%, transparent); +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-search kbd { + background: #222; + border-color: #3d3a39; + color: #8b949e; + box-shadow: none; +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-tabs { + background: linear-gradient(180deg, #141414 0%, #101010 100%); +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-tab.is-active { + box-shadow: 0 -1px 0 #1a1a1a, 0 0 0 1px color-mix(in srgb, var(--oneos-border) 80%, transparent); +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-tab.is-active::after { + box-shadow: none; +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-breadcrumb__sep { + color: #565656; +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-avatar img { + border-color: #1a1a1a; +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-frame { + background: #101010; +} + +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-menu__btn:focus-visible, +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-icon-btn:focus-visible, +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-brand__link:focus-visible, +.oneos-shell[data-oneos-theme='dark'] .oneos-shell-avatar:focus-visible { + box-shadow: 0 0 0 2px #101010, 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 45%, transparent); +} + +.oneos-shell *, +.oneos-shell *::before, +.oneos-shell *::after { + box-sizing: border-box; +} + +/* ——— Sidebar ——— */ +.oneos-shell-aside { + flex: 0 0 var(--oneos-aside-w); + width: var(--oneos-aside-w); + display: flex; + flex-direction: column; + background: var(--oneos-aside-bg); + border-right: 1px solid color-mix(in srgb, var(--oneos-border) 85%, transparent); + box-shadow: var(--oneos-shadow-aside); + z-index: 40; + transition: + width 0.28s var(--oneos-ease), + flex-basis 0.28s var(--oneos-ease); + position: relative; +} + +.oneos-shell-aside::after { + content: ''; + position: absolute; + inset: 0 0 0 auto; + width: 1px; + background: linear-gradient( + 180deg, + transparent 0%, + color-mix(in srgb, var(--oneos-primary) 12%, transparent) 20%, + color-mix(in srgb, var(--oneos-border) 90%, transparent) 50%, + transparent 100% + ); + pointer-events: none; +} + +.oneos-shell--collapsed .oneos-shell-aside { + flex-basis: var(--oneos-aside-w-collapsed); + width: var(--oneos-aside-w-collapsed); +} + +.oneos-shell-brand { + height: var(--oneos-header-h); + display: flex; + align-items: center; + justify-content: center; + padding: 0 12px; + border-bottom: 1px solid color-mix(in srgb, var(--oneos-border) 55%, transparent); + flex-shrink: 0; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.6) 100%); + backdrop-filter: blur(8px); +} + +.oneos-shell-brand__link { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + min-height: 44px; + width: 100%; + border-radius: 12px; + text-decoration: none; + color: var(--oneos-ink); + overflow: hidden; + cursor: pointer; + transition: + background 0.2s var(--oneos-ease), + transform 0.2s var(--oneos-ease); +} + +.oneos-shell-brand__link:hover { + background: color-mix(in srgb, var(--oneos-primary) 6%, transparent); +} + +.oneos-shell-brand__link:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 45%, transparent); +} + +.oneos-shell-brand__mark { + display: grid; + place-items: center; + flex-shrink: 0; + filter: drop-shadow(0 2px 6px color-mix(in srgb, var(--oneos-primary) 35%, transparent)); + transition: transform 0.25s var(--oneos-ease), filter 0.25s var(--oneos-ease); +} + +.oneos-shell-brand__link:hover .oneos-shell-brand__mark { + transform: translateY(-1px) scale(1.04); + filter: drop-shadow(0 4px 10px color-mix(in srgb, var(--oneos-primary) 45%, transparent)); +} + +.oneos-shell-brand__text { + font-size: 15px; + font-weight: 700; + letter-spacing: -0.03em; + white-space: nowrap; + background: linear-gradient(135deg, #18181b 0%, #2b9260 55%, #32a06e 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.oneos-shell-nav { + flex: 1; + min-height: 0; + overflow: auto; + padding: 10px 0 20px; + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--oneos-muted) 35%, transparent) transparent; +} + +.oneos-shell-nav::-webkit-scrollbar { + width: 4px; +} + +.oneos-shell-nav::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--oneos-muted) 30%, transparent); + border-radius: 999px; +} + +.oneos-shell-menu, +.oneos-shell-menu__sub { + list-style: none; + margin: 0; + padding: 0; +} + +.oneos-shell-menu__item { + margin: 0 10px 3px; +} + +.oneos-shell-menu__btn { + position: relative; + display: flex; + align-items: center; + gap: 10px; + width: 100%; + min-height: 42px; + padding: 8px 12px; + border: none; + border-radius: 10px; + background: transparent; + color: var(--oneos-ink-soft); + font: inherit; + text-align: left; + cursor: pointer; + transition: + background 0.2s var(--oneos-ease), + color 0.2s var(--oneos-ease), + box-shadow 0.2s var(--oneos-ease), + transform 0.15s var(--oneos-ease); +} + +.oneos-shell-menu__btn::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + width: 3px; + height: 0; + border-radius: 0 3px 3px 0; + background: linear-gradient(180deg, var(--oneos-primary-soft), var(--oneos-primary)); + transform: translateY(-50%); + opacity: 0; + transition: + height 0.22s var(--oneos-ease), + opacity 0.22s var(--oneos-ease); +} + +.oneos-shell-menu__btn:hover { + background: color-mix(in srgb, var(--oneos-primary) 7%, transparent); + color: var(--oneos-ink); +} + +.oneos-shell-menu__btn:active { + transform: scale(0.985); +} + +.oneos-shell-menu__btn:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 40%, transparent); +} + +.oneos-shell-menu__btn.is-active { + color: var(--oneos-primary); + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--oneos-primary) 14%, transparent) 0%, + color-mix(in srgb, var(--oneos-primary) 6%, transparent) 100% + ); + font-weight: 600; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--oneos-primary) 12%, transparent); +} + +.oneos-shell-menu__btn.is-active::before { + height: 18px; + opacity: 1; +} + +.oneos-shell-menu__btn.is-active .oneos-shell-menu__icon { + opacity: 1; + color: var(--oneos-primary); +} + +.oneos-shell-menu__icon { + flex-shrink: 0; + opacity: 0.72; + transition: + opacity 0.2s var(--oneos-ease), + color 0.2s var(--oneos-ease), + transform 0.2s var(--oneos-ease); +} + +.oneos-shell-menu__btn:hover .oneos-shell-menu__icon { + opacity: 1; + transform: scale(1.06); +} + +.oneos-shell-menu__label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + letter-spacing: -0.01em; +} + +.oneos-shell-menu__arrow { + display: inline-flex; + color: var(--oneos-muted); + flex-shrink: 0; + transition: transform 0.22s var(--oneos-ease), color 0.2s ease; +} + +.oneos-shell-menu__btn.is-parent[aria-expanded='true'] .oneos-shell-menu__arrow { + color: var(--oneos-primary); +} + +.oneos-shell-menu__sub { + padding: 2px 0 6px 8px; + animation: oneos-shell-sub-in 0.22s var(--oneos-ease); +} + +@keyframes oneos-shell-sub-in { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.oneos-shell-menu__item--lv1 .oneos-shell-menu__btn { + min-height: 38px; + padding-left: 14px; + font-size: 13px; +} + +.oneos-shell-menu__item--lv2 .oneos-shell-menu__btn { + min-height: 36px; + padding-left: 18px; + font-size: 13px; +} + +.oneos-shell--collapsed .oneos-shell-brand__link { + justify-content: center; + padding: 6px; +} + +.oneos-shell--collapsed .oneos-shell-menu__btn { + justify-content: center; + padding-left: 0; + padding-right: 0; +} + +.oneos-shell--collapsed .oneos-shell-menu__btn::before { + display: none; +} + +.oneos-shell--collapsed .oneos-shell-menu__item { + margin-left: 8px; + margin-right: 8px; +} + +/* ——— Main / Chrome ——— */ +.oneos-shell-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.oneos-shell-chrome { + flex-shrink: 0; + background: color-mix(in srgb, var(--oneos-surface) 88%, transparent); + backdrop-filter: blur(14px) saturate(1.2); + -webkit-backdrop-filter: blur(14px) saturate(1.2); + border-bottom: 1px solid color-mix(in srgb, var(--oneos-border) 80%, transparent); + box-shadow: var(--oneos-shadow-chrome); + z-index: 30; +} + +.oneos-shell-header { + height: var(--oneos-header-h); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 14px 0 10px; +} + +.oneos-shell-header__left, +.oneos-shell-header__right { + display: flex; + align-items: center; + gap: 4px; + min-width: 0; +} + +.oneos-shell-header__right { + gap: 6px; +} + +.oneos-shell-text-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 36px; + padding: 0 10px; + border: 1px dashed color-mix(in srgb, var(--oneos-ink) 22%, transparent); + border-radius: 10px; + background: transparent; + color: color-mix(in srgb, var(--oneos-ink) 68%, transparent); + font-size: 0.75rem; + font-weight: 500; + line-height: 1.2; + white-space: nowrap; + cursor: pointer; + transition: + color 0.18s var(--oneos-ease), + border-color 0.18s var(--oneos-ease), + background 0.18s var(--oneos-ease); +} + +.oneos-shell-text-btn:hover { + color: var(--oneos-primary); + border-color: color-mix(in srgb, var(--oneos-primary) 55%, transparent); + background: color-mix(in srgb, var(--oneos-primary) 8%, transparent); +} + +.oneos-shell-text-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 40%, transparent); +} + +.oneos-shell-release-btn { + position: relative; + display: inline-grid; + place-items: center; + width: 36px; + height: 36px; + min-width: 36px; + border: none; + border-radius: 12px; + background: transparent; + color: var(--oneos-primary); + cursor: pointer; + transition: + transform 0.18s var(--oneos-ease), + box-shadow 0.18s var(--oneos-ease); +} + +.oneos-shell-release-btn__ring { + position: absolute; + inset: 2px; + border-radius: 11px; + background: + linear-gradient(135deg, color-mix(in srgb, var(--oneos-primary) 22%, #fff), color-mix(in srgb, #6366f1 18%, #fff)); + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff 70%, transparent), + 0 4px 12px color-mix(in srgb, var(--oneos-primary) 18%, transparent); + transition: opacity 0.18s var(--oneos-ease), transform 0.18s var(--oneos-ease); +} + +.oneos-shell-release-btn__icon { + position: relative; + z-index: 1; + display: grid; + place-items: center; + color: var(--oneos-primary); + filter: drop-shadow(0 1px 0 color-mix(in srgb, #fff 50%, transparent)); +} + +.oneos-shell-release-btn__dot { + position: absolute; + top: 5px; + right: 5px; + z-index: 2; + width: 8px; + height: 8px; + border-radius: 999px; + background: #f97316; + box-shadow: 0 0 0 2px #fff; +} + +.oneos-shell-release-btn:hover { + transform: translateY(-1px); +} + +.oneos-shell-release-btn:hover .oneos-shell-release-btn__ring { + transform: scale(1.04); + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff 80%, transparent), + 0 6px 16px color-mix(in srgb, var(--oneos-primary) 28%, transparent); +} + +.oneos-shell-release-btn:active { + transform: translateY(0) scale(0.96); +} + +.oneos-shell-release-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 40%, transparent); +} + +.oneos-shell-release-btn.has-unread .oneos-shell-release-btn__ring { + animation: oneos-release-pulse 2.4s var(--oneos-ease) infinite; +} + +@media (prefers-reduced-motion: reduce) { + .oneos-shell-text-btn, + .oneos-shell-release-btn, + .oneos-shell-release-btn__ring { + transition: none; + } + + .oneos-shell-release-btn.has-unread .oneos-shell-release-btn__ring { + animation: none; + } +} + +@keyframes oneos-release-pulse { + 0%, + 100% { + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff 70%, transparent), + 0 4px 12px color-mix(in srgb, var(--oneos-primary) 18%, transparent); + } + 50% { + box-shadow: + inset 0 1px 0 color-mix(in srgb, #fff 80%, transparent), + 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 12%, transparent), + 0 8px 18px color-mix(in srgb, var(--oneos-primary) 28%, transparent); + } +} + +.oneos-shell-icon-btn { + display: inline-grid; + place-items: center; + width: 36px; + height: 36px; + min-width: 36px; + border: none; + border-radius: 10px; + background: transparent; + color: color-mix(in srgb, var(--oneos-ink) 72%, transparent); + cursor: pointer; + transition: + background 0.18s var(--oneos-ease), + color 0.18s var(--oneos-ease), + transform 0.15s var(--oneos-ease), + box-shadow 0.18s var(--oneos-ease); +} + +.oneos-shell-icon-btn:hover { + background: color-mix(in srgb, var(--oneos-primary) 8%, #f1f5f9); + color: var(--oneos-ink); + transform: translateY(-1px); +} + +.oneos-shell-icon-btn:active { + transform: translateY(0) scale(0.96); +} + +.oneos-shell-icon-btn:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 40%, transparent); +} + +.oneos-shell-icon-btn--notify { + position: relative; +} + +.oneos-shell-icon-btn--notify::after { + display: none; +} + +.oneos-shell-icon-btn--notify.has-unread::after { + display: none; +} + +.oneos-shell-notify { + position: relative; + display: inline-flex; + align-items: center; +} + +.oneos-shell-notify__callout { + position: absolute; + top: calc(100% + 8px); + right: 0; + left: auto; + transform: none; + z-index: 30; + white-space: nowrap; + padding: 6px 10px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + color: #9a3412; + background: #fff7ed; + border: 1px solid #fdba74; + box-shadow: 0 4px 14px rgba(249, 115, 22, 0.18); + pointer-events: none; + animation: oneos-shell-notify-blink 1.6s ease-in-out infinite; +} + +.oneos-shell-notify__callout::after { + content: ''; + position: absolute; + right: 13px; + left: auto; + top: -5px; + width: 10px; + height: 10px; + transform: rotate(45deg); + background: #fff7ed; + border-left: 1px solid #fdba74; + border-top: 1px solid #fdba74; +} + +@keyframes oneos-shell-notify-blink { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.55; + } +} + +.oneos-shell-notify__badge { + position: absolute; + top: 4px; + right: 4px; + min-width: 16px; + height: 16px; + padding: 0 4px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + line-height: 16px; + text-align: center; + color: #fff; + background: #f97316; + box-shadow: 0 0 0 2px var(--oneos-surface); + font-variant-numeric: tabular-nums; +} + +.oneos-shell-notify__panel { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 40; + width: min(420px, calc(100vw - 32px)); + max-height: min(520px, 70vh); + display: flex; + flex-direction: column; + border-radius: 14px; + border: 1px solid color-mix(in srgb, var(--oneos-primary) 12%, #e2e8f0); + background: #fff; + box-shadow: 0 12px 40px rgba(15, 23, 42, 0.14); + overflow: hidden; +} + +.oneos-shell-notify__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border-bottom: 1px solid #e2e8f0; +} + +.oneos-shell-notify__title { + margin: 0; + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 700; + color: #0f172a; +} + +.oneos-shell-notify__tag { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + color: #9a3412; + background: #fff7ed; + border: 1px solid #fdba74; + animation: oneos-shell-notify-blink 1.6s ease-in-out infinite; + font-variant-numeric: tabular-nums; +} + +.oneos-shell-notify__link { + border: 0; + background: transparent; + color: var(--oneos-primary); + font-size: 12px; + font-weight: 600; + cursor: pointer; + padding: 4px 0; +} + +.oneos-shell-notify__link:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.oneos-shell-notify__link:focus-visible { + outline: 2px solid color-mix(in srgb, var(--oneos-primary) 45%, transparent); + outline-offset: 2px; +} + +.oneos-shell-notify__body { + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + padding: 8px; +} + +.oneos-shell-notify__list { + margin: 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: column; + gap: 8px; +} + +.oneos-shell-notify__item { + position: relative; + border-radius: 10px; + border: 1px solid #e2e8f0; + background: #f8fafc; + overflow: hidden; +} + +.oneos-shell-notify__item.is-unread { + background: #fff; + border-color: color-mix(in srgb, var(--oneos-primary) 22%, #e2e8f0); +} + +.oneos-shell-notify__item-main { + display: block; + width: 100%; + text-align: left; + border: 0; + background: transparent; + padding: 10px 12px; + cursor: pointer; + color: inherit; +} + +.oneos-shell-notify__item-main:hover { + background: color-mix(in srgb, var(--oneos-primary) 5%, #fff); +} + +.oneos-shell-notify__meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.oneos-shell-notify__dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: #cbd5e1; + flex-shrink: 0; +} + +.oneos-shell-notify__item.is-unread .oneos-shell-notify__dot { + background: #f97316; + box-shadow: 0 0 0 3px rgba(249, 115, 22, 0.18); +} + +.oneos-shell-notify__time { + font-size: 11px; + color: #64748b; + font-variant-numeric: tabular-nums; +} + +.oneos-shell-notify__chip { + display: inline-flex; + align-items: center; + padding: 1px 6px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + color: #334155; + background: #e2e8f0; +} + +.oneos-shell-notify__chip--urge { + color: #9a3412; + background: #ffedd5; +} + +.oneos-shell-notify__text { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: #0f172a; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.oneos-shell-notify__item.is-expanded .oneos-shell-notify__text { + -webkit-line-clamp: unset; +} + +.oneos-shell-notify__summary { + margin: 6px 0 0; + font-size: 12px; + line-height: 1.45; + color: #64748b; +} + +.oneos-shell-notify__handle { + position: absolute; + top: 10px; + right: 10px; + border: 0; + border-radius: 8px; + padding: 4px 10px; + font-size: 12px; + font-weight: 600; + color: #fff; + background: var(--oneos-primary); + cursor: pointer; +} + +.oneos-shell-notify__handle:hover { + filter: brightness(0.96); +} + +.oneos-shell-notify__handle:focus-visible { + outline: 2px solid color-mix(in srgb, var(--oneos-primary) 45%, transparent); + outline-offset: 2px; +} + +.oneos-shell-notify__item-main { + padding-right: 72px; +} + +/* 挂到 document.body,避开顶栏 backdrop-filter 对 fixed 的包含块影响 */ +.oneos-shell-notify__all-overlay { + position: fixed; + inset: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px 16px; + background: rgba(15, 23, 42, 0.45); + box-sizing: border-box; +} + +.oneos-shell-notify__all-modal { + width: min(560px, calc(100vw - 32px)); + max-height: min(720px, calc(100vh - 48px)); + display: flex; + flex-direction: column; + border-radius: 14px; + border: 1px solid color-mix(in srgb, var(--oneos-primary) 12%, #e2e8f0); + background: #fff; + box-shadow: 0 20px 56px rgba(15, 23, 42, 0.22); + overflow: hidden; +} + +.oneos-shell-notify__all-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px; + border-bottom: 1px solid #e2e8f0; +} + +.oneos-shell-notify__all-title { + margin: 0; + font-size: 16px; + font-weight: 700; + color: #0f172a; +} + +.oneos-shell-notify__all-close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 0; + border-radius: 8px; + background: transparent; + color: #64748b; + cursor: pointer; +} + +.oneos-shell-notify__all-close:hover { + background: #f1f5f9; + color: #0f172a; +} + +.oneos-shell-notify__all-close:focus-visible { + outline: 2px solid color-mix(in srgb, var(--oneos-primary) 45%, transparent); + outline-offset: 2px; +} + +.oneos-shell-notify__all-tabs { + display: flex; + gap: 4px; + padding: 10px 16px 0; + border-bottom: 1px solid #e2e8f0; +} + +.oneos-shell-notify__all-tab { + display: inline-flex; + align-items: center; + gap: 6px; + border: 0; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + background: transparent; + padding: 8px 12px; + font-size: 13px; + font-weight: 600; + color: #64748b; + cursor: pointer; + min-height: 40px; +} + +.oneos-shell-notify__all-tab:hover { + color: #0f172a; +} + +.oneos-shell-notify__all-tab.is-active { + color: var(--oneos-primary); + border-bottom-color: var(--oneos-primary); +} + +.oneos-shell-notify__all-tab:focus-visible { + outline: 2px solid color-mix(in srgb, var(--oneos-primary) 45%, transparent); + outline-offset: 2px; +} + +.oneos-shell-notify__all-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: #475569; + background: #e2e8f0; +} + +.oneos-shell-notify__all-tab.is-active .oneos-shell-notify__all-count { + color: #fff; + background: var(--oneos-primary); +} + +.oneos-shell-notify__all-body { + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + padding: 12px; +} + +.oneos-shell-notify__detail { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 50; + width: min(440px, calc(100vw - 32px)); + display: flex; + flex-direction: column; + border-radius: 14px; + border: 1px solid color-mix(in srgb, var(--oneos-primary) 12%, #e2e8f0); + background: #fff; + box-shadow: 0 12px 40px rgba(15, 23, 42, 0.16); + overflow: hidden; +} + +.oneos-shell-notify__detail-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + padding: 14px 16px; + border-bottom: 1px solid #e2e8f0; +} + +.oneos-shell-notify__detail-title { + margin: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 700; + color: #0f172a; + line-height: 1.45; +} + +.oneos-shell-notify__detail-name { + flex: 1 1 100%; + font-size: 15px; + font-weight: 700; + color: #0f172a; +} + +.oneos-shell-notify__detail-close { + flex-shrink: 0; + border: 0; + background: transparent; + color: #64748b; + font-size: 12px; + font-weight: 600; + cursor: pointer; + padding: 4px 0; +} + +.oneos-shell-notify__detail-close:hover { + color: #0f172a; +} + +.oneos-shell-notify__detail-close:focus-visible { + outline: 2px solid color-mix(in srgb, var(--oneos-primary) 45%, transparent); + outline-offset: 2px; +} + +.oneos-shell-notify__detail-body { + padding: 14px 16px; + display: flex; + flex-direction: column; + gap: 10px; + max-height: min(360px, 50vh); + overflow-y: auto; +} + +.oneos-shell-notify__detail-text { + margin: 0; + font-size: 14px; + line-height: 1.65; + color: #334155; + white-space: pre-wrap; +} + +.oneos-shell-notify__detail-summary { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: #64748b; +} + +.oneos-shell-notify__detail-foot { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid #e2e8f0; + background: #f8fafc; +} + +.oneos-shell-notify__detail-secondary, +.oneos-shell-notify__detail-primary { + border: 0; + border-radius: 8px; + padding: 8px 14px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + min-height: 36px; +} + +.oneos-shell-notify__detail-secondary { + color: #334155; + background: #e2e8f0; +} + +.oneos-shell-notify__detail-secondary:hover { + background: #cbd5e1; +} + +.oneos-shell-notify__detail-primary { + color: #fff; + background: var(--oneos-primary); +} + +.oneos-shell-notify__detail-primary:hover { + filter: brightness(0.96); +} + +.oneos-shell-notify__detail-secondary:focus-visible, +.oneos-shell-notify__detail-primary:focus-visible { + outline: 2px solid color-mix(in srgb, var(--oneos-primary) 45%, transparent); + outline-offset: 2px; +} + +.oneos-shell-notify__empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 36px 20px 32px; + text-align: center; + animation: oneos-shell-empty-in 220ms ease-out; +} + +.oneos-shell-notify__empty-visual { + position: relative; + width: 64px; + height: 64px; + margin-bottom: 2px; +} + +.oneos-shell-notify__empty-ring { + position: absolute; + inset: 0; + border-radius: 999px; + background: + radial-gradient(circle at 50% 40%, color-mix(in srgb, var(--oneos-primary) 14%, #fff) 0%, transparent 68%), + #f1f5f9; + border: 1px solid #e2e8f0; +} + +.oneos-shell-notify__empty-icon { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: #64748b; +} + +.oneos-shell-notify__empty-title { + margin: 0; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.01em; + color: #0f172a; +} + +.oneos-shell-notify__empty-desc { + margin: 0; + max-width: 220px; + font-size: 12px; + line-height: 1.65; + color: #64748b; +} + +@keyframes oneos-shell-empty-in { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .oneos-shell-notify__empty { + animation: none; + } +} + +@keyframes oneos-shell-pulse { + 0%, + 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.15); + opacity: 0.85; + } +} + +.oneos-shell-breadcrumb { + margin-left: 6px; + min-width: 0; +} + +.oneos-shell-breadcrumb ol { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0; + margin: 0; + padding: 0; + list-style: none; + font-size: 13px; + color: var(--oneos-muted); +} + +.oneos-shell-breadcrumb li { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.oneos-shell-breadcrumb__sep { + margin: 0 6px; + color: #cbd5e1; + font-weight: 400; +} + +.oneos-shell-breadcrumb .is-current { + color: var(--oneos-ink); + font-weight: 600; + letter-spacing: -0.01em; +} + +.oneos-shell-search { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + min-width: 180px; + padding: 0 12px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--oneos-border) 90%, transparent); + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); + color: var(--oneos-muted); + cursor: text; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9); + transition: + border-color 0.2s var(--oneos-ease), + box-shadow 0.2s var(--oneos-ease), + background 0.2s var(--oneos-ease); +} + +.oneos-shell-search:hover { + border-color: color-mix(in srgb, var(--oneos-primary) 28%, var(--oneos-border)); +} + +.oneos-shell-search:focus-within { + border-color: color-mix(in srgb, var(--oneos-primary) 55%, transparent); + box-shadow: + 0 0 0 3px color-mix(in srgb, var(--oneos-primary) 16%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + background: #fff; +} + +.oneos-shell-search input { + flex: 1; + min-width: 0; + border: none; + background: transparent; + font: inherit; + font-size: 13px; + color: var(--oneos-ink); + outline: none; +} + +.oneos-shell-search input::placeholder { + color: #94a3b8; +} + +.oneos-shell-search kbd { + font-family: inherit; + font-size: 11px; + font-weight: 500; + line-height: 1; + padding: 3px 6px; + border-radius: 6px; + border: 1px solid var(--oneos-border); + background: #fff; + color: #94a3b8; + box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04); +} + +.oneos-shell-avatar { + width: 36px; + height: 36px; + padding: 2px; + border: none; + border-radius: 999px; + overflow: hidden; + background: linear-gradient(135deg, var(--oneos-primary-soft), var(--oneos-primary)); + cursor: pointer; + transition: + transform 0.2s var(--oneos-ease), + box-shadow 0.2s var(--oneos-ease); +} + +.oneos-shell-avatar:hover { + transform: translateY(-1px) scale(1.04); + box-shadow: 0 4px 12px color-mix(in srgb, var(--oneos-primary) 35%, transparent); +} + +.oneos-shell-avatar:focus-visible { + outline: none; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px color-mix(in srgb, var(--oneos-primary) 40%, transparent); +} + +.oneos-shell-avatar img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 999px; + border: 2px solid #fff; +} + +/* ——— Tabs ——— */ +.oneos-shell-tabs { + display: flex; + align-items: flex-end; + gap: 4px; + min-height: var(--oneos-tabs-h); + padding: 0 14px; + background: linear-gradient(180deg, #f1f5f9 0%, #e8eef5 100%); + border-top: 1px solid color-mix(in srgb, var(--oneos-border) 70%, transparent); +} + +.oneos-shell-tab { + display: inline-flex; + align-items: center; + gap: 4px; + min-height: 32px; + padding: 0 4px 0 12px; + margin-bottom: 0; + border-radius: 10px 10px 0 0; + background: transparent; + color: var(--oneos-muted); + font-size: 13px; + transition: + background 0.18s var(--oneos-ease), + color 0.18s var(--oneos-ease), + box-shadow 0.18s var(--oneos-ease); +} + +.oneos-shell-tab:hover:not(.is-active) { + background: color-mix(in srgb, var(--oneos-surface) 55%, transparent); + color: var(--oneos-ink); +} + +.oneos-shell-tab.is-active { + background: var(--oneos-surface); + color: var(--oneos-ink); + font-weight: 600; + box-shadow: + 0 -1px 0 #fff, + 0 0 0 1px color-mix(in srgb, var(--oneos-border) 60%, transparent); + position: relative; +} + +.oneos-shell-tab.is-active::after { + content: ''; + position: absolute; + left: 12px; + right: 12px; + bottom: 0; + height: 2px; + border-radius: 2px 2px 0 0; + background: linear-gradient(90deg, var(--oneos-primary-soft), var(--oneos-primary)); +} + +.oneos-shell-tab__label { + border: none; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; + padding: 7px 4px; + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.oneos-shell-tab__close { + display: inline-grid; + place-items: center; + width: 18px; + height: 18px; + min-width: 18px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--oneos-muted); + cursor: pointer; + padding: 0; + opacity: 0.65; + transition: + background 0.15s ease, + color 0.15s ease, + opacity 0.15s ease; +} + +.oneos-shell-tab:hover .oneos-shell-tab__close, +.oneos-shell-tab.is-active .oneos-shell-tab__close { + opacity: 1; +} + +.oneos-shell-tab__close:hover { + background: color-mix(in srgb, var(--oneos-ink) 8%, transparent); + color: var(--oneos-ink); +} + +.oneos-shell-content { + flex: 1; + min-height: 0; + overflow: hidden; + background: var(--oneos-bg); + display: flex; + flex-direction: column; +} + +.oneos-shell-frame { + flex: 1; + width: 100%; + height: 100%; + min-height: 0; + border: 0; + background: #fff; +} + +.oneos-shell-welcome { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12px; + padding: 48px 24px; + text-align: center; + color: var(--oneos-ink); +} + +.oneos-shell-welcome h1 { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.02em; +} + +.oneos-shell-welcome p { + margin: 0; + max-width: 420px; + color: var(--oneos-muted); + line-height: 1.6; + font-size: 14px; +} + +.oneos-shell-content > .vm-page, +.oneos-shell-content > .wb-page { + min-height: 100%; + height: auto; + max-height: none; + overflow: auto; +} + +@media (max-width: 900px) { + .oneos-shell-search { + min-width: 0; + width: 36px; + padding: 0; + justify-content: center; + } + + .oneos-shell-search input, + .oneos-shell-search kbd { + display: none; + } + + .oneos-shell-breadcrumb { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .oneos-shell-aside, + .oneos-shell-menu__btn, + .oneos-shell-menu__icon, + .oneos-shell-menu__arrow, + .oneos-shell-brand__link, + .oneos-shell-brand__mark, + .oneos-shell-icon-btn, + .oneos-shell-avatar, + .oneos-shell-tab, + .oneos-shell-search, + .oneos-shell-menu__sub { + transition: none !important; + animation: none !important; + } + + .oneos-shell-icon-btn--notify::after { + animation: none; + } + + .oneos-shell-notify__callout, + .oneos-shell-notify__tag { + animation: none !important; + } +} diff --git a/src/common/oneos-app-shell/oneos-theme-content.css b/src/common/oneos-app-shell/oneos-theme-content.css new file mode 100644 index 0000000..b30f3ff --- /dev/null +++ b/src/common/oneos-app-shell/oneos-theme-content.css @@ -0,0 +1,163 @@ +/** + * OneOS 内容区暗色 token(参照原型导航 / VoltAgent 深色画布) + * 由 html[data-oneos-theme="dark"] 驱动;浅色保持各页原有 :root 定义。 + */ + +html[data-oneos-theme='dark'] { + color-scheme: dark; + + --ln-primary: #00d992; + --ln-primary-hover: #2fd6a1; + --ln-primary-focus: #10b981; + --ln-primary-active: #10b981; + --ln-primary-soft: rgba(0, 217, 146, 0.12); + --ln-primary-soft-strong: rgba(0, 217, 146, 0.2); + --ln-ink: #f2f2f2; + --ln-body: #bdbdbd; + --ln-body-strong: #f2f2f2; + --ln-muted: #8b949e; + --ln-muted-soft: #6b7280; + --ln-muted-80: #d1d5db; + --ln-divider-soft: #1a1a1a; + --ln-hairline: #3d3a39; + --ln-hairline-strong: #565656; + --ln-hairline-tertiary: #3d3a39; + --ln-canvas: #101010; + --ln-canvas-parchment: #101010; + --ln-surface-pearl: #1a1a1a; + --ln-surface-card: #1a1a1a; + --ln-surface-strong: #222222; + --ln-on-primary: #101010; + --ln-success: #00d992; + --ln-warning: #f59e0b; + --ln-error: #f87171; + --ln-brand-secure: #2fd6a1; + --ln-shadow-soft: none; + --ln-shadow-hover: none; + --ln-shadow-float: none; + --ln-shadow-modal: 0 20px 40px rgba(0, 0, 0, 0.45); + --ln-link: #00d992; + --ln-canvas-soft: #1a1a1a; + + --vm-primary: var(--ln-primary); + --vm-primary-dark: var(--ln-primary-focus); + --vm-primary-soft: var(--ln-primary-soft); + --vm-bg: var(--ln-canvas-parchment); + --vm-surface: var(--ln-surface-card); + --vm-border: var(--ln-hairline); + --vm-text: var(--ln-ink); + --vm-muted: var(--ln-muted); + --vm-danger: var(--ln-error); + --vm-warning: var(--ln-warning); + --vm-link: var(--ln-link); +} + +html[data-oneos-theme='dark'], +html[data-oneos-theme='dark'] body { + background-color: #101010 !important; + color: #f2f2f2; +} + +html[data-oneos-theme='dark'] #root { + background-color: #101010; + color: #f2f2f2; + min-height: 100%; +} + +/* 常见页面根容器 */ +html[data-oneos-theme='dark'] .vm-page, +html[data-oneos-theme='dark'] .wb-page, +html[data-oneos-theme='dark'] .page, +html[data-oneos-theme='dark'] .ant-layout, +html[data-oneos-theme='dark'] .ant-layout-content { + background: #101010 !important; + color: #f2f2f2; +} + +html[data-oneos-theme='dark'] .ant-card, +html[data-oneos-theme='dark'] .ant-table, +html[data-oneos-theme='dark'] .ant-table-container, +html[data-oneos-theme='dark'] .ant-table-thead > tr > th, +html[data-oneos-theme='dark'] .ant-modal-content, +html[data-oneos-theme='dark'] .ant-drawer-content, +html[data-oneos-theme='dark'] .ant-input, +html[data-oneos-theme='dark'] .ant-input-affix-wrapper, +html[data-oneos-theme='dark'] .ant-select-selector, +html[data-oneos-theme='dark'] .ant-picker { + background: #1a1a1a !important; + border-color: #3d3a39 !important; + color: #f2f2f2 !important; +} + +html[data-oneos-theme='dark'] .ant-table-thead > tr > th { + background: #161616 !important; + color: #bdbdbd !important; +} + +html[data-oneos-theme='dark'] .ant-table-tbody > tr > td { + border-color: #3d3a39 !important; + color: #f2f2f2 !important; +} + +html[data-oneos-theme='dark'] .ant-table-tbody > tr:hover > td { + background: #222222 !important; +} + +html[data-oneos-theme='dark'] .ant-btn-default { + background: #1a1a1a !important; + border-color: #3d3a39 !important; + color: #f2f2f2 !important; +} + +html[data-oneos-theme='dark'] .ant-btn-primary { + background: #00d992 !important; + border-color: #00d992 !important; + color: #101010 !important; +} + +/* 工作台 / 通用卡片:覆盖硬编码浅色渐变与表头 */ +html[data-oneos-theme='dark'] .wb-hero--welcome, +html[data-oneos-theme='dark'] .wb-hero--personal, +html[data-oneos-theme='dark'] .wb-panel, +html[data-oneos-theme='dark'] .wb-insights, +html[data-oneos-theme='dark'] .wb-section--flow, +html[data-oneos-theme='dark'] .wb-section--approval, +html[data-oneos-theme='dark'] .wb-kpi-tile, +html[data-oneos-theme='dark'] .wb-notice-card { + background: #1a1a1a !important; + box-shadow: none; + color: #f2f2f2; +} + +html[data-oneos-theme='dark'] .wb-kpi-chip { + background: #222222 !important; + box-shadow: none; + color: #f2f2f2; +} + +html[data-oneos-theme='dark'] .wb-panel-header, +html[data-oneos-theme='dark'] .wb-section-head, +html[data-oneos-theme='dark'] .wb-todo-table thead th, +html[data-oneos-theme='dark'] .wb-insight-table th, +html[data-oneos-theme='dark'] .wb-insight-metric { + background: #161616 !important; + color: #bdbdbd; +} + +html[data-oneos-theme='dark'] .wb-page--layout-personal { + background: #101010 !important; +} + +html[data-oneos-theme='dark'] .wb-todo-table tbody tr:hover td, +html[data-oneos-theme='dark'] .wb-kpi-chip:hover { + background: #222222 !important; +} + +html[data-oneos-theme='dark'] .wb-tabs, +html[data-oneos-theme='dark'] .wb-drawer, +html[data-oneos-theme='dark'] .wb-modal, +html[data-oneos-theme='dark'] .wb-overlay-panel { + background: #1a1a1a; + color: #f2f2f2; + border-color: #3d3a39; +} diff --git a/src/common/oneos-app-shell/release-demo-bridge.ts b/src/common/oneos-app-shell/release-demo-bridge.ts new file mode 100644 index 0000000..7ef51ba --- /dev/null +++ b/src/common/oneos-app-shell/release-demo-bridge.ts @@ -0,0 +1,64 @@ +/** 原型演示外壳 ↔ 工作台 iframe:版本更新日志 */ + +export const ONEOS_RELEASE_DEMO_ACTION = 'ONEOS_RELEASE_DEMO_ACTION'; + +export type ReleaseDemoAction = 'open' | 'reset'; + +export type ReleaseDemoActionPayload = { + type: typeof ONEOS_RELEASE_DEMO_ACTION; + action: ReleaseDemoAction; +}; + +export function isReleaseDemoAction(data: unknown): data is ReleaseDemoActionPayload { + if (!data || typeof data !== 'object') return false; + const payload = data as ReleaseDemoActionPayload; + return ( + payload.type === ONEOS_RELEASE_DEMO_ACTION && + (payload.action === 'open' || payload.action === 'reset') + ); +} + +export function postReleaseDemoAction(target: Window, action: ReleaseDemoAction) { + target.postMessage( + { type: ONEOS_RELEASE_DEMO_ACTION, action } satisfies ReleaseDemoActionPayload, + '*', + ); +} + +/** @deprecated 使用 postReleaseDemoAction(target, 'open') */ +export function postReleaseDemoReset(target: Window) { + postReleaseDemoAction(target, 'reset'); +} + +export function postReleaseDemoOpen(target: Window) { + postReleaseDemoAction(target, 'open'); +} + +/** 与工作台 `utils/release-seen.ts` 同源 key */ +export const RELEASE_SEEN_STORAGE_KEY = 'oneos-wb-release-seen-v1'; + +/** 当前发布版本(外壳「未读」点用;与 workbench data/release-notes 保持一致) */ +export const SHELL_CURRENT_RELEASE_VERSION = '1.1.5'; + +export function clearAllReleaseSeen(): void { + if (typeof window === 'undefined') return; + try { + window.localStorage.removeItem(RELEASE_SEEN_STORAGE_KEY); + } catch { + /* ignore */ + } +} + +export function hasUnseenRelease(operatorName?: string): boolean { + if (typeof window === 'undefined') return false; + try { + const raw = window.localStorage.getItem(RELEASE_SEEN_STORAGE_KEY); + if (!raw) return true; + const map = JSON.parse(raw) as Record; + if (!map || typeof map !== 'object') return true; + if (operatorName) return map[operatorName] !== SHELL_CURRENT_RELEASE_VERSION; + return !Object.values(map).includes(SHELL_CURRENT_RELEASE_VERSION); + } catch { + return true; + } +} diff --git a/src/common/oneos-app-shell/theme.ts b/src/common/oneos-app-shell/theme.ts new file mode 100644 index 0000000..c5cf3a9 --- /dev/null +++ b/src/common/oneos-app-shell/theme.ts @@ -0,0 +1,122 @@ +/** + * OneOS 浅色 / 暗色主题(localStorage + 跨 iframe postMessage) + * 暗色参照原型导航页(VoltAgent 深色画布)。 + * 不对「旧 ONEOS」目录下原型生效。 + */ + +import './oneos-theme-content.css'; + +export type OneOsTheme = 'light' | 'dark'; + +export const ONEOS_THEME_STORAGE_KEY = 'oneos-ui-theme'; +export const ONEOS_THEME_ATTR = 'data-oneos-theme'; +export const ONEOS_THEME_MESSAGE = 'ONEOS_THEME_CHANGE'; + +/** sidebar「旧ONEOS」目录下的原型,不接入主题切换 */ +export const LEGACY_ONEOS_PROTO_IDS = new Set([ + 'oneos-web-business', + 'oneos-web-data-analysis', + 'oneos-web-finance', + 'oneos-web-help-center', + 'oneos-web-lease-contract', + 'oneos-web-ledger-data', + 'oneos-web-ops', + 'oneos-web-procurement', +]); + +export function isLegacyOneOsPrototype(id?: string | null): boolean { + if (!id) return false; + return LEGACY_ONEOS_PROTO_IDS.has(id); +} + +export function readStoredOneOsTheme(): OneOsTheme { + if (typeof window === 'undefined') return 'light'; + try { + const v = window.localStorage.getItem(ONEOS_THEME_STORAGE_KEY); + if (v === 'dark' || v === 'light') return v; + } catch { + /* ignore */ + } + return 'light'; +} + +export function applyOneOsTheme(theme: OneOsTheme, root: HTMLElement = document.documentElement) { + root.setAttribute(ONEOS_THEME_ATTR, theme); + if (root === document.documentElement) { + document.documentElement.style.colorScheme = theme; + } +} + +export function persistOneOsTheme(theme: OneOsTheme) { + try { + window.localStorage.setItem(ONEOS_THEME_STORAGE_KEY, theme); + } catch { + /* ignore */ + } +} + +export function broadcastOneOsTheme(theme: OneOsTheme) { + if (typeof window === 'undefined') return; + window.dispatchEvent(new CustomEvent(ONEOS_THEME_MESSAGE, { detail: { theme } })); + try { + window.parent?.postMessage({ type: ONEOS_THEME_MESSAGE, theme }, '*'); + } catch { + /* ignore */ + } + const frames = document.querySelectorAll('iframe'); + frames.forEach((frame) => { + try { + frame.contentWindow?.postMessage({ type: ONEOS_THEME_MESSAGE, theme }, '*'); + } catch { + /* ignore */ + } + }); +} + +export function setOneOsTheme(theme: OneOsTheme) { + applyOneOsTheme(theme); + persistOneOsTheme(theme); + broadcastOneOsTheme(theme); +} + +export function toggleOneOsTheme(current?: OneOsTheme): OneOsTheme { + const next: OneOsTheme = (current ?? readStoredOneOsTheme()) === 'dark' ? 'light' : 'dark'; + setOneOsTheme(next); + return next; +} + +type BootstrapOptions = { + prototypeId?: string; + /** 强制跳过(如旧 ONEOS) */ + skip?: boolean; +}; + +/** + * 在原型预览入口调用:同步 localStorage / URL / 父页 postMessage。 + */ +export function bootstrapOneOsTheme(options: BootstrapOptions = {}) { + if (typeof window === 'undefined') return; + if (options.skip || isLegacyOneOsPrototype(options.prototypeId)) return; + + const fromQuery = new URLSearchParams(window.location.search).get('oneosTheme'); + const initial: OneOsTheme = + fromQuery === 'dark' || fromQuery === 'light' ? fromQuery : readStoredOneOsTheme(); + + applyOneOsTheme(initial); + persistOneOsTheme(initial); + + window.addEventListener('message', (event) => { + const data = event.data; + if (!data || data.type !== ONEOS_THEME_MESSAGE) return; + if (data.theme !== 'dark' && data.theme !== 'light') return; + applyOneOsTheme(data.theme); + persistOneOsTheme(data.theme); + }); + + window.addEventListener('storage', (event) => { + if (event.key !== ONEOS_THEME_STORAGE_KEY) return; + if (event.newValue === 'dark' || event.newValue === 'light') { + applyOneOsTheme(event.newValue); + } + }); +} diff --git a/src/common/selfOperatedLogisticsBridge.js b/src/common/selfOperatedLogisticsBridge.js new file mode 100644 index 0000000..2adff2c --- /dev/null +++ b/src/common/selfOperatedLogisticsBridge.js @@ -0,0 +1,567 @@ +/** + * 自营物流闭环桥:自营合同 ↔ 轻量调度任务 ↔ 车辆运营态 ↔ 物流业务明细自动生成 + * 原型本地种子 + localStorage,未接真实 API。 + * 规则见:src/resources/self-operated-logistics/assumptions.md + */ + +export const SO_CONTRACT_STORAGE_KEY = 'oneos.self-operated.contracts.v1'; +export const SO_DISPATCH_STORAGE_KEY = 'oneos.self-operated.dispatch-tasks.v1'; +export const SO_LEDGER_AUTO_STORAGE_KEY = 'oneos.self-operated.ledger-auto.v1'; +export const SO_VEHICLE_OPS_STORAGE_KEY = 'oneos.self-operated.vehicle-ops.v1'; + +export const CONTRACT_STATUS = { + draft: { key: 'draft', label: '草稿', color: 'default' }, + active: { key: 'active', label: '生效', color: 'processing' }, + performing: { key: 'performing', label: '履约中', color: 'success' }, + ended: { key: 'ended', label: '已结束', color: 'default' }, + terminated: { key: 'terminated', label: '已终止', color: 'error' }, +}; + +export const DISPATCH_STATUS = { + pending_assign: { key: 'pending_assign', label: '待派', color: 'default' }, + assigned: { key: 'assigned', label: '已派', color: 'processing' }, + in_progress: { key: 'in_progress', label: '执行中', color: 'warning' }, + completed: { key: 'completed', label: '已办结', color: 'success' }, + cancelled: { key: 'cancelled', label: '已取消', color: 'error' }, +}; + +/** inventory | self_operated */ +export const VEHICLE_OPS = { + inventory: 'inventory', + self_operated: 'self_operated', +}; + +export const SEED_VEHICLES = [ + { plateNo: '粤B·H2001', brandModel: '佛山飞驰·35T', opsStatus: VEHICLE_OPS.inventory }, + { plateNo: '粤B·H2002', brandModel: '佛山飞驰·35T', opsStatus: VEHICLE_OPS.self_operated }, + { plateNo: '粤B·H2003', brandModel: '未势能源·49T', opsStatus: VEHICLE_OPS.inventory }, + { plateNo: '粤B·H2008', brandModel: '未势能源·49T', opsStatus: VEHICLE_OPS.inventory }, +]; + +export const SEED_DRIVERS = [ + { name: '陈志强', phone: '13800138001' }, + { name: '李伟', phone: '13800138002' }, + { name: '王海涛', phone: '13800138003' }, +]; + +function nowStamp() { + const d = new Date(); + const p = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; +} + +function todayDate() { + return nowStamp().slice(0, 10); +} + +function readJson(key, fallback) { + try { + const raw = localStorage.getItem(key); + if (!raw) return fallback; + const parsed = JSON.parse(raw); + return parsed == null ? fallback : parsed; + } catch { + return fallback; + } +} + +function writeJson(key, value) { + localStorage.setItem(key, JSON.stringify(value)); + try { + window.dispatchEvent(new CustomEvent('oneos-self-operated-bridge', { detail: { key } })); + } catch { + /* ignore */ + } +} + +function roundMoney(value) { + return Math.round(Number(value || 0) * 100) / 100; +} + +function computeAmount(unitPrice, quantity) { + return roundMoney(Number(unitPrice || 0) * Number(quantity || 0)); +} + +function computeTotalCost(row) { + return roundMoney( + Number(row.hydrogenFee || 0) + + Number(row.manualReimbursement || 0) + + Number(row.etcFee || 0) + + Number(row.electricityFee || 0) + + Number(row.salary || 0) + + Number(row.vehicleFee || 0), + ); +} + +function brandModelOf(plateNo) { + const hit = SEED_VEHICLES.find((v) => v.plateNo === plateNo); + return hit ? hit.brandModel : ''; +} + +// —— 车辆运营态 —— + +export function loadVehicleOpsMap() { + const stored = readJson(SO_VEHICLE_OPS_STORAGE_KEY, null); + if (stored && typeof stored === 'object') return stored; + const map = {}; + SEED_VEHICLES.forEach((v) => { + map[v.plateNo] = v.opsStatus; + }); + writeJson(SO_VEHICLE_OPS_STORAGE_KEY, map); + return map; +} + +export function getVehicleOpsStatus(plateNo) { + const map = loadVehicleOpsMap(); + return map[plateNo] || VEHICLE_OPS.inventory; +} + +export function setVehicleOpsStatus(plateNo, status) { + const map = loadVehicleOpsMap(); + map[plateNo] = status; + writeJson(SO_VEHICLE_OPS_STORAGE_KEY, map); + return map; +} + +/** + * 模拟运维交车:库存 → 自营 + */ +export function simulateHandover(plateNo) { + const plate = String(plateNo || '').trim(); + if (!plate) return { ok: false, message: '请选择车牌' }; + const current = getVehicleOpsStatus(plate); + if (current === VEHICLE_OPS.self_operated) { + return { ok: true, message: `${plate} 已在自营运营态`, already: true }; + } + setVehicleOpsStatus(plate, VEHICLE_OPS.self_operated); + return { ok: true, message: `已模拟交车:${plate} → 自营运营态` }; +} + +/** + * 模拟运维还车:自营 → 库存;存在未办结任务时拦截 + */ +export function simulateReturn(plateNo) { + const plate = String(plateNo || '').trim(); + if (!plate) return { ok: false, message: '请选择车牌' }; + const open = loadDispatchTasks().filter( + (t) => t.plateNo === plate && !['completed', 'cancelled'].includes(t.status), + ); + if (open.length > 0) { + return { + ok: false, + message: `${plate} 仍有 ${open.length} 条未办结调度任务,无法还车`, + }; + } + setVehicleOpsStatus(plate, VEHICLE_OPS.inventory); + return { ok: true, message: `已模拟还车:${plate} → 库存` }; +} + +// —— 合同 —— + +function seedContracts() { + return [ + { + id: 'soc-seed-001', + contractCode: 'ZY-2026-0001', + customerName: '深圳城配物流有限公司', + businessName: '深圳南山短驳项目', + vehicleType: '35T 氢能重卡', + vehicleCount: 2, + serviceStart: '2026-07-01', + serviceEnd: '2026-12-31', + handoverDate: '2026-07-05', + handoverPlace: '深圳坪山运维基地', + pricingSummary: '线路计价 · 元/趟', + unitPriceDefault: 1800, + status: 'active', + attachmentName: '自营服务合同-南山短驳.pdf', + createdAt: '2026-06-28 10:00', + activatedAt: '2026-06-28 15:20', + remark: '种子合同:已生效,可创建调度任务', + }, + { + id: 'soc-seed-002', + contractCode: 'ZY-2026-0002', + customerName: '广州港务运输集团', + businessName: '南沙港区接驳', + vehicleType: '49T 氢能重卡', + vehicleCount: 1, + serviceStart: '2026-08-01', + serviceEnd: '2027-01-31', + handoverDate: '2026-08-03', + handoverPlace: '广州南沙停车场', + pricingSummary: '线路计价 · 元/趟', + unitPriceDefault: 2200, + status: 'draft', + attachmentName: '', + createdAt: '2026-07-10 09:30', + activatedAt: '', + remark: '草稿:需业务确认生效', + }, + ]; +} + +export function loadContracts() { + const stored = readJson(SO_CONTRACT_STORAGE_KEY, null); + if (Array.isArray(stored) && stored.length > 0) return stored; + const seed = seedContracts(); + writeJson(SO_CONTRACT_STORAGE_KEY, seed); + return seed; +} + +export function saveContracts(list) { + writeJson(SO_CONTRACT_STORAGE_KEY, list || []); +} + +export function getContractById(id) { + return loadContracts().find((c) => c.id === id) || null; +} + +export function getActiveContracts() { + return loadContracts().filter((c) => c.status === 'active' || c.status === 'performing'); +} + +export function createContract(payload) { + const list = loadContracts(); + const n = list.length + 1; + const row = { + id: `soc-${Date.now()}`, + contractCode: payload.contractCode || `ZY-2026-${String(n).padStart(4, '0')}`, + customerName: String(payload.customerName || '').trim(), + businessName: String(payload.businessName || '').trim(), + vehicleType: String(payload.vehicleType || '').trim(), + vehicleCount: Number(payload.vehicleCount) || 1, + serviceStart: payload.serviceStart || '', + serviceEnd: payload.serviceEnd || '', + handoverDate: payload.handoverDate || '', + handoverPlace: payload.handoverPlace || '', + pricingSummary: payload.pricingSummary || '线路计价', + unitPriceDefault: Number(payload.unitPriceDefault) || 0, + status: 'draft', + attachmentName: payload.attachmentName || '', + createdAt: nowStamp(), + activatedAt: '', + remark: payload.remark || '', + }; + list.unshift(row); + saveContracts(list); + return row; +} + +/** + * 草稿 → 生效;进入可派车;标记可发起交车(原型 toast/handoverHint) + */ +export function activateContract(contractId) { + const list = loadContracts(); + const idx = list.findIndex((c) => c.id === contractId); + if (idx < 0) return { ok: false, message: '合同不存在' }; + const c = list[idx]; + if (c.status !== 'draft') { + return { ok: false, message: `当前状态「${CONTRACT_STATUS[c.status]?.label || c.status}」不可生效` }; + } + list[idx] = { + ...c, + status: 'active', + activatedAt: nowStamp(), + }; + saveContracts(list); + return { + ok: true, + contract: list[idx], + message: `合同 ${c.contractCode} 已生效,可创建调度任务;交车请在调度侧「模拟交车」或运维交车办理`, + handoverHint: { + contractCode: c.contractCode, + handoverDate: c.handoverDate, + handoverPlace: c.handoverPlace, + vehicleCount: c.vehicleCount, + }, + }; +} + +export function endContract(contractId) { + const list = loadContracts(); + const idx = list.findIndex((c) => c.id === contractId); + if (idx < 0) return { ok: false, message: '合同不存在' }; + const open = loadDispatchTasks().filter( + (t) => t.contractId === contractId && !['completed', 'cancelled'].includes(t.status), + ); + if (open.length > 0) { + return { ok: false, message: `仍有 ${open.length} 条未办结调度任务,无法结束合同` }; + } + list[idx] = { ...list[idx], status: 'ended' }; + saveContracts(list); + return { ok: true, message: `合同 ${list[idx].contractCode} 已结束` }; +} + +// —— 调度任务 —— + +function seedDispatchTasks() { + return [ + { + id: 'sdt-seed-001', + taskCode: 'DD-2026-0001', + contractId: 'soc-seed-001', + contractCode: 'ZY-2026-0001', + businessName: '深圳南山短驳项目', + customerName: '深圳城配物流有限公司', + planDate: '2026-07-18', + routeDesc: '坪山基地 → 南山仓 A', + multiTrip: '否', + routePricing: '线路计价', + unitPrice: 1800, + quantity: 1, + plateNo: '粤B·H2002', + driver: '陈志强', + phone: '13800138001', + status: 'assigned', + salary: 350, + remark: '种子任务:车辆已在自营态,可直接办结演示自动台账', + createdAt: '2026-07-15 11:00', + assignedAt: '2026-07-15 11:05', + completedAt: '', + ledgerRowId: '', + }, + ]; +} + +export function loadDispatchTasks() { + const stored = readJson(SO_DISPATCH_STORAGE_KEY, null); + if (Array.isArray(stored) && stored.length > 0) return stored; + const seed = seedDispatchTasks(); + writeJson(SO_DISPATCH_STORAGE_KEY, seed); + return seed; +} + +export function saveDispatchTasks(list) { + writeJson(SO_DISPATCH_STORAGE_KEY, list || []); +} + +export function createDispatchTask(payload) { + const contract = getContractById(payload.contractId); + if (!contract) return { ok: false, message: '请选择自营合同' }; + if (!['active', 'performing'].includes(contract.status)) { + return { ok: false, message: '仅生效/履约中的合同可创建调度任务' }; + } + const list = loadDispatchTasks(); + const n = list.length + 1; + const plateNo = String(payload.plateNo || '').trim(); + const hasPlate = Boolean(plateNo); + const row = { + id: `sdt-${Date.now()}`, + taskCode: `DD-2026-${String(n).padStart(4, '0')}`, + contractId: contract.id, + contractCode: contract.contractCode, + businessName: contract.businessName, + customerName: contract.customerName, + planDate: payload.planDate || todayDate(), + routeDesc: String(payload.routeDesc || '').trim(), + multiTrip: payload.multiTrip === '是' ? '是' : '否', + routePricing: payload.routePricing || contract.pricingSummary || '线路计价', + unitPrice: Number(payload.unitPrice ?? contract.unitPriceDefault) || 0, + quantity: Number(payload.quantity) || 1, + plateNo, + driver: String(payload.driver || '').trim(), + phone: String(payload.phone || '').trim(), + status: hasPlate ? 'assigned' : 'pending_assign', + salary: Number(payload.salary) || 0, + remark: String(payload.remark || '').trim(), + createdAt: nowStamp(), + assignedAt: hasPlate ? nowStamp() : '', + completedAt: '', + ledgerRowId: '', + }; + list.unshift(row); + saveDispatchTasks(list); + if (contract.status === 'active') { + const contracts = loadContracts(); + const cidx = contracts.findIndex((c) => c.id === contract.id); + if (cidx >= 0) { + contracts[cidx] = { ...contracts[cidx], status: 'performing' }; + saveContracts(contracts); + } + } + return { ok: true, task: row, message: `已创建调度任务 ${row.taskCode}` }; +} + +export function assignDispatchTask(taskId, payload) { + const list = loadDispatchTasks(); + const idx = list.findIndex((t) => t.id === taskId); + if (idx < 0) return { ok: false, message: '任务不存在' }; + const t = list[idx]; + if (['completed', 'cancelled'].includes(t.status)) { + return { ok: false, message: '已办结/已取消任务不可改派' }; + } + const plateNo = String(payload.plateNo || '').trim(); + if (!plateNo) return { ok: false, message: '请选择车牌' }; + list[idx] = { + ...t, + plateNo, + driver: String(payload.driver || t.driver || '').trim(), + phone: String(payload.phone || t.phone || '').trim(), + status: 'assigned', + assignedAt: nowStamp(), + }; + saveDispatchTasks(list); + return { ok: true, task: list[idx], message: `已派车 ${plateNo}` }; +} + +export function startDispatchTask(taskId) { + const list = loadDispatchTasks(); + const idx = list.findIndex((t) => t.id === taskId); + if (idx < 0) return { ok: false, message: '任务不存在' }; + const t = list[idx]; + if (t.status !== 'assigned') { + return { ok: false, message: '仅「已派」任务可开始执行' }; + } + if (!t.plateNo) return { ok: false, message: '请先派车' }; + list[idx] = { ...t, status: 'in_progress' }; + saveDispatchTasks(list); + return { ok: true, task: list[idx], message: '任务已进入执行中' }; +} + +export function cancelDispatchTask(taskId) { + const list = loadDispatchTasks(); + const idx = list.findIndex((t) => t.id === taskId); + if (idx < 0) return { ok: false, message: '任务不存在' }; + const t = list[idx]; + if (t.status === 'completed') { + return { ok: false, message: '已办结任务不可取消(台账行已生成)' }; + } + if (t.status === 'cancelled') return { ok: true, task: t, message: '任务已是取消状态' }; + list[idx] = { ...t, status: 'cancelled' }; + saveDispatchTasks(list); + return { ok: true, task: list[idx], message: `已取消 ${t.taskCode}` }; +} + +/** + * 办结 → 自动生成台账行(严格:车辆须已交车) + */ +export function completeDispatchTask(taskId, confirmPayload) { + const list = loadDispatchTasks(); + const idx = list.findIndex((t) => t.id === taskId); + if (idx < 0) return { ok: false, message: '任务不存在' }; + const t = list[idx]; + if (t.status === 'completed') { + return { ok: false, message: '任务已办结', ledgerRowId: t.ledgerRowId }; + } + if (t.status === 'cancelled') { + return { ok: false, message: '已取消任务不可办结' }; + } + if (!t.plateNo) return { ok: false, message: '请先派车再办结' }; + + const ops = getVehicleOpsStatus(t.plateNo); + if (ops !== VEHICLE_OPS.self_operated) { + return { + ok: false, + code: 'NEED_HANDOVER', + message: `${t.plateNo} 尚未交车(当前非自营运营态),请先「模拟交车」或完成运维交车后再办结`, + }; + } + + const dispatchDate = (confirmPayload && confirmPayload.dispatchDate) || t.planDate || todayDate(); + const quantity = Number(confirmPayload?.quantity ?? t.quantity) || 1; + const unitPrice = Number(confirmPayload?.unitPrice ?? t.unitPrice) || 0; + const salary = Number(confirmPayload?.salary ?? t.salary) || 0; + const remark = String(confirmPayload?.remark ?? t.remark ?? '').trim(); + + const year = dispatchDate.slice(0, 4); + const month = String(Number(dispatchDate.slice(5, 7)) || 1); + const amount = computeAmount(unitPrice, quantity); + const ledgerDraft = { + id: `auto-${t.id}`, + year, + month, + dispatchDate, + businessName: t.businessName, + driver: t.driver, + phone: t.phone, + plateNo: t.plateNo, + systemVehicleType: brandModelOf(t.plateNo), + unitPrice, + quantity, + amount, + hydrogenFee: 0, + etcFee: 0, + salary, + electricityFee: 0, + manualReimbursement: 0, + dailySocialSecurityFee: 0, + dailyTrailerServiceFee: 0, + dailyTrailerFee: 0, + dailyParkingFee: 0, + dailyTireFee: 0, + vehicleFee: 0, + totalCost: 0, + profitLoss: 0, + multiTrip: t.multiTrip || '否', + routePricing: t.routePricing || '线路计价', + remark: remark ? `[调度自动·${t.taskCode}] ${remark}` : `[调度自动·${t.taskCode}]`, + rowSource: 'auto', + dispatchTaskId: t.id, + dispatchTaskCode: t.taskCode, + contractCode: t.contractCode, + }; + ledgerDraft.totalCost = computeTotalCost(ledgerDraft); + ledgerDraft.profitLoss = roundMoney(amount - ledgerDraft.totalCost); + + const autoRows = loadAutoLedgerRows(); + const withoutDup = autoRows.filter((r) => r.dispatchTaskId !== t.id && r.id !== ledgerDraft.id); + withoutDup.unshift(ledgerDraft); + saveAutoLedgerRows(withoutDup); + + list[idx] = { + ...t, + status: 'completed', + quantity, + unitPrice, + salary, + remark, + completedAt: nowStamp(), + ledgerRowId: ledgerDraft.id, + }; + saveDispatchTasks(list); + + return { + ok: true, + task: list[idx], + ledgerRow: ledgerDraft, + message: `已办结 ${t.taskCode},并自动生成物流业务明细 1 行`, + }; +} + +// —— 台账自动行 —— + +export function loadAutoLedgerRows() { + const stored = readJson(SO_LEDGER_AUTO_STORAGE_KEY, null); + return Array.isArray(stored) ? stored : []; +} + +export function saveAutoLedgerRows(list) { + writeJson(SO_LEDGER_AUTO_STORAGE_KEY, list || []); +} + +export function removeAutoLedgerRow(rowId) { + const next = loadAutoLedgerRows().filter((r) => r.id !== rowId); + saveAutoLedgerRows(next); + return next; +} + +/** + * 合并种子/当前列表与自动生成行:自动行按 id 覆盖同 id,其余追加到顶部 + */ +export function mergeLedgerWithAutoRows(baseRows) { + const auto = loadAutoLedgerRows(); + const autoIds = new Set(auto.map((r) => r.id)); + const rest = (baseRows || []).filter((r) => !autoIds.has(r.id)); + return [...auto, ...rest]; +} + +export function subscribeSelfOperatedBridge(handler) { + const fn = (e) => handler(e?.detail); + window.addEventListener('oneos-self-operated-bridge', fn); + window.addEventListener('storage', fn); + return () => { + window.removeEventListener('oneos-self-operated-bridge', fn); + window.removeEventListener('storage', fn); + }; +} diff --git a/src/prototypes/business-dept-ledger/.spec/requirements-prd.md b/src/prototypes/business-dept-ledger/.spec/requirements-prd.md new file mode 100644 index 0000000..c86cf9f --- /dev/null +++ b/src/prototypes/business-dept-ledger/.spec/requirements-prd.md @@ -0,0 +1,40 @@ +# 业务部台账 · 产品需求说明(PRD) + +> 原型路径:`/prototypes/business-dept-ledger` +> 实现页:`src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx` + +--- + +## 1. 模块定位 + +| 项 | 说明 | +|---|---| +| 目标用户 | 业务部业管 / 运营查看汇总 | +| 核心任务 | 按业务条线汇总查看业绩、成本、盈亏,并支持租赁/氢费下钻 | +| 内容来源 | 各业务明细模块按月汇总(原型本地种子,未接真实 API) | + +--- + +## 2. 各业务条线数据统计来源(摘要) + +主表分组表头在业务名称后方标注最小颗粒度来源;悬停可查看业绩/成本/盈亏字段口径。 + +| 业务条线 | 最小颗粒度 | 业绩 | 成本 | 盈亏 | +|---|---|---|---|---| +| 物流业务 | 物流业务明细 | 「金额」列 | 「总成本」列 | 金额 − 总成本 | +| 租赁业务 | 租赁业务明细 | 「实收金额」列 | 「车辆实际成本」列 | 实收金额 − 车辆实际成本 | +| 氢费业务 | 车辆氢费明细 | 「加氢总价(元)」列 | 「成本总价(元)」列 | 加氢总价 − 成本总价 | +| 电费业务 | 充电订单 | 「对客费用(元)」列 | 「成本费用(元)」列 | 对客费用 − 成本费用 | +| ETC业务 | ETC业务 | 字段口径待补充 | 字段口径待补充 | 字段口径待补充 | + +完整判定、前置条件与代码映射见 [sector-data-source.md](./sector-data-source.md)。 + +--- + +## 3. 验收重点 + +- [ ] 主表五条业务线后方分别显示:`物流业务明细` / `租赁业务明细` / `车辆氢费明细` / `充电订单` / `ETC业务`(不再显示「预留明细」) +- [ ] 悬停业务名称或来源标注,可看到该条线最小颗粒度与业绩/成本/盈亏口径 +- [ ] 悬停「业绩」「成本」「盈亏」列头,提示对应明细字段 +- [ ] 电费来源为「充电订单」;ETC 来源为「ETC业务」 +- [ ] 文档与页面口径一致:`.spec/sector-data-source.md`、本 PRD diff --git a/src/prototypes/business-dept-ledger/.spec/sector-data-source.md b/src/prototypes/business-dept-ledger/.spec/sector-data-source.md new file mode 100644 index 0000000..46ec61d --- /dev/null +++ b/src/prototypes/business-dept-ledger/.spec/sector-data-source.md @@ -0,0 +1,60 @@ +# 业务部台账 · 各业务条线数据统计来源 + +> 实现:`src/prototypes/oneos-web-data-analysis/pages/05-业务部台账.jsx` +> 常量:`SECTOR_DATA_SOURCE`、`SECTOR_METRIC_SOURCE` +> UI:主表分组表头「业务名称 ← 明细来源」;业绩/成本/盈亏列悬停展示字段口径 + +## 对照数据源 + +| 业务条线 | 最小颗粒度数据来源 | 原型本地种子 | 真实 API | +|---|---|---|---| +| 物流业务 | 物流业务明细 | 是(汇总种子) | 未接 | +| 租赁业务 | 租赁业务明细 | 是(汇总种子) | 未接 | +| 氢费业务 | 车辆氢费明细 | 是(汇总种子) | 未接 | +| 电费业务 | 充电订单 | 是(汇总种子) | 未接 | +| ETC业务 | ETC业务 | 是(汇总种子) | 未接 | + +主表当前展示的是**按月(或客户/事业部)汇总后的结果**;上表「最小颗粒度」指最终应回溯到的明细模块。 + +## 业绩 / 成本 / 盈亏口径(判定顺序) + +按业务条线取数;同一条线内先取业绩、成本字段,再计算盈亏。 + +| 优先级 | 指标 | 规则 | +|---|---|---| +| 1 | 业绩 | 取对应明细模块指定金额列(见下表) | +| 2 | 成本 | 取对应明细模块指定成本列(见下表) | +| 3 | 盈亏 | 业绩 − 成本(见下表公式) | + +### 分条线字段映射 + +| 业务条线 | 业绩来源列 | 成本来源列 | 盈亏公式 | +|---|---|---|---| +| 物流业务 | 物流业务明细「金额」 | 物流业务明细「总成本」 | 金额 − 总成本 | +| 租赁业务 | 租赁业务明细「实收金额」 | 租赁业务明细「车辆实际成本」 | 实收金额 − 车辆实际成本 | +| 氢费业务 | 车辆氢费明细「加氢总价(元)」 | 车辆氢费明细「成本总价(元)」 | 加氢总价(元)− 成本总价(元) | +| 电费业务 | 充电订单「对客费用(元)」 | 充电订单「成本费用(元)」 | 对客费用(元)− 成本费用(元) | +| ETC业务 | ETC业务(字段口径待补充) | ETC业务(字段口径待补充) | ETC业务(字段口径待补充) | + +## 前置条件 + +- 仅在业务部台账主表分组列展示来源标注;钻取子页「数据来源」仍使用 `SECTOR_DATA_SOURCE` 短名。 +- ETC 业务本期仅确认最小颗粒度模块为「ETC业务」,业绩/成本/盈亏字段列名待业务补充后更新本规格与代码常量。 + +## 用户可见结果 + +| 位置 | 展示 | +|---|---| +| 分组表头业务名称后方 | `←物流业务明细` / `←租赁业务明细` / `←车辆氢费明细` / `←充电订单` / `←ETC业务` | +| 悬停业务名称或来源标注 | 最小颗粒度 + 业绩/成本/盈亏口径全文 | +| 悬停「业绩」「成本」「盈亏」列头 | 该列对应字段口径 | + +## 与代码映射 + +| 常量 / 函数 | 作用 | +|---|---| +| `SECTOR_DATA_SOURCE` | 表头后方短名;钻取页数据来源短名 | +| `SECTOR_METRIC_SOURCE` | 业绩/成本/盈亏字段口径 | +| `buildSectorSourceTooltip` | 分组表头悬停全文 | +| `renderSectorMetricTitle` | 指标列头悬停 | +| `renderSectorGroupTitle` | 渲染「业务名 ← 来源」 | diff --git a/src/prototypes/business-dept-ledger/annotation-source.json b/src/prototypes/business-dept-ledger/annotation-source.json new file mode 100644 index 0000000..7db424d --- /dev/null +++ b/src/prototypes/business-dept-ledger/annotation-source.json @@ -0,0 +1,69 @@ +{ + "documentVersion": 1, + "format": "axhub-annotation-source", + "data": { + "version": 2, + "prototypeName": "business-dept-ledger", + "pageId": "main", + "updatedAt": 1783938000000, + "nodes": [ + { + "id": "bdl-list-table", + "index": 1, + "title": "主表 · 业务条线数据来源", + "pageId": "main", + "locator": { + "selectors": [ + "[data-annotation-id=\"bdl-list-table\"]" + ], + "fingerprint": "bdl-list-table", + "path": [] + }, + "aiPrompt": "业务部台账主表各业务条线业绩/成本/盈亏数据来源口径。", + "annotationText": "## 各业务条线数据统计来源\n\n分组表头业务名称后方标注最小颗粒度来源;悬停查看业绩/成本/盈亏字段口径。\n\n| 业务条线 | 最小颗粒度 | 业绩 | 成本 | 盈亏 |\n|---|---|---|---|---|\n| 物流业务 | 物流业务明细 | 金额 | 总成本 | 金额 − 总成本 |\n| 租赁业务 | 租赁业务明细 | 实收金额 | 车辆实际成本 | 实收金额 − 车辆实际成本 |\n| 氢费业务 | 车辆氢费明细 | 加氢总价(元) | 成本总价(元) | 加氢总价 − 成本总价 |\n| 电费业务 | 充电订单 | 对客费用(元) | 成本费用(元) | 对客费用 − 成本费用 |\n| ETC业务 | ETC业务 | 待补充 | 待补充 | 待补充 |\n\n完整规格见 `.spec/sector-data-source.md`。原型本地种子,未接真实 API。", + "hasMarkdown": true, + "color": "#2563eb", + "images": [], + "createdAt": 1783938000000, + "updatedAt": 1783938000000 + } + ], + "notes": { + "bdl-list-table": "## 各业务条线数据统计来源\n\n分组表头业务名称后方标注最小颗粒度来源;悬停查看业绩/成本/盈亏字段口径。\n\n| 业务条线 | 最小颗粒度 | 业绩 | 成本 | 盈亏 |\n|---|---|---|---|---|\n| 物流业务 | 物流业务明细 | 金额 | 总成本 | 金额 − 总成本 |\n| 租赁业务 | 租赁业务明细 | 实收金额 | 车辆实际成本 | 实收金额 − 车辆实际成本 |\n| 氢费业务 | 车辆氢费明细 | 加氢总价(元) | 成本总价(元) | 加氢总价 − 成本总价 |\n| 电费业务 | 充电订单 | 对客费用(元) | 成本费用(元) | 对客费用 − 成本费用 |\n| ETC业务 | ETC业务 | 待补充 | 待补充 | 待补充 |\n\n完整规格见 `.spec/sector-data-source.md`。原型本地种子,未接真实 API。", + "bdl-doc-sector-data-source": "# 业务部台账 · 各业务条线数据统计来源\n\n见 `.spec/sector-data-source.md`。" + }, + "directory": [ + { + "type": "group", + "id": "bdl-dir-logic", + "title": "业务逻辑规格", + "children": [ + { + "type": "note", + "id": "bdl-doc-sector-data-source", + "title": "各业务条线数据统计来源", + "markdownPath": ".spec/sector-data-source.md" + }, + { + "type": "note", + "id": "bdl-doc-prd", + "title": "产品需求说明(PRD)", + "markdownPath": ".spec/requirements-prd.md" + } + ] + }, + { + "type": "group", + "id": "bdl-dir-page", + "title": "页面", + "children": [ + { + "type": "annotation", + "id": "bdl-list-table", + "title": "主表 · 业务条线数据来源" + } + ] + } + ] + } +} diff --git a/src/prototypes/insurance-procurement/.spec/requirements-prd.md b/src/prototypes/insurance-procurement/.spec/requirements-prd.md new file mode 100644 index 0000000..fe65ade --- /dev/null +++ b/src/prototypes/insurance-procurement/.spec/requirements-prd.md @@ -0,0 +1,376 @@ +# 保险采购 · 产品需求说明(全模块) + +| 项 | 内容 | +|---|---| +| 模块名称 | 保险采购 | +| 所属系统 | ONE-OS(PC) | +| 交互原型 | `/prototypes/insurance-procurement` | +| 业务条线 | 运维管理条线(证照 / 保险相关) | +| 文档状态 | AutoPRD · 已对齐可运行原型 | + +--- + +## 1. 一句话与目标 + +**一句话** +在同一模块内管理车队**保单台账**与**采购前比价审批**两条业务线,保证续保不遗漏、变更可追溯,并为交车提供交强险 + 商业险合规校验。 + +**要解决的问题** +- 保险信息散落在表格、邮件与线下批单中,台账不全、临期易漏 +- 停保 / 复驶 / 退保缺少统一留痕 +- 采购前多方比价难追溯,审批结果难跟踪 +- 交车时无法与车辆管理共用同一套「保险是否有效」口径 + +**本期目标** +1. 一车一档维护五类险种台账;支持手工新增、批量识别、批量导入 +2. 支持停保 / 复驶 / 退保办理与操作历史 +3. 输出与车辆管理一致的车辆级保险状态,支撑交车拦截 +4. 比价单批次管理:多方报价、最晚付费预警、勾选提交采购审批(本页只读跟踪) + +**非目标(本期不做)** +- 比价单与保单台账**自动双向同步**;审批通过后**不自动写入**正式保单 +- 在本模块内办理审批(通过 / 驳回 / 撤回均在审批中心) +- 维护行驶证、道路运输证等证照档案(见证照管理) +- 事故赔付、保险上浮等事故链路台账(见事故管理) + +--- + +## 2. 模块边界(最重要) + +```mermaid +flowchart LR + IPC[保险采购] + VM[车辆管理] + OPS[交车管理] + APPR[审批中心] + CERT[证照管理] + WB[工作台] + IPC -->|输出保险状态口径| VM + VM -->|交车前置校验| OPS + CERT -.->|并列交车前置| OPS + IPC -->|发起采购审批| APPR + APPR -->|回写采购状态| IPC + WB -->|临期 KPI / 快捷入口| IPC +``` + +| 业务线 / 子域 | 做什么 | 不做什么 | +|---------------|--------|----------| +| 保单管理 | 一车一档、多通道录入、停保/复驶/退保、状态与交车口径 | 不替代证照管理;不办事故赔付 | +| 比价单 | 批次比价、付费预警、提交采购、只读跟踪审批 | 不自动生成正式保单;不在本页审批 | +| 与车辆 / 交车 | 提供交强 + 商业是否有效的统一口径 | 不在本页完成交车动作 | + +**外部依赖(产品口径)** + +| 依赖方 | 交互方式 | 产品要求 | +|--------|----------|----------| +| 车辆管理 | 读取保险状态 / 到期日 | 口径与本模块一致:异常则禁止交车 | +| 交车管理 | 交付前校验交强 + 商业有效 | 不合规须可理解拦截原因 | +| 审批中心 | 接收采购申请并回写状态 / 当前审批人 | 本页只读展示,不办理审批 | +| 工作台 | KPI、快捷「发起保险采购」 | 跳转至本模块对应能力 | +| 证照管理 | 并列交车前置 | 证照与保险各自独立维护 | + +**关键约束** +- **保单台账 ↔ 比价单互不自动同步**;审批通过后须运营在保单管理侧另行录入正式保单 +- **交车只看交强险 + 商业险**;超赔 / 货物 / 驾意仅参与临期预警,不参与交车判定 +- **最晚付费临期 / 超期**仅服务比价采购节奏,不写入保单台账 + +--- + +## 3. 用户与角色 + +| 角色 | 主要目标 | +|------|----------| +| 运维部 | 维护保单台账、办理停保/复驶/退保;交车前确认保险有效 | +| 安全部 | 从合规视角关注保险与证照并列的交车前置 | +| 采购部 | 创建比价单、录入报价、提交采购申请;关注临期与付费节奏 | +| 业务管理组(业管) | 在审批中心确认报价等节点(本页只读看到结果) | + +> 部门名对齐业务条线说明;`module-role-mapping` 将本模块主责记为运维部、安全部,采购部为比价线主操作用户。 + +--- + +## 4. 用户故事与故事点(业务条线说明口径) + +> 故事点供排期参考,可按团队基准调整。合计约 **28 SP**。 + +### Epic A · 保单台账与交车合规(约 15 SP) + +#### A1 · 一车一档台账与检索(约 3 SP) +- **角色**:运维部、采购部 +- **起点**:需要按车牌 / VIN / 运营状态 / 保险状态等快速定位车辆保险档案。 +- **怎么运作**: + 1. 在列表筛选或点击 KPI 卡片缩小范围。 + 2. 查看五类险种到期日与车辆级保险状态。 + 3. 进入「管理」打开车辆保险档案。 +- **关键结果**:`可按条件定位车辆` / `空态提示无可匹配台账` +- **闭环**:台账可检索、可下钻,与 KPI 口径一致。 +- **排期(可选)**:US-IPC-01 · 规模 M · 作为运维/采购,我想快速筛出台账车辆,以便处理续保与异常。 + +#### A2 · 多通道录入正式保单(约 5 SP) +- **角色**:运维部、采购部 +- **起点**:拿到新保或续保材料(纸质 / 电子保单、批量表格)。 +- **怎么运作**: + 1. 选择新增、保单批量识别或批量导入。 + 2. 识别类须人工确认后再落库;导入仅处理新保/续保。 + 3. 按车牌/VIN(或批单场景按保单号)匹配车辆并写入对应险种台账。 +- **关键结果**:`确认后落库` / `识别失败可理解提示` / `车牌不一致则拦截` +- **闭环**:正式保单进入一车一档,列表与车辆管理可见最新到期与状态。 +- **排期(可选)**:US-IPC-02 · 规模 L · 作为运维/采购,我想用识别或导入补齐台账,以便续保不遗漏。 + +#### A3 · 停保 / 复驶 / 退保留痕(约 4 SP) +- **角色**:运维部 +- **起点**:车辆需临时停保、恢复行驶或办理退保。 +- **怎么运作**: + 1. 在车辆保险档案对**当前有效**记录发起对应办理。 + 2. 填写时间、金额等业务字段并上传批单附件。 + 3. 写入操作历史;更新险种与车辆状态展示。 +- **关键结果**:`变更可追溯` / `归档记录只读` +- **闭环**:停保/复驶/退保全程留痕,交车口径随之更新。 +- **排期(可选)**:US-IPC-03 · 规模 M。 + +#### A4 · 交强 + 商业决定是否可交车(约 3 SP) +- **角色**:运维部、采购部 +- **起点**:采购/运维维护交强险与商业险台账,作为交车前置合规数据。 +- **怎么运作**: + 1. 台账维护一车一档核心险种。 + 2. 交车时按「交强 + 商业均为正常或临期」判定是否放行。 + 3. 异常(未购买 / 已到期 / 已退保视同无效)则拦截并提示原因。 +- **关键结果**:`允许交车` / `禁止交车 · 保险异常` +- **闭环**:交车合规与台账一致、可追溯;异常车辆不能进入履约运营。 +- **排期(可选)**:US-17 · 规模 M · 作为采购/运维,我想维护交强+商业险并在交车时校验,以便不合规车辆无法交车。 + +### Epic B · 比价采购与审批跟踪(约 13 SP) + +#### B1 · 比价单批次与付费预警(约 4 SP) +- **角色**:采购部 +- **起点**:需要按批次组织待购车辆与险种,并盯住最晚付费日。 +- **怎么运作**: + 1. 在比价单管理新建或编辑批次;可从临期预警一键带入。 + 2. 用全部 / 临期(≤3 天)/ 超期看板筛选批次。 + 3. 保存时校验购买记录、备注与附件。 +- **关键结果**:`可按付费节奏跟进` / `未满足保存条件则拦截` +- **闭环**:采购节奏可视、可预警;不与保单台账自动混写。 +- **排期(可选)**:US-IPC-04 · 规模 M。 + +#### B2 · 多方报价与提交采购(约 5 SP) +- **角色**:采购部 +- **起点**:同一购买需求需比较多家保司报价后发起采购。 +- **怎么运作**: + 1. 为每行录入多家报价并指定唯一最终比价。 + 2. 填写最晚付费日,勾选可提交行。 + 3. 提交采购申请进入审批中心。 +- **关键结果**:`审批中/已通过行不可再提` / `撤回或驳回后可再提` +- **闭环**:比价过程可追溯;正式保单仍须审批通过后在台账侧补录。 +- **排期(可选)**:US-IPC-05 · 规模 L。 + +#### B3 · 审批只读回显(约 4 SP) +- **角色**:采购部、业务管理组 +- **起点**:采购申请已在审批中心流转。 +- **怎么运作**: + 1. 本页展示采购状态与当前审批人(可为空)。 + 2. 业管等角色在审批中心办理节点。 + 3. 结果回写本页;通过后运营去保单管理录入正式保单。 +- **关键结果**:`本页不可办理审批` / `状态与审批中心一致` +- **闭环**:采购决策可跟踪;台账仍以人工录入为准。 +- **排期(可选)**:US-IPC-06 · 规模 M。 + +--- + +## 5. 功能模块说明(正向 / 逆向) + +### 5.1 保险台账列表与 KPI + +**正向** +1. 按车牌、VIN、品牌型号、运营状态、保险状态、险种 + 到期区间筛选。 +2. 点击 KPI(台账车辆、状态正常、核心逾期、核心待购等)联动列表或打开预警。 +3. 行内「管理」打开车辆保险档案。 + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| 无匹配数据 | 空态提示无可匹配台账车辆 | +| 退出运营车辆 | 列表可查看但样式弱化 | +| 险种到期筛选未选险种 | 须先选险种再筛到期区间 | +| 车牌与多车牌条件互斥 | 按产品约定互斥,避免条件冲突 | + +### 5.2 保单录入(新增 / 识别 / 导入) + +**正向** +1. 新增:完整字段手工录入后保存。 +2. 批量识别:选业务类型 → 上传 → 识别 → **人工确认** → 落库。 +3. 批量导入:下载模板填写 → 上传 → 仅新保/续保入库。 +4. 识别任务记录:查看进度,对待确认结果继续确认。 + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| 识别失败 | 提示清晰度 / 格式 / 损坏等原因 | +| 无成功结果可确认 | 提示暂无识别成功结果 | +| 识别车牌与所选车不一致 | 拦截并提示两边车牌 | +| 导入中的停保/复驶/退保行 | 自动跳过 | +| OCR 确认页 | 不展示承保险种明细、分项保费(手工路径仍可填) | + +### 5.3 车辆保险档案 · 停保 / 复驶 / 退保 + +**正向** +1. 分险种查看历史;对当前有效记录办理停保、复驶或退保。 +2. 填写业务时间 / 金额等并上传批单附件。 +3. 全周期时间轴与操作历史可追溯。 + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| 历史归档记录 | 只读,不可再办停保/退保 | +| 已退保 | 仅允许复驶 | +| 已停保 | 可复驶或退保 | +| 新保覆盖已退保险种 | 旧记录归档后再写入新保单 | + +### 5.4 比价单管理与编辑 + +**正向** +1. 新建 / 编辑比价单,添加购买记录与多方报价。 +2. 设最终比价、最晚付费日;保存备注与附件。 +3. 勾选行提交采购;跟踪审批状态。 +4. 可从险种临期预警一键生成比价单(附件须补传)。 + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| 保存校验不通过 | 拦截并提示缺记录 / 备注 / 附件等 | +| 审批中或已通过行 | 勾选禁用,不可重复提交 | +| 撤回 / 驳回 | 恢复可提交 | +| 修改行内险种 | 清空该行报价与最终比价 | +| 删除比价单 | 二次确认,删除后不可恢复 | +| 一键生成时已有审批中/通过的同车同险 | 跳过该条 | + +--- + +## 6. 关键业务逻辑(必须对齐) + +### 6.1 险种范围 + +| 险种 | 台账 | 临期预警 | 交车判定 | +|------|------|----------|----------| +| 交强险 | 是 | 是 | **是(核心)** | +| 商业险 | 是 | 是 | **是(核心)** | +| 超赔险 | 是 | 是 | 否 | +| 驾意险 | 是 | 是 | 否 | +| 货物险 | 是 | 是 | 否 | + +### 6.2 单险种状态(按优先级命中即止) + +1. 已退保 +2. 无保单号 → 未购买 +3. 停保标记 → 已停保 +4. 有号但无到期日 → 未购买 +5. 到期日 ≤ 今天 → 已到期 +6. 到期日 ≤ 今天 + 30 天 → 临期 +7. 否则 → 正常 + +列表到期日取同车同险种**最新一条**记录。 + +### 6.3 车辆级保险状态(交车联动) + +| 条件 | 车辆保险状态 | 交车 | +|------|--------------|------| +| 交强与商业均为正常或临期(仍在有效期内) | 正常 / 临期 | **允许**(临期可提示尽快续保) | +| 交强或商业任一未购买或已到期 | 异常 | **禁止** | +| 已退保 | 视同无有效保障 | **禁止** | + +### 6.4 新保 / 续保录入必填(业务口径) + +- 车牌与车架号至少填一项 +- 必填:险种、保险公司、保单号、生效日、到期日、保险费合计 +- 承保险种明细、分项保费:非必填 + +### 6.5 批单匹配方式 + +| 业务 | 匹配方式 | +|------|----------| +| 保单录入 | 按车牌 / 车架号匹配车辆 | +| 停保 / 复驶 / 退保 | 按**保单号**在全库匹配已有记录 | + +### 6.6 比价保存与提交 + +**保存须同时满足**:至少 1 条购买记录;每行已选车辆;整单备注非空;整单至少 1 个附件。 + +**提交采购另须**:至少勾选 1 行;行内已有报价并设最终比价;已填最晚付费日;采购状态为未提交 / 撤回 / 驳回;未保存须先保存。 + +**报价**:保司 + 金额大于 0;同一保司不可对同行重复报价。 + +**最晚付费预警(仅比价)**:≤ 3 个自然日为临期;已过为超期。 + +### 6.7 采购状态(购买记录行级) + +| 状态 | 可否再提交 | 说明 | +|------|------------|------| +| 未提交 | 可以 | 默认 | +| 审批中 | 不可以 | 本页提交后写入;勾选禁用 | +| 审批通过 | 不可以 | 审批中心回写;勾选禁用 | +| 撤回 / 审批驳回 | 可以 | 审批中心回写后可再提 | + +> 审批办理不在本页;当前审批人由工作流回写,可为空。 + +--- + +## 7. 总览流程图 + +```mermaid +flowchart TB + entry[保险采购入口] + entry --> policy[保单管理] + entry --> compare[比价单] + + policy --> input[新增 / 识别 / 导入] + input --> confirm[人工确认落库] + confirm --> archive[车辆保险档案] + archive --> change[停保 / 复驶 / 退保] + archive --> status[更新车辆保险状态] + status --> handover[交车校验交强+商业] + + compare --> batch[新建或预警生成批次] + batch --> quote[多方报价与最终比价] + quote --> save[保存备注与附件] + save --> submit[勾选提交采购] + submit --> appr[审批中心流转] + appr --> readonly[本页只读回显] + readonly --> manual[审批通过后人工录入正式保单] + manual --> policy +``` + +--- + +## 8. 验收清单(产品 / 测试) + +**保单台账** +- [ ] 筛选与 KPI 口径一致;空态文案正确 +- [ ] 五类险种独立台账;列表到期日取最新一条 +- [ ] 新增 / OCR 确认 / 导入(仅新保续保)均可落库 +- [ ] OCR 失败、车牌不一致有明确拦截 +- [ ] 停保 / 复驶 / 退保须附件并写入操作历史;归档只读 + +**交车合规** +- [ ] 仅交强 + 商业决定车辆级状态 +- [ ] 异常禁止交车;正常/临期允许(临期可提示续保) +- [ ] 超赔 / 货物 / 驾意不参与交车判定 + +**比价采购** +- [ ] 保存 / 提交校验完整 +- [ ] 最晚付费临期 ≤3 天、超期看板正确 +- [ ] 审批中 / 通过行不可再提;撤回 / 驳回可再提 +- [ ] 审批通过**不**自动写入保单台账 +- [ ] 本页不可办理审批,状态只读回显 + +**边界** +- [ ] 保单与比价互不自动同步 +- [ ] 一键生成跳过已审批中/通过的同车同险 + +--- + +## 9. 交付口径 + +> 「保险采购」在 OneOS PC 承载**保单台账**与**比价采购**两条独立业务线:运维部、采购部可一车一档维护交强 / 商业等五类险种并办理停保复驶退保,车辆管理与交车据此校验核心险种有效性;采购部可在比价单中完成多方报价、付费预警与采购审批跟踪,审批通过后须人工补录正式保单——台账与比价互不同步,审批不在本页办理。 diff --git a/src/prototypes/insurance-procurement/annotation-source.json b/src/prototypes/insurance-procurement/annotation-source.json index 4912131..266ac4b 100644 --- a/src/prototypes/insurance-procurement/annotation-source.json +++ b/src/prototypes/insurance-procurement/annotation-source.json @@ -5,7 +5,7 @@ "version": 2, "prototypeName": "insurance-procurement", "pageId": "list", - "updatedAt": 1783800844965, + "updatedAt": 1784545564192, "nodes": [ { "id": "ipc-header", @@ -502,6 +502,13 @@ "title": "保险采购说明", "defaultExpanded": true, "children": [ + { + "type": "markdown", + "id": "ipc-doc-prd", + "title": "产品需求说明(PRD)", + "markdownPath": ".spec/requirements-prd.md", + "markdown": "# 保险采购 · 产品需求说明(全模块)\n\n| 项 | 内容 |\n|---|---|\n| 模块名称 | 保险采购 |\n| 所属系统 | ONE-OS(PC) |\n| 交互原型 | `/prototypes/insurance-procurement` |\n| 业务条线 | 运维管理条线(证照 / 保险相关) |\n| 文档状态 | AutoPRD · 已对齐可运行原型 |\n\n---\n\n## 1. 一句话与目标\n\n**一句话** \n在同一模块内管理车队**保单台账**与**采购前比价审批**两条业务线,保证续保不遗漏、变更可追溯,并为交车提供交强险 + 商业险合规校验。\n\n**要解决的问题** \n- 保险信息散落在表格、邮件与线下批单中,台账不全、临期易漏 \n- 停保 / 复驶 / 退保缺少统一留痕 \n- 采购前多方比价难追溯,审批结果难跟踪 \n- 交车时无法与车辆管理共用同一套「保险是否有效」口径 \n\n**本期目标** \n1. 一车一档维护五类险种台账;支持手工新增、批量识别、批量导入 \n2. 支持停保 / 复驶 / 退保办理与操作历史 \n3. 输出与车辆管理一致的车辆级保险状态,支撑交车拦截 \n4. 比价单批次管理:多方报价、最晚付费预警、勾选提交采购审批(本页只读跟踪) \n\n**非目标(本期不做)** \n- 比价单与保单台账**自动双向同步**;审批通过后**不自动写入**正式保单 \n- 在本模块内办理审批(通过 / 驳回 / 撤回均在审批中心) \n- 维护行驶证、道路运输证等证照档案(见证照管理) \n- 事故赔付、保险上浮等事故链路台账(见事故管理) \n\n---\n\n## 2. 模块边界(最重要)\n\n```mermaid\nflowchart LR\n IPC[保险采购]\n VM[车辆管理]\n OPS[交车管理]\n APPR[审批中心]\n CERT[证照管理]\n WB[工作台]\n IPC -->|输出保险状态口径| VM\n VM -->|交车前置校验| OPS\n CERT -.->|并列交车前置| OPS\n IPC -->|发起采购审批| APPR\n APPR -->|回写采购状态| IPC\n WB -->|临期 KPI / 快捷入口| IPC\n```\n\n| 业务线 / 子域 | 做什么 | 不做什么 |\n|---------------|--------|----------|\n| 保单管理 | 一车一档、多通道录入、停保/复驶/退保、状态与交车口径 | 不替代证照管理;不办事故赔付 |\n| 比价单 | 批次比价、付费预警、提交采购、只读跟踪审批 | 不自动生成正式保单;不在本页审批 |\n| 与车辆 / 交车 | 提供交强 + 商业是否有效的统一口径 | 不在本页完成交车动作 |\n\n**外部依赖(产品口径)**\n\n| 依赖方 | 交互方式 | 产品要求 |\n|--------|----------|----------|\n| 车辆管理 | 读取保险状态 / 到期日 | 口径与本模块一致:异常则禁止交车 |\n| 交车管理 | 交付前校验交强 + 商业有效 | 不合规须可理解拦截原因 |\n| 审批中心 | 接收采购申请并回写状态 / 当前审批人 | 本页只读展示,不办理审批 |\n| 工作台 | KPI、快捷「发起保险采购」 | 跳转至本模块对应能力 |\n| 证照管理 | 并列交车前置 | 证照与保险各自独立维护 |\n\n**关键约束** \n- **保单台账 ↔ 比价单互不自动同步**;审批通过后须运营在保单管理侧另行录入正式保单 \n- **交车只看交强险 + 商业险**;超赔 / 货物 / 驾意仅参与临期预警,不参与交车判定 \n- **最晚付费临期 / 超期**仅服务比价采购节奏,不写入保单台账 \n\n---\n\n## 3. 用户与角色\n\n| 角色 | 主要目标 |\n|------|----------|\n| 运维部 | 维护保单台账、办理停保/复驶/退保;交车前确认保险有效 |\n| 安全部 | 从合规视角关注保险与证照并列的交车前置 |\n| 采购部 | 创建比价单、录入报价、提交采购申请;关注临期与付费节奏 |\n| 业务管理组(业管) | 在审批中心确认报价等节点(本页只读看到结果) |\n\n> 部门名对齐业务条线说明;`module-role-mapping` 将本模块主责记为运维部、安全部,采购部为比价线主操作用户。\n\n---\n\n## 4. 用户故事与故事点(业务条线说明口径)\n\n> 故事点供排期参考,可按团队基准调整。合计约 **28 SP**。\n\n### Epic A · 保单台账与交车合规(约 15 SP)\n\n#### A1 · 一车一档台账与检索(约 3 SP)\n- **角色**:运维部、采购部 \n- **起点**:需要按车牌 / VIN / 运营状态 / 保险状态等快速定位车辆保险档案。 \n- **怎么运作**: \n 1. 在列表筛选或点击 KPI 卡片缩小范围。 \n 2. 查看五类险种到期日与车辆级保险状态。 \n 3. 进入「管理」打开车辆保险档案。 \n- **关键结果**:`可按条件定位车辆` / `空态提示无可匹配台账` \n- **闭环**:台账可检索、可下钻,与 KPI 口径一致。 \n- **排期(可选)**:US-IPC-01 · 规模 M · 作为运维/采购,我想快速筛出台账车辆,以便处理续保与异常。\n\n#### A2 · 多通道录入正式保单(约 5 SP)\n- **角色**:运维部、采购部 \n- **起点**:拿到新保或续保材料(纸质 / 电子保单、批量表格)。 \n- **怎么运作**: \n 1. 选择新增、保单批量识别或批量导入。 \n 2. 识别类须人工确认后再落库;导入仅处理新保/续保。 \n 3. 按车牌/VIN(或批单场景按保单号)匹配车辆并写入对应险种台账。 \n- **关键结果**:`确认后落库` / `识别失败可理解提示` / `车牌不一致则拦截` \n- **闭环**:正式保单进入一车一档,列表与车辆管理可见最新到期与状态。 \n- **排期(可选)**:US-IPC-02 · 规模 L · 作为运维/采购,我想用识别或导入补齐台账,以便续保不遗漏。\n\n#### A3 · 停保 / 复驶 / 退保留痕(约 4 SP)\n- **角色**:运维部 \n- **起点**:车辆需临时停保、恢复行驶或办理退保。 \n- **怎么运作**: \n 1. 在车辆保险档案对**当前有效**记录发起对应办理。 \n 2. 填写时间、金额等业务字段并上传批单附件。 \n 3. 写入操作历史;更新险种与车辆状态展示。 \n- **关键结果**:`变更可追溯` / `归档记录只读` \n- **闭环**:停保/复驶/退保全程留痕,交车口径随之更新。 \n- **排期(可选)**:US-IPC-03 · 规模 M。\n\n#### A4 · 交强 + 商业决定是否可交车(约 3 SP)\n- **角色**:运维部、采购部 \n- **起点**:采购/运维维护交强险与商业险台账,作为交车前置合规数据。 \n- **怎么运作**: \n 1. 台账维护一车一档核心险种。 \n 2. 交车时按「交强 + 商业均为正常或临期」判定是否放行。 \n 3. 异常(未购买 / 已到期 / 已退保视同无效)则拦截并提示原因。 \n- **关键结果**:`允许交车` / `禁止交车 · 保险异常` \n- **闭环**:交车合规与台账一致、可追溯;异常车辆不能进入履约运营。 \n- **排期(可选)**:US-17 · 规模 M · 作为采购/运维,我想维护交强+商业险并在交车时校验,以便不合规车辆无法交车。\n\n### Epic B · 比价采购与审批跟踪(约 13 SP)\n\n#### B1 · 比价单批次与付费预警(约 4 SP)\n- **角色**:采购部 \n- **起点**:需要按批次组织待购车辆与险种,并盯住最晚付费日。 \n- **怎么运作**: \n 1. 在比价单管理新建或编辑批次;可从临期预警一键带入。 \n 2. 用全部 / 临期(≤3 天)/ 超期看板筛选批次。 \n 3. 保存时校验购买记录、备注与附件。 \n- **关键结果**:`可按付费节奏跟进` / `未满足保存条件则拦截` \n- **闭环**:采购节奏可视、可预警;不与保单台账自动混写。 \n- **排期(可选)**:US-IPC-04 · 规模 M。\n\n#### B2 · 多方报价与提交采购(约 5 SP)\n- **角色**:采购部 \n- **起点**:同一购买需求需比较多家保司报价后发起采购。 \n- **怎么运作**: \n 1. 为每行录入多家报价并指定唯一最终比价。 \n 2. 填写最晚付费日,勾选可提交行。 \n 3. 提交采购申请进入审批中心。 \n- **关键结果**:`审批中/已通过行不可再提` / `撤回或驳回后可再提` \n- **闭环**:比价过程可追溯;正式保单仍须审批通过后在台账侧补录。 \n- **排期(可选)**:US-IPC-05 · 规模 L。\n\n#### B3 · 审批只读回显(约 4 SP)\n- **角色**:采购部、业务管理组 \n- **起点**:采购申请已在审批中心流转。 \n- **怎么运作**: \n 1. 本页展示采购状态与当前审批人(可为空)。 \n 2. 业管等角色在审批中心办理节点。 \n 3. 结果回写本页;通过后运营去保单管理录入正式保单。 \n- **关键结果**:`本页不可办理审批` / `状态与审批中心一致` \n- **闭环**:采购决策可跟踪;台账仍以人工录入为准。 \n- **排期(可选)**:US-IPC-06 · 规模 M。\n\n---\n\n## 5. 功能模块说明(正向 / 逆向)\n\n### 5.1 保险台账列表与 KPI\n\n**正向** \n1. 按车牌、VIN、品牌型号、运营状态、保险状态、险种 + 到期区间筛选。 \n2. 点击 KPI(台账车辆、状态正常、核心逾期、核心待购等)联动列表或打开预警。 \n3. 行内「管理」打开车辆保险档案。 \n\n**逆向 / 边界**\n\n| 情况 | 系统表现 |\n|------|----------|\n| 无匹配数据 | 空态提示无可匹配台账车辆 |\n| 退出运营车辆 | 列表可查看但样式弱化 |\n| 险种到期筛选未选险种 | 须先选险种再筛到期区间 |\n| 车牌与多车牌条件互斥 | 按产品约定互斥,避免条件冲突 |\n\n### 5.2 保单录入(新增 / 识别 / 导入)\n\n**正向** \n1. 新增:完整字段手工录入后保存。 \n2. 批量识别:选业务类型 → 上传 → 识别 → **人工确认** → 落库。 \n3. 批量导入:下载模板填写 → 上传 → 仅新保/续保入库。 \n4. 识别任务记录:查看进度,对待确认结果继续确认。 \n\n**逆向 / 边界**\n\n| 情况 | 系统表现 |\n|------|----------|\n| 识别失败 | 提示清晰度 / 格式 / 损坏等原因 |\n| 无成功结果可确认 | 提示暂无识别成功结果 |\n| 识别车牌与所选车不一致 | 拦截并提示两边车牌 |\n| 导入中的停保/复驶/退保行 | 自动跳过 |\n| OCR 确认页 | 不展示承保险种明细、分项保费(手工路径仍可填) |\n\n### 5.3 车辆保险档案 · 停保 / 复驶 / 退保\n\n**正向** \n1. 分险种查看历史;对当前有效记录办理停保、复驶或退保。 \n2. 填写业务时间 / 金额等并上传批单附件。 \n3. 全周期时间轴与操作历史可追溯。 \n\n**逆向 / 边界**\n\n| 情况 | 系统表现 |\n|------|----------|\n| 历史归档记录 | 只读,不可再办停保/退保 |\n| 已退保 | 仅允许复驶 |\n| 已停保 | 可复驶或退保 |\n| 新保覆盖已退保险种 | 旧记录归档后再写入新保单 |\n\n### 5.4 比价单管理与编辑\n\n**正向** \n1. 新建 / 编辑比价单,添加购买记录与多方报价。 \n2. 设最终比价、最晚付费日;保存备注与附件。 \n3. 勾选行提交采购;跟踪审批状态。 \n4. 可从险种临期预警一键生成比价单(附件须补传)。 \n\n**逆向 / 边界**\n\n| 情况 | 系统表现 |\n|------|----------|\n| 保存校验不通过 | 拦截并提示缺记录 / 备注 / 附件等 |\n| 审批中或已通过行 | 勾选禁用,不可重复提交 |\n| 撤回 / 驳回 | 恢复可提交 |\n| 修改行内险种 | 清空该行报价与最终比价 |\n| 删除比价单 | 二次确认,删除后不可恢复 |\n| 一键生成时已有审批中/通过的同车同险 | 跳过该条 |\n\n---\n\n## 6. 关键业务逻辑(必须对齐)\n\n### 6.1 险种范围\n\n| 险种 | 台账 | 临期预警 | 交车判定 |\n|------|------|----------|----------|\n| 交强险 | 是 | 是 | **是(核心)** |\n| 商业险 | 是 | 是 | **是(核心)** |\n| 超赔险 | 是 | 是 | 否 |\n| 驾意险 | 是 | 是 | 否 |\n| 货物险 | 是 | 是 | 否 |\n\n### 6.2 单险种状态(按优先级命中即止)\n\n1. 已退保 \n2. 无保单号 → 未购买 \n3. 停保标记 → 已停保 \n4. 有号但无到期日 → 未购买 \n5. 到期日 ≤ 今天 → 已到期 \n6. 到期日 ≤ 今天 + 30 天 → 临期 \n7. 否则 → 正常 \n\n列表到期日取同车同险种**最新一条**记录。\n\n### 6.3 车辆级保险状态(交车联动)\n\n| 条件 | 车辆保险状态 | 交车 |\n|------|--------------|------|\n| 交强与商业均为正常或临期(仍在有效期内) | 正常 / 临期 | **允许**(临期可提示尽快续保) |\n| 交强或商业任一未购买或已到期 | 异常 | **禁止** |\n| 已退保 | 视同无有效保障 | **禁止** |\n\n### 6.4 新保 / 续保录入必填(业务口径)\n\n- 车牌与车架号至少填一项 \n- 必填:险种、保险公司、保单号、生效日、到期日、保险费合计 \n- 承保险种明细、分项保费:非必填 \n\n### 6.5 批单匹配方式\n\n| 业务 | 匹配方式 |\n|------|----------|\n| 保单录入 | 按车牌 / 车架号匹配车辆 |\n| 停保 / 复驶 / 退保 | 按**保单号**在全库匹配已有记录 |\n\n### 6.6 比价保存与提交\n\n**保存须同时满足**:至少 1 条购买记录;每行已选车辆;整单备注非空;整单至少 1 个附件。 \n\n**提交采购另须**:至少勾选 1 行;行内已有报价并设最终比价;已填最晚付费日;采购状态为未提交 / 撤回 / 驳回;未保存须先保存。 \n\n**报价**:保司 + 金额大于 0;同一保司不可对同行重复报价。 \n\n**最晚付费预警(仅比价)**:≤ 3 个自然日为临期;已过为超期。\n\n### 6.7 采购状态(购买记录行级)\n\n| 状态 | 可否再提交 | 说明 |\n|------|------------|------|\n| 未提交 | 可以 | 默认 |\n| 审批中 | 不可以 | 本页提交后写入;勾选禁用 |\n| 审批通过 | 不可以 | 审批中心回写;勾选禁用 |\n| 撤回 / 审批驳回 | 可以 | 审批中心回写后可再提 |\n\n> 审批办理不在本页;当前审批人由工作流回写,可为空。\n\n---\n\n## 7. 总览流程图\n\n```mermaid\nflowchart TB\n entry[保险采购入口]\n entry --> policy[保单管理]\n entry --> compare[比价单]\n\n policy --> input[新增 / 识别 / 导入]\n input --> confirm[人工确认落库]\n confirm --> archive[车辆保险档案]\n archive --> change[停保 / 复驶 / 退保]\n archive --> status[更新车辆保险状态]\n status --> handover[交车校验交强+商业]\n\n compare --> batch[新建或预警生成批次]\n batch --> quote[多方报价与最终比价]\n quote --> save[保存备注与附件]\n save --> submit[勾选提交采购]\n submit --> appr[审批中心流转]\n appr --> readonly[本页只读回显]\n readonly --> manual[审批通过后人工录入正式保单]\n manual --> policy\n```\n\n---\n\n## 8. 验收清单(产品 / 测试)\n\n**保单台账** \n- [ ] 筛选与 KPI 口径一致;空态文案正确 \n- [ ] 五类险种独立台账;列表到期日取最新一条 \n- [ ] 新增 / OCR 确认 / 导入(仅新保续保)均可落库 \n- [ ] OCR 失败、车牌不一致有明确拦截 \n- [ ] 停保 / 复驶 / 退保须附件并写入操作历史;归档只读 \n\n**交车合规** \n- [ ] 仅交强 + 商业决定车辆级状态 \n- [ ] 异常禁止交车;正常/临期允许(临期可提示续保) \n- [ ] 超赔 / 货物 / 驾意不参与交车判定 \n\n**比价采购** \n- [ ] 保存 / 提交校验完整 \n- [ ] 最晚付费临期 ≤3 天、超期看板正确 \n- [ ] 审批中 / 通过行不可再提;撤回 / 驳回可再提 \n- [ ] 审批通过**不**自动写入保单台账 \n- [ ] 本页不可办理审批,状态只读回显 \n\n**边界** \n- [ ] 保单与比价互不自动同步 \n- [ ] 一键生成跳过已审批中/通过的同车同险 \n\n---\n\n## 9. 交付口径\n\n> 「保险采购」在 OneOS PC 承载**保单台账**与**比价采购**两条独立业务线:运维部、采购部可一车一档维护交强 / 商业等五类险种并办理停保复驶退保,车辆管理与交车据此校验核心险种有效性;采购部可在比价单中完成多方报价、付费预警与采购审批跟踪,审批通过后须人工补录正式保单——台账与比价互不同步,审批不在本页办理。\n" + }, { "type": "markdown", "id": "ipc-doc-overview", diff --git a/src/prototypes/oneos-h5-h2-order/.spec/flows.md b/src/prototypes/oneos-h5-h2-order/.spec/flows.md new file mode 100644 index 0000000..1091269 --- /dev/null +++ b/src/prototypes/oneos-h5-h2-order/.spec/flows.md @@ -0,0 +1,68 @@ +# 流程图 + +> 标注面板支持 Mermaid 渲染。下列流程与当前原型行为一致(本地 Store,未接真实 API)。 + +--- + +## 1. 主路径:从打开到上报 + +```mermaid +flowchart TD + A[打开加氢订单 H5] --> B[查看本站列表与 KPI] + B --> C{前一日手工台账已上传?} + C -->|否| D[提示:手工台账有缺失…] + D --> E[进入手工台账 · 选中昨日] + E --> F[拍照/相册上传并确认] + F --> C + C -->|是| G[点新增] + G --> H[填写时间/车牌/面板/量价] + H --> I{同站同车牌同时间已存在?} + I -->|是| J[拦截提示] + J --> H + I -->|否| K[保存写入 Bridge] + K --> B +``` + +--- + +## 2. 手工台账补传 + +```mermaid +flowchart LR + A[手工台账 Tab] --> B[月历:绿已传 / 橙未传] + B --> C[点选 ≤今日 且 ≥首笔日] + C --> D[上传图片] + D --> E[二次确认] + E --> F[归档不可删 · 可追加] +``` + +--- + +## 3. 编辑 / 删除与锁定 + +```mermaid +flowchart TD + A[打开详情] --> L{羚牛车辆?} + L -->|否| D[可编辑 / 可删除] + L -->|是| B{已核对或已对账?} + B -->|是| C[只读 · 禁改删] + B -->|否| D + D --> E[写回 Bridge] +``` + +--- + +## 4. 与 Web / 台账联动(概念) + +```mermaid +sequenceDiagram + participant H5 as 加氢订单 H5 + participant BR as Bridge Store + participant WEB as 加氢记录 Web + participant LED as 车辆氢费明细 + participant SITE as 站点对账单 + H5->>BR: 新增/改删流水 · 上传台账 + WEB->>BR: 同 Store 读写 · 导出 + LED->>BR: 完成核对 → 已核对 + SITE->>BR: 提交对账单 → 已对账 +``` diff --git a/src/prototypes/oneos-h5-h2-order/.spec/key-logic.md b/src/prototypes/oneos-h5-h2-order/.spec/key-logic.md new file mode 100644 index 0000000..b3f4c48 --- /dev/null +++ b/src/prototypes/oneos-h5-h2-order/.spec/key-logic.md @@ -0,0 +1,52 @@ +# 关键逻辑索引 + +本页汇总加氢订单(H5)复杂判定入口;细则见各规格全文。原型数据均为本地种子 / 内存 Store,**未接真实 API**。 + +--- + +## 1. 判定主题一览 + +| 主题 | 一句话 | 全文 | +|---|---|---| +| 手工台账门禁 | **昨日**未传 → 今日禁新增;固定提示文案并直达台账 | [feature-manual-ledger.md](./feature-manual-ledger.md) | +| 核对 ≠ 对账 | 能源部核对 / 站点对账单提交,触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md) | +| 记录锁定 | **仅羚牛**:已核对或已对账 → 站端不可改删;非羚牛不锁 | [feature-detail.md](./feature-detail.md)、[reconcile-linkage.md](./reconcile-linkage.md)、[fleet-plate-tag.md](./fleet-plate-tag.md) | +| 重复键 | 同站 + 同车牌 + 同加氢时间 | Bridge / [Web record-data-model](../oneos-web-h2-station/.spec/record-data-model.md) | +| 总额计算 | 总额 = 单价 × 量(两位小数) | [feature-create.md](./feature-create.md) | +| 羚牛车辆 | 本地系统车牌集合 | [fleet-plate-tag.md](./fleet-plate-tag.md) | + +--- + +## 2. 手工台账门禁(摘要) + +| 优先级 | 条件 | 结果 | +|--------|------|------| +| 1 | 编辑已有记录 | 不校验 | +| 2 | 昨日早于本站首笔加氢日 | 不要求昨日台账,可新增 | +| 3 | 昨日已有 ≥1 张台账图 | 可新增 | +| 4 | 昨日未上传 | 提示并跳转手工台账(选中昨日) | + +用户可见文案:`手工台账有缺失,作为对账重要依据,请先上传手工台账` + +--- + +## 3. 核对 / 对账(摘要) + +| 状态 | 谁触发 | 站端影响 | +|---|---|---| +| 已核对 | 车辆氢费明细「完成核对」 | **羚牛**不可改删 | +| 已对账 | 站点信息对账单提交 | **羚牛**不可改删;回写对账时间 | +| 非羚牛 | — | 不参与核对/对账展示与锁定 | + +H5 列表主要展示核对状态(仅羚牛);对账细节见联动规格。 + +--- + +## 4. 代码路径 + +| 能力 | 路径 | +|---|---| +| Bridge | `src/common/h2VehicleLedgerBridge.js` | +| 门禁工具 | `utils/manual-ledger.ts` → `isManualLedgerGateBlocked` | +| 订单 CRUD | `utils/orders.ts` | +| 页面 | `index.tsx` / `ManualLedgerPanel` / `CreateWizard` | diff --git a/src/prototypes/oneos-h5-h2-order/.spec/prototype-review.md b/src/prototypes/oneos-h5-h2-order/.spec/prototype-review.md new file mode 100644 index 0000000..43ceac6 --- /dev/null +++ b/src/prototypes/oneos-h5-h2-order/.spec/prototype-review.md @@ -0,0 +1,64 @@ +# Prototype Review + +- 审查目标:`src/prototypes/oneos-h5-h2-order`(加氢站管理 → **加氢订单** H5;入口 `index.tsx`) +- 用户资料/参考资料: + - 本轮用户说明:对「加氢订单」执行 Prototype Review / 需求评审(未另附独立需求附件) + - `.spec/`:`requirements-prd.md`(v1.7)、`roles.md`、`user-stories.md`、`key-logic.md`、`flows.md`、`feature-list.md`、`feature-create.md`、`feature-detail.md`、`feature-manual-ledger.md`、`fleet-plate-tag.md`、`reconcile-linkage.md`;早期 `2026-07-14-requirements-design.md`(已标注作废条目) + - `src/resources/`:`oneos-web-legacy/…/车辆氢费明细-需求文档.md`(文首已注明以 verify-reconcile / reconcile-linkage 为准) + - 关联规格:`oneos-web-h2-station/.spec/fleet-vehicle-verify.md`、`record-data-model.md`;`vehicle-h2-fee-ledger/.spec/verify-reconcile.md` + - 源码:`index.tsx`、`components/*`、`utils/*`、`types.ts`、`annotation-source.json`;共享 Bridge / 品牌 Store +- 生成时间:2026-07-18 20:14 +- 修复闭环:2026-07-18 20:32(按 Review 处理全部冲突与 P0–P3 项) +- 资料冲突/待确认:**无**(已对齐) +- 独立评估:`full` + +## 总体点评 + +加氢订单 H5 主路径完整可演示。评审指出的冲突已闭环:**昨日门禁文案**、**列表顶栏橙条**、**锁定仅羚牛**(规格与实现一致)、**早期切站稿作废标注**、**删除确认「未锁定」**、历史 resources 权威顺序说明。剩余为原型边界内 Mock(OCR / 车机 / 无真实 API),不构成业务冲突。 + +## P0-P3 优先级问题 + +### P1 - 手工台账页文案把「今日上传」当成新增门禁 — **已修复** + +- 处理:改为「今日不作为新增门禁,仅要求前一日已上传」;昨日缺失时台账页单独橙条并可选中昨日。 + +### P2 - 「记录锁定」规格未写明羚牛例外 — **已修复** + +- 处理:`feature-detail.md` / `reconcile-linkage.md` / `key-logic.md` / `flows.md` / PRD v1.7 / US-04 统一为「锁定仅羚牛」;与 `isOrderLocked` 实现一致。 + +### P2 - 门禁提示缺列表顶栏橙条 — **已修复** + +- 处理:订单列表顶部增加可点橙条(跳转台账并选中昨日);保留 FAB 旁提示 + Tab 红点。 + +### P3 - 早期设计文档仍写切站 — **已修复** + +- 处理:`2026-07-14-requirements-design.md` 文首与表格标注作废/覆盖项;权威以 PRD 为准。台账规格区分 H5 固定本站 vs Web 多站。 + +### P3 - 删除确认文案偏「未对账」 — **已修复** + +- 处理:确认语改为「确认删除车牌 … 的未锁定记录?」;台账二次确认与 Web 文案对齐。 + +## 完整性与项目对齐 + +| 维度 | 结论 | +|------|------| +| 核心用户 / 主流程 | 对齐 PRD v1.7 | +| 范围边界 | 真登录 / 真 OCR / PLC / 原生 App / 后端联调仍不做 | +| 资料冲突 | 已消除;resources 旧文档文首指向现行核对≠对账规格 | +| 双端口径 | 门禁昨日、锁定羚牛、Bridge 共享 — 与 Web / 台账一致 | + +## 业务逻辑连贯性 + +- 台账门禁:昨日未传 → 顶栏橙条 / Toast / 跳转台账选中昨日;今日状态 ≠ 门禁。 +- 锁定:先判羚牛,再判核对∨对账。 +- 总额偏差二次确认、重复键拦截:与 Web 共用规则。 + +## 状态、异常、边界与恢复 + +评审所列空态、门禁、必填、重复、脏返回、锁定、归档路径仍成立;本轮仅修正文案/提示位与规格一致性,未改变 Mock 边界。 + +## 证据与评估说明 + +- 用户资料优先级:无新附件;以 `.spec` PRD v1.7 为基线。 +- 读取范围:见文首;闭环改动见 `ManualLedgerPanel.tsx`、`index.tsx` 及上述规格文件。 +- 独立评估:`full`。 diff --git a/src/prototypes/oneos-h5-h2-order/.spec/roles.md b/src/prototypes/oneos-h5-h2-order/.spec/roles.md new file mode 100644 index 0000000..4e685f4 --- /dev/null +++ b/src/prototypes/oneos-h5-h2-order/.spec/roles.md @@ -0,0 +1,38 @@ +# 需求角色 + +| 项 | 说明 | +|---|---| +| 适用原型 | 加氢订单(H5)`oneos-h5-h2-order` | +| 关联 Web | 加氢记录 `oneos-web-h2-station`(同一业务、不同端) | +| 数据 | 原型本地种子 + 共享 Bridge(未接真实登录 / API) | + +--- + +## 1. 角色一览 + +| 角色 | 端 | 核心诉求 | 本原型是否为主用户 | +|---|---|---|---| +| **加氢站操作人员** | H5(手机浏览器) | 在站场快速上报本站加氢流水;补传手工台账照片 | **是** | +| **加氢站站长 / 多站管理员** | Web 为主,H5 可看本站 | 多站台账、导出、手工台账补传与督导 | 间接(H5 固定单站演示) | +| **能源部核对人员** | 车辆氢费明细 | 完成核对,回写「已核对」 | 否(触发方在台账) | +| **站点对账人员** | 站点信息 · 对账单 | 提交对账单,回写「已对账」 | 否(触发方在站点信息) | +| **超级管理员** | Web | 查看全部站点流水与台账 | 否(Web 标注状态演示) | + +--- + +## 2. 本原型主角色:加氢站操作人员 + +| 项 | 说明 | +|---|---| +| 场景 | 户外 / 站场,手机竖屏 | +| 权限边界 | 仅本站数据;不可切站(原型固定演示站) | +| 日常动作 | 看列表 KPI → 上传昨日手工台账(若缺失)→ 新增加氢记录 → 查看/编辑未锁定记录 | +| 约束 | 前一日手工台账未上传则今日不可新增;已核对或已对账不可改删 | + +--- + +## 3. 角色边界(本期不做) + +- 真登录与组织权限中心对接 +- 能源部 / 对账人员在 H5 内操作核对或对账 +- 跨站汇总与导出(属 Web「加氢记录」) diff --git a/src/prototypes/oneos-h5-h2-order/.spec/user-stories.md b/src/prototypes/oneos-h5-h2-order/.spec/user-stories.md new file mode 100644 index 0000000..538a2e5 --- /dev/null +++ b/src/prototypes/oneos-h5-h2-order/.spec/user-stories.md @@ -0,0 +1,84 @@ +# 用户故事 + +> 以加氢站操作人员为主;验收口径见 [requirements-prd.md](./requirements-prd.md)。 + +--- + +## US-01 查看本站加氢订单 + +**作为** 加氢站操作人员, +**我希望** 打开加氢订单即看到本站列表与 KPI, +**以便** 快速了解今日/近期上报情况。 + +**验收要点** + +- 默认已登录本站;无切站 +- KPI:订单数 / 加氢量 / 加氢金额随列表汇总 +- 卡片展示加氢时间、车牌、量、额、核对状态等 + +--- + +## US-02 补传缺失的手工台账后再新增 + +**作为** 加氢站操作人员, +**我希望** 在前一日手工台账未上传时被明确拦截,并直达台账页补传, +**以便** 保证纸质台账作为对账依据不缺失。 + +**验收要点** + +- 顶栏橙条 + 底栏提示:「手工台账有缺失,作为对账重要依据,请先上传手工台账」 +- 点「新增」或顶栏橙条跳转手工台账并选中昨日 +- 补传昨日确认后可新增 + +--- + +## US-03 新增加氢记录 + +**作为** 加氢站操作人员, +**我希望** 用车牌键盘 + 面板拍照/OCR 快速填单, +**以便** 在站场少打字完成上报。 + +**验收要点** + +- 加氢时间默认=点击新增时刻(到分) +- 站名只读为本站 +- 总额 = 单价 × 加氢量(界面不展示公式) +- 同站同车牌同时间重复保存拦截 + +--- + +## US-04 查看详情并编辑/删除未锁定记录 + +**作为** 加氢站操作人员, +**我希望** 打开详情查看字段与识别原图,并对未核对未对账记录改删, +**以便** 纠正录入错误。 + +**验收要点** + +- 羚牛车辆已核对或已对账:不可编辑/删除,有锁定说明;非羚牛不锁定 +- 详情展示车牌/面板含水印原图 + +--- + +## US-05 按日历管理手工台账 + +**作为** 加氢站操作人员, +**我希望** 在月历上看已传/未传并补传过往日, +**以便** 多日未操作后仍能补齐缺口。 + +**验收要点** + +- 自首笔加氢日起绿/橙标记;首笔前不要求 +- 禁选未来日;已归档图不可删,仅可追加 + +--- + +## US-06 识别羚牛车辆 + +**作为** 加氢站操作人员, +**我希望** 在列表/详情看到是否羚牛车辆, +**以便** 理解后续核对对账是否适用该车。 + +**验收要点** + +- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定) diff --git a/src/prototypes/oneos-h5-h2-order/components/CreateWizard.tsx b/src/prototypes/oneos-h5-h2-order/components/CreateWizard.tsx index 2597bd8..86e2a97 100644 --- a/src/prototypes/oneos-h5-h2-order/components/CreateWizard.tsx +++ b/src/prototypes/oneos-h5-h2-order/components/CreateWizard.tsx @@ -16,17 +16,6 @@ import { import { PhoneShell } from './PhoneShell'; import { PlateKeyboardSheet } from './PlateKeyboardSheet'; -function listDispenserBrandHints(): string[] { - const store = (window as unknown as { H2DispenserBrandStore?: { labels?: () => string[] } }) - .H2DispenserBrandStore; - if (!store || typeof store.labels !== 'function') return []; - try { - return store.labels().slice(0, 4); - } catch { - return []; - } -} - type Props = { mode: 'create' | 'edit'; draft: CreateDraft; @@ -107,7 +96,6 @@ export function CreateWizard({ const [plateOpen, setPlateOpen] = useState(false); const vehicleTag = useMemo(() => getVehicleTag(draft.plateNo), [draft.plateNo]); - const brandHints = listDispenserBrandHints(); function patchDraft(patch: Partial) { const nextPrice = patch.unitPrice !== undefined ? patch.unitPrice : draft.unitPrice; @@ -380,12 +368,6 @@ export function CreateWizard({

    请拍摄清晰的加氢机照片

    - {brandHints.length ? ( -

    - OCR 参考品牌型号(站点信息维护):{brandHints.join('、')} - {brandHints.length >= 4 ? '…' : ''} -

    - ) : null}
    原型本地演示种子:[`utils/mileage-task.ts`](../utils/mileage-task.ts) +> **未接真实采购合同 / 任务工单 API** + +## 业务背景 + +采购部发起车辆采购合同时,合同可关联对应批次车辆的**里程要求**与**时间起止**;里程任务通过**任务工单**下发到车辆。手机列表卡片展示该车当前里程任务完成情况,便于外勤扫一眼进度与是否赶得上截止。 + +## 展示位置 + +车辆卡片中,在「来源 / 客户名」等标签行**下方**,全宽分隔区: + +1. 标题「里程任务」(有任务时右侧展示完成百分比) +2. 有任务:进度条 +3. 有任务:剩余公里 + 按近 7 活跃日推算的预计完成天数 +4. 进行中任务:文案后方完成预判标签(对照任务截止剩余日历天) + +## 判定 / 展示优先级 + +| 顺序 | 条件 | 结果 | +|------|------|------| +| 1 | 车辆未挂接有效里程任务工单 | 展示标题 +「暂无里程任务」(无进度条、无剩余/预计、无预判标签) | +| 2 | 已完成里程 ≥ 目标里程(或进度 ≥ 100%) | 进度 100%、成功色进度条与百分比;文案「已完成」;**不**展示预判标签 | +| 3 | 有任务且未完成,且近 7 个活跃日均里程可算 | 进度条 +「剩余 N km · 预计 D 天内完成」+ 预判标签;预判为「无法完成」时进度条/百分比为危险红;「有完成风险」为警告橙 | +| 4 | 有任务且未完成,但无活跃日(近窗内日里程均 ≤ 0) | 进度条(警告橙)+「剩余 N km · 暂无法预计」+ 预判标签「剩余 x 天,预计有完成风险」 | + +## 口径公式 + +| 项 | 口径 | +|----|------| +| 完成百分比 | `min(100, 保留 1 位小数(任务内已完成里程 / 合同目标里程 × 100))` | +| 剩余公里 | `max(0, 目标里程 − 已完成里程)` | +| 活跃日 | 自然日内该车行驶里程 **> 0 km** | +| 近 7 活跃日均 | 取最近最多 **7** 个活跃日的日里程算术平均;不足 7 个则有几个算几个 | +| 预计天数 D | `ceil(剩余公里 / 近 7 活跃日均)`;均里程为 0 或无活跃日 → 无法预计 | +| 截止剩余日历天 x | 任务/合同时间窗截止日相对今天的剩余自然日(下限 0) | + +## 完成预判标签(文案后方) + +用预计天数 D(近 7 活跃日推算)对照截止剩余日历天 x: + +| 顺序 | 条件 | 标签文案 | 色调 | 进度条 / 百分比 | +|------|------|----------|------|----------------| +| 1 | D 无法算出(无活跃日) | 剩余 x 天,预计有完成风险 | 警告橙 | 同警告橙 | +| 2 | x ≤ 0 或 D > x | 剩余 x 天,预计无法完成 | 危险红 | 同危险红 | +| 3 | D > x × 85%,或 (x − D) ≤ 7 | 剩余 x 天,预计有完成风险 | 警告橙 | 同警告橙 | +| 4 | 其余 | 剩余 x 天,预计可完成 | 成功绿 | 默认主色蓝 | + +## 前置条件 + +- 仅列表卡片只读展示;不提供下发、编辑、跳转工单详情(本期不做) +- 筛选 / KPI / 搜索不影响本区展示规则,只影响哪些车出现在列表 + +## 数据源(原型) + +| 来源 | 说明 | +|------|------| +| 演示种子 | 按车牌稳定 hash:约 65% 挂接演示任务(目标里程、已完成、近窗活跃日里程序列、截止剩余日历天);其余为无任务 | +| 台账导表 | 仅借用车牌标识做稳定种子;**不**把导表「当前总里程」直接当作任务进度真相 | + +正式环境应由采购合同批次里程要求 + 时间起止 + 任务工单执行回写提供目标 / 已完成 / 日里程明细 / 截止日。 + +## 用户可见文案 + +| 场景 | 文案 | +|------|------| +| 无任务 | 暂无里程任务 | +| 进行中 | 剩余 {N} km · 预计 {D} 天内完成 | +| 进行中且无法预计 | 剩余 {N} km · 暂无法预计 | +| 预判标签 | 剩余 {x} 天,预计无法完成 / 预计可完成 / 预计有完成风险 | +| 已完成 | 已完成(无预判标签) | + +## 代码路径 + +- 解析:`utils/mileage-task.ts` → `resolveMileageTaskCard` / `resolveMileageForecast` +- 展示:`components/VehicleCard.tsx` + `styles/index.css`(`.h5-va-mileage-task*` / `.h5-va-mileage-forecast`) diff --git a/src/prototypes/oneos-h5-vehicle-assets/.spec/more-filters.md b/src/prototypes/oneos-h5-vehicle-assets/.spec/more-filters.md new file mode 100644 index 0000000..0519449 --- /dev/null +++ b/src/prototypes/oneos-h5-vehicle-assets/.spec/more-filters.md @@ -0,0 +1,38 @@ +# 更多筛选 · 判定规格 + +「更多筛选」底部面板内的条件与列表筛选项行(品牌车型 / 运营城市 / 车辆状态)叠加生效;任一非「全部」即点亮更多筛选角标。 + +## 面板项与匹配口径 + +| 优先级 | 筛选项 | 选项 | 匹配规则 | 数据源 | +|--------|--------|------|----------|--------| +| 1 | 证照状态 | 全部 / 正常 / 异常 | 异常 = 检验有效期早于今天;正常 = 非异常 | 台账证照有效期 | +| 2 | 保险状态 | 全部 / 正常 / 异常 | 异常 = 保险状态为「异常」;正常 = 非异常 | 台账保险状态 | +| 3 | 出库状态 | 全部 / 未出库 / 已出库 | 见下表(由车辆状态推导,无独立出库字段) | 台账车辆状态 | +| 4 | 车辆来源 | 全部 / 自有 / 外租 / 挂靠 | 精确等于台账「车辆来源」 | 台账车辆来源 | +| 5 | 在线状态 | 全部 / 在线 / 离线 | 精确等于台账「在线状态」 | 台账在线状态 | + +以上条件与 KPI、搜索、品牌车型、运营城市、车辆状态为 **AND**。 + +## 出库状态推导(原型本地) + +| 出库选项 | 车辆状态集合 | +|----------|----------------| +| 未出库 | 待验车、未备车、已备车、待交车 | +| 已出库 | 有明确车辆状态,且不属于「未出库」集合(如已交车、待还车、异动中等) | + +**前置条件** + +- 车辆状态为空、`-`、`无`:不匹配「未出库」也不匹配「已出库」(选任一侧时该车被滤掉) +- 正式环境若上线独立「出库」字段,以字段为准,本表仅作原型推导 + +## 交互与布局 + +- 面板挂在手机壳操作区内,宽度不超过壳层;内容区可滚动,底部「重置 / 完成」固定 +- 「重置」只清更多筛选五项,不清筛选项行与 KPI / 搜索 +- 「完成」关闭面板并保留当前选择;列表已随选择即时刷新 + +## 代码路径(实现对照) + +- 匹配:`utils/filter.ts` → `matchOutboundFilter` / `matchVehicleSourceFilter` / `matchOnlineStatusFilter` 等 +- UI:`components/FilterSheet.tsx` diff --git a/src/prototypes/oneos-h5-vehicle-assets/.spec/requirements-prd.md b/src/prototypes/oneos-h5-vehicle-assets/.spec/requirements-prd.md new file mode 100644 index 0000000..136c93f --- /dev/null +++ b/src/prototypes/oneos-h5-vehicle-assets/.spec/requirements-prd.md @@ -0,0 +1,276 @@ +# 车辆资产(H5)· 产品需求说明(全模块) + +## 1. 一句话与目标 + +**一句话** +在 App / 手机浏览器提供与 PC「车辆资产」口径一致的只读查车与异常浏览能力。 + +**要解决的问题** +- 外勤与业务人员无法在手机上快速按车牌查车、看营运分类与证照/保险异常 +- PC 宽表页不适合塞进 WebView,需要独立手机信息架构 + +**本期目标** +1. 列表:车牌搜索、KPI 分类横滑、证照/保险异常筛选、车辆卡片(含里程任务进度) +2. 详情:多 Tab 对齐 PC,全程只读浏览 +3. 全角色同一套界面;手机端不做编辑 + +**非目标(本期不做)** +- 任意字段编辑(含 Admin) +- 导入 / 导出、催办、跳转证照管理或保险采购等业务处理页 +- 里程任务工单下发 / 编辑、从卡片跳转工单详情、按截止日逾期标红 +- 在租车型占比等 PC 尾部分析入口 +- 将 PC 页做响应式自适应 + +## 2. 模块边界 + +```mermaid +flowchart LR + App[App WebView] --> H5[车辆资产 H5] + H5 -.口径一致.-> PC[车辆资产 PC] + H5 -.只读.-> Data[车辆台账与关联记录] +``` + +| 业务线 / 子域 | 做什么 | 不做什么 | +|---------------|--------|----------| +| 查车列表 | 搜索、KPI、异常筛选、卡片进详情、卡片里程任务进度 | 批量操作、写操作、下发/编辑工单 | +| 车辆详情 | 11 个 Tab 只读浏览 | 行内编辑、跳转处理 | + +**外部依赖(产品口径)** + +| 依赖方 | 交互方式 | 产品要求 | +|--------|----------|----------| +| 车辆资产 PC | 同一套台账与统计口径 | KPI / 证照异常 / 保险异常定义一致;台账数据同源(业务导表) | +| 采购合同 / 任务工单 | 合同关联批次里程要求与起止;工单下发后回写进度 | 列表卡片只读展示完成百分比、剩余公里与预计天数;无工单时仍展示「暂无里程任务」 | +| App 壳 | 内嵌打开本 H5 | 固定打开手机页,不打开 PC 宽表地址 | + +**关键约束** +- 手机端明确提示:修改请在 PC 完成 +- 筛选与 KPI 叠加:先按 KPI,再按证照/保险条件 +- 列表车辆数据与 PC「车辆资产」共用业务导表台账(含真实车牌、客户、运营状态分布) + +## 3. 用户与角色 + +| 角色 | 主要目标 | +|------|----------| +| 运维 / 业务 / 管理等全角色 | 同一套界面查车、看异常与档案摘要 | +| Admin | 与普通用户相同;手机端仍无编辑(改数回 PC) | + +## 4. 用户故事与故事点(业务条线说明口径) + +合计约 **15 SP**。 + +### Epic A · 列表查车(约 10 SP) + +#### A1 · 车牌搜索与卡片列表 +- **角色**:全体手机用户 +- **起点**:从 App 进入车辆资产 H5 +- **怎么运作**: + 1. 顶栏左对齐「车辆资产」;搜索栏提示「搜索车牌 / VIN / 停车场 / 客户名称」,支持按这些关键词过滤;右侧扫码为占位提示 + 2. KPI 横滑摘要卡(每屏 3 张、共 2 页;图标 + 计数 + 占比);下方固定筛选项行:品牌车型 / 运营城市(省→市,可只选省)/ 车辆状态 +「更多筛选」(证照 / 保险 / 出库 / 来源 / 在线);前三项为 App 原生底部选择器,选项来自业务导表;更多筛选面板限制在手机操作区内并可滚动 + 3. 列表横卡含车牌、车型、来源标签、按运营状态变化的次要标签(租赁/自营→客户名省略;库存→未备车警告/已备车正常;非运营→非运营车辆;退出→三方退租/销售/报废)、营运状态圆点、里程与异常角标(不含车辆照片);标签下方展示「里程任务」分隔区(有任务:百分比 + 进度条 + 剩余公里/预计天数 + 完成预判标签;无任务:「暂无里程任务」);列表不展示总车辆数,不提供翻页按钮,滚动触底异步加载更多 + 4. 底部 App Tab 为单条悬浮胶囊壳层预览:车辆 / 业务 / 工作台(中间圆形仅图标)/ 消息 / 我的;非车辆入口仅提示;点卡片进详情 +- **闭环**:找到目标车并进入详情 +- **排期**:US-01 · M + +#### A2 · KPI 分类与异常筛选 +- **角色**:运维 / 业务 +- **起点**:需要按营运分类或证照/保险异常缩小范围 +- **怎么运作**: + 1. 横滑点选 KPI 摘要卡(全部营运/租赁/物流/库存/非运营/退出;每屏完整显示 3 张,翻页看其余 3 张;显示计数与相对占比) + 2. 筛选项行:品牌车型(品牌→车型)、运营城市(省→市,可只选省)、车辆状态(与 PC 字典一致,含调拨中 / 替换中等;无数据时计数为 0 仍可选);点「更多筛选」可再调证照状态、保险状态、出库状态、车辆来源、在线状态 + 3. 列表随条件刷新并从顶部重新加载;不展示列表总辆数 +- **闭环**:得到与 PC 同口径的筛选结果 +- **排期**:US-02 · M + +#### A3 · 卡片里程任务进度 +- **角色**:运维 / 业务 / 管理 +- **起点**:采购合同已为批次车辆关联里程要求与起止时间,并通过任务工单下发 +- **怎么运作**: + 1. 列表每张卡片固定展示「里程任务」区 + 2. 有工单:展示完成百分比、进度条、剩余公里;预计完成天数按近 7 个活跃日(日里程 > 0)平均日里程推算;文案后方展示完成预判标签(对照任务截止剩余日历天:预计无法完成 / 可完成 / 有完成风险) + 3. 无工单:仍保留区块,文案「暂无里程任务」 +- **闭环**:不进详情也能判断该车里程任务是否在轨、还差多少 +- **排期**:US-04 · S + +### Epic B · 详情只读(约 5 SP) + +#### B1 · 多 Tab 档案浏览 +- **角色**:全体手机用户 +- **起点**:从列表进入某车详情 +- **怎么运作**: + 1. 详情与列表共用一体浅蓝渐变底;顶栏透明,仅返回 + 车牌;不挂底部悬浮 Tab + 2. 摘要为浮起白卡:车牌、品牌型号、营运状态(与列表同文案/色点)、来源·城市·项目等标签、里程、证照/保险异常角标(无车辆实景大图) + 3. 横滑切换与 PC 一致的 11 个 Tab;选中态为浅底主色字(对齐列表 Chip) + 4. 各 Tab 仅展示信息(白卡 Section / 记录列表),无编辑按钮;检验有效期后方展示「剩余 x 天」标签;沪牌车在档案信息额外展示等级评定时间与剩余天数;证照信息 Tab 对齐证照管理八类证照;保险记录 Tab 对齐保险采购五险种与采购历史;租赁记录为纵向时间轴(近→远,未还车标注),并支持按客户名称 / 项目名称搜索;事故等业务 Tab 无种子时展示演示用例;无数据时为空态插画 +- **闭环**:不回 PC 也能看完档案与业务记录摘要 +- **排期**:US-03 · M + +## 5. 功能模块说明(正向 / 逆向) + +### 5.1 列表 + +**正向** +1. 默认展示「全部营运」KPI 下的卡片列表(首屏一批,触底异步加载更多;不含退出运营、不含非运营车辆) +2. 搜索、切 KPI、改筛选后列表刷新并从顶部重新加载;不展示总车辆数、无翻页按钮 +3. 证照/保险异常在卡片上可见 +4. 每张卡片标签下方展示「里程任务」:有工单则百分比 + 进度条 + 剩余/预计 + 完成预判标签;无工单则「暂无里程任务」 + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| 无匹配 | 展示「暂无匹配车辆」+ 引导文案;可清空搜索或重置筛选 | +| 仅筛选异常 | 只保留对应异常车辆 | +| 更多筛选多项叠加 | 与 KPI / 搜索 / 筛选项行条件同时生效(AND) | +| 已加载全部 | 底部提示「没有更多了」 | +| 里程任务已完成 | 进度 100%,文案「已完成」,无预判标签 | +| 有任务但近窗无活跃日 | 仍显示剩余公里,预计侧「暂无法预计」,预判标签为「剩余 x 天,预计有完成风险」 | +| 按活跃日推算赶不上截止 | 预判标签「剩余 x 天,预计无法完成」 | + +### 5.2 详情 + +**正向** +1. 进入详情后背景与列表一体连续;摘要卡可见状态、标签、里程与异常 +2. 11 个 Tab 均可切换;内容区白卡展示字段或记录 +3. 检验有效期后方展示剩余/已过期天数标签;沪牌车额外展示等级评定时间与剩余天数 +4. 证照信息含八类证照(行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表),字段与证照管理对齐;顶部索引单行横滑点选锚点 +5. 保险记录对齐保险采购:五险种卡片 + 当前保单摘要 + 采购/批单历史(纵向时间轴,只读) +6. 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审均为纵向时间轴(近→远);事故及右侧 Tab 支持时间快捷/自定义筛选(自定义为手机端原生滚轮选日);调拨卡片展示出发城市、到达城市、停车场、接收人、接收日(不展示路线与调出人);租赁可搜客户/项目,未还车显示「未还车」 +7. 业务 Tab 无种子时展示演示用例;租赁不足 3 条时补足 +8. 无记录时展示美化空态;无底部 App Tab,避免挡内容 + +**逆向 / 边界** + +| 情况 | 系统表现 | +|------|----------| +| 任意角色 | 均无编辑、删除、导入入口 | +| 返回 | 顶栏返回或系统返回回到列表;列表底栏样式不受详情影响 | +| 检验有效期为空 | 显示「未上传」,无剩余天数标签 | +| 非沪牌 | 不展示等级评定时间 / 剩余天数 | +| 未绑定加氢卡 | 显示「尚未绑定加氢卡」 | +| 某险种无采购记录 | 显示「暂无记录」,引导回 PC 保险采购 | +| 租赁未还车 | 还车字段显示「未还车」 | +| 租赁搜索无匹配 | 展示「暂无匹配记录」,可清空搜索 | +| 业务 Tab 无种子 | 展示稳定演示用例(非真实业务单据) | + +## 6. 关键业务逻辑 + +### 6.1 KPI 口径(与 PC 一致) + +| KPI | 口径 | +|-----|------| +| 全部营运(原「所有营运车辆」) | 非「非运营车辆」,且运营状态不为「退出运营」 | +| 租赁 / 物流 | 运营状态等于对应值 | +| 库存 | 可运营或待运营 | +| 非运营 | 台账类型为非运营车辆 | +| 退出运营 | 运营状态为退出运营 | + +### 6.2 异常口径 + +| 异常 | 口径 | +|------|------| +| 证照异常 | 检验有效期早于今天 | +| 保险异常 | 保险状态为「异常」 | + +### 6.3 更多筛选口径 + +| 筛选项 | 口径 | +|--------|------| +| 证照 / 保险 | 同 §6.2 | +| 出库状态 | 由车辆状态推导:未出库 = 待验车/未备车/已备车/待交车;已出库 = 其余有状态车辆(原型无独立出库字段) | +| 车辆来源 | 自有 / 外租 / 挂靠,与台账一致 | +| 在线状态 | 在线 / 离线,与台账一致 | + +规格全文见 [more-filters.md](./more-filters.md)。 + +### 6.4 里程任务进度(列表卡片) + +| 项 | 口径 | +|----|------| +| 来源 | 采购合同批次里程要求 + 时间起止 + 任务工单下发(原型为本地演示种子,未接真实 API) | +| 完成百分比 | 任务内已完成里程 / 目标里程,封顶 100%,展示保留 1 位小数 | +| 剩余公里 | 目标 − 已完成,下限 0 | +| 活跃日 | 当日行驶里程 > 0 km | +| 预计天数 D | 剩余 ÷ 近最多 7 个活跃日平均日里程,向上取整;无活跃日则「暂无法预计」 | +| 截止剩余日历天 x | 任务时间窗相对今天的剩余自然日 | +| 预判标签 | 用 D 对照 x:无法完成 / 有完成风险 / 可完成(见规格全文);无法完成时进度条与百分比为危险红,有完成风险为警告橙 | +| 无工单 | 仍展示区块,文案「暂无里程任务」 | + +规格全文见 [mileage-task.md](./mileage-task.md)。 + +### 6.5 检验有效期剩余天数 · 沪牌等级评定 + +| 项 | 口径 | +|----|------| +| 检验有效期 | 日期后方标签:剩余 x 天 / 已过期 x 天;空为「未上传」 | +| 沪牌判定 | 车牌以「沪」开头 | +| 等级评定时间 | 仅沪牌展示;空为「未上传」 | +| 剩余天数 | 仅沪牌展示;相对等级评定日计算 | + +规格全文见 [expire-remain-shanghai.md](./expire-remain-shanghai.md)。 + +### 6.6 证照信息(八类) + +与证照管理一致:行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表。规格全文见 [license-certs.md](./license-certs.md)。 + +### 6.7 保险记录(对齐保险采购) + +五险种(交强 / 商业 / 超赔 / 驾意 / 货物)+ 当前保单摘要 + 采购历史(纵向时间轴)。规格全文见 [insurance-records.md](./insurance-records.md)。 + +### 6.8 业务记录纵向时间轴 + +租赁 / 保险历史 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审统一近→远时间轴。 + +| 能力 | 口径 | +|------|------| +| 时间筛选 | 事故及右侧 Tab:全部 / 近30天 / 近90天 / 近1年 / 自定义;按各 Tab 关键时间过滤;无匹配可重置 | +| 自定义选日 | 手机端底部滚轮(年 / 月 / 日,精确到日);起日不得晚于止日;本模块选日入口统一该交互 | +| 调拨卡片 | 出发城市、到达城市、停车场、接收人、接收日;不展示「路线」合并行与「调出」人 | + +规格全文见 [biz-timeline.md](./biz-timeline.md)、[lease-records.md](./lease-records.md)。 + +### 6.9 业务记录演示用例与空态 + +无种子时 H5 本地生成演示用例;真正无数据时展示美化空态。规格全文见 [demo-biz-records.md](./demo-biz-records.md)。 + +## 7. 总览流程图 + +```mermaid +flowchart TB + entry[App 打开车辆资产 H5] --> list[列表:搜索 / KPI / 筛选] + list --> card[点车辆卡片] + card --> detail[详情摘要] + detail --> tabs[切换只读 Tab] + tabs --> back[返回列表] +``` + +## 8. 验收清单(产品 / 测试) + +- [ ] 独立原型可打开,桌面 PhoneShell / 窄屏可用 +- [ ] KPI 六类计数与同数据 PC 一致 +- [ ] 证照异常、保险异常筛选结果与 PC 规则一致 +- [ ] 「更多筛选」含证照 / 保险 / 出库 / 来源 / 在线;面板不超出手机操作区,内容可滚动;启用后角标点亮 +- [ ] 出库:未出库对应待验车/未备车/已备车/待交车;来源与在线与台账字段一致 +- [ ] 车辆状态选项与 PC 字典一致,含「调拨中」「替换中」 +- [ ] 搜索支持车牌 / VIN / 停车场 / 客户名称过滤列表;可一键清空搜索 +- [ ] 无匹配时有引导操作(清空搜索 / 重置筛选) +- [ ] 列表无总辆数、无翻页按钮;滚动触底可异步加载更多,到底提示「没有更多了」 +- [ ] 列表进详情:一体渐变底连续,无白顶栏色带;摘要为浮起白卡(状态/标签/里程/异常与列表同语义);详情无底栏 +- [ ] 详情检验有效期后方有「剩余 x 天」或「已过期 x 天」标签;空为「未上传」 +- [ ] 沪牌详情可见「等级评定时间」「剩余天数」;非沪牌不出现 +- [ ] 证照信息 Tab 可见八类证照,与证照管理种类一致 +- [ ] 保险记录可见五险种卡片、当前保单摘要与历史时间轴;整体状态口径与保险采购一致 +- [ ] 保单附件可预览与下载(原型演示) +- [ ] 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审为近→远时间轴;租赁未还车显示「未还车」 +- [ ] 调拨卡片可见出发城市、到达城市、停车场、接收人、接收日;无「路线」「调出」 +- [ ] 事故及右侧业务 Tab 可按时间快捷或自定义区间筛选;自定义起止为底部滚轮选日(精确到日),无匹配可重置 +- [ ] 租赁记录可按客户名称 / 项目名称搜索;无匹配可清空搜索 +- [ ] 任意车辆打开上述业务 Tab 可见列表(种子或演示用例);空态有插画与说明 +- [ ] 详情 11 个 Tab 可进入且只读,无编辑按钮 +- [ ] 列表卡片标签下有「里程任务」区:有任务可见百分比、进度条、剩余公里与预计天数;无任务显示「暂无里程任务」 +- [ ] 预计天数按近 7 活跃日均里程推算;无活跃日时显示「暂无法预计」 +- [ ] 进行中任务文案后方有完成预判标签(剩余 x 天 + 预计无法完成 / 可完成 / 有完成风险) +- [ ] 页脚说明「修改请在 PC」可见 + +**交付口径** +本期交付可嵌入 App 的车辆资产只读 H5:能查、能筛异常、能看详情全 Tab,并在列表卡片扫读里程任务进度;写操作与异常处理仍回 PC。 diff --git a/src/prototypes/oneos-h5-vehicle-assets/annotation-source.json b/src/prototypes/oneos-h5-vehicle-assets/annotation-source.json new file mode 100644 index 0000000..7f1343c --- /dev/null +++ b/src/prototypes/oneos-h5-vehicle-assets/annotation-source.json @@ -0,0 +1,293 @@ +{ + "documentVersion": 1, + "format": "axhub-annotation-source", + "data": { + "version": 2, + "prototypeName": "oneos-h5-vehicle-assets", + "pageId": "list", + "updatedAt": 1784600555394, + "nodes": [ + { + "id": "h5-va-search", + "index": 1, + "title": "车牌搜索", + "pageId": "list", + "color": "#32a06e", + "hasMarkdown": true, + "annotationText": "支持按车牌关键词过滤列表。", + "locator": { + "selectors": [ + "[data-annotation-id='h5-va-search']" + ], + "fingerprint": "search|list", + "path": [] + }, + "images": [], + "controls": [], + "createdAt": 1784557399047, + "updatedAt": 1784557399047 + }, + { + "id": "h5-va-kpi", + "index": 2, + "title": "KPI 分类", + "pageId": "list", + "color": "#2b9260", + "hasMarkdown": true, + "annotationText": "与 PC 车辆资产 KPI 口径一致;「全部营运」不含退出运营、不含非运营车辆;摘要卡展示计数与占比。", + "locator": { + "selectors": [ + "[data-annotation-id='h5-va-kpi']" + ], + "fingerprint": "kpi|list", + "path": [] + }, + "images": [], + "controls": [], + "createdAt": 1784557399047, + "updatedAt": 1784570335522 + }, + { + "id": "h5-va-filter", + "index": 3, + "title": "异常筛选", + "pageId": "list", + "color": "#d97706", + "hasMarkdown": true, + "annotationText": "筛选项行:品牌车型 / 运营城市(先省后市,可只选省)/ 车辆状态 + 更多筛选(证照 / 保险 / 出库 / 来源 / 在线)。", + "locator": { + "selectors": [ + "[data-annotation-id='h5-va-filter']" + ], + "fingerprint": "filter|list", + "path": [] + }, + "images": [], + "controls": [], + "createdAt": 1784557399047, + "updatedAt": 1784570606914 + }, + { + "id": "h5-va-card", + "index": 4, + "title": "车辆卡片", + "pageId": "list", + "color": "#52525b", + "hasMarkdown": true, + "annotationText": "Boutique 横卡:车牌、车型、标签、营运状态、里程与异常角标;点击进入详情。", + "locator": { + "selectors": [ + "[data-annotation-id='h5-va-card']" + ], + "fingerprint": "card|list", + "path": [] + }, + "images": [], + "controls": [], + "createdAt": 1784557399047, + "updatedAt": 1784557399047 + }, + { + "id": "h5-va-detail", + "index": 5, + "title": "详情摘要", + "pageId": "detail", + "color": "#32a06e", + "hasMarkdown": true, + "annotationText": "车牌、车型、营运状态与证照/保险异常;全程只读。", + "locator": { + "selectors": [ + "[data-annotation-id='h5-va-detail']" + ], + "fingerprint": "detail|detail", + "path": [] + }, + "images": [], + "controls": [], + "createdAt": 1784557399047, + "updatedAt": 1784557399047 + }, + { + "id": "h5-va-tabs", + "index": 6, + "title": "详情 Tab", + "pageId": "detail", + "color": "#2b9260", + "hasMarkdown": true, + "annotationText": "11 个 Tab 对齐 PC,内容只读。", + "locator": { + "selectors": [ + "[data-annotation-id='h5-va-tabs']" + ], + "fingerprint": "tabs|detail", + "path": [] + }, + "images": [], + "controls": [], + "createdAt": 1784557399047, + "updatedAt": 1784557399047 + }, + { + "id": "h5-va-mileage-task", + "index": 7, + "title": "里程任务进度", + "pageId": "list", + "color": "#2563eb", + "hasMarkdown": true, + "annotationText": "标签下方展示里程任务:有工单显示百分比、进度条、剩余公里与预计天数,文案后方完成预判标签(剩余x天+预计无法完成/可完成/有完成风险);无工单显示「暂无里程任务」。", + "locator": { + "selectors": [ + "[data-annotation-id='h5-va-mileage-task']" + ], + "fingerprint": "mileage-task|list", + "path": [] + }, + "images": [], + "controls": [], + "createdAt": 1784568570662, + "updatedAt": 1784568901056 + } + ] + }, + "markdownMap": {}, + "assetMap": {}, + "directory": { + "nodes": [ + { + "type": "folder", + "id": "h5-va-doc-root", + "title": "车辆资产(H5)说明", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "h5-va-doc-prd", + "title": "产品需求说明(PRD)", + "markdownPath": ".spec/requirements-prd.md", + "markdown": "# 车辆资产(H5)· 产品需求说明(全模块)\n\n## 1. 一句话与目标\n\n**一句话** \n在 App / 手机浏览器提供与 PC「车辆资产」口径一致的只读查车与异常浏览能力。\n\n**要解决的问题** \n- 外勤与业务人员无法在手机上快速按车牌查车、看营运分类与证照/保险异常 \n- PC 宽表页不适合塞进 WebView,需要独立手机信息架构 \n\n**本期目标** \n1. 列表:车牌搜索、KPI 分类横滑、证照/保险异常筛选、车辆卡片(含里程任务进度) \n2. 详情:多 Tab 对齐 PC,全程只读浏览 \n3. 全角色同一套界面;手机端不做编辑 \n\n**非目标(本期不做)** \n- 任意字段编辑(含 Admin) \n- 导入 / 导出、催办、跳转证照管理或保险采购等业务处理页 \n- 里程任务工单下发 / 编辑、从卡片跳转工单详情、按截止日逾期标红 \n- 在租车型占比等 PC 尾部分析入口 \n- 将 PC 页做响应式自适应 \n\n## 2. 模块边界\n\n```mermaid\nflowchart LR\n App[App WebView] --> H5[车辆资产 H5]\n H5 -.口径一致.-> PC[车辆资产 PC]\n H5 -.只读.-> Data[车辆台账与关联记录]\n```\n\n| 业务线 / 子域 | 做什么 | 不做什么 |\n|---------------|--------|----------|\n| 查车列表 | 搜索、KPI、异常筛选、卡片进详情、卡片里程任务进度 | 批量操作、写操作、下发/编辑工单 |\n| 车辆详情 | 11 个 Tab 只读浏览 | 行内编辑、跳转处理 |\n\n**外部依赖(产品口径)** \n\n| 依赖方 | 交互方式 | 产品要求 |\n|--------|----------|----------|\n| 车辆资产 PC | 同一套台账与统计口径 | KPI / 证照异常 / 保险异常定义一致;台账数据同源(业务导表) |\n| 采购合同 / 任务工单 | 合同关联批次里程要求与起止;工单下发后回写进度 | 列表卡片只读展示完成百分比、剩余公里与预计天数;无工单时仍展示「暂无里程任务」 |\n| App 壳 | 内嵌打开本 H5 | 固定打开手机页,不打开 PC 宽表地址 |\n\n**关键约束** \n- 手机端明确提示:修改请在 PC 完成 \n- 筛选与 KPI 叠加:先按 KPI,再按证照/保险条件 \n- 列表车辆数据与 PC「车辆资产」共用业务导表台账(含真实车牌、客户、运营状态分布) \n\n## 3. 用户与角色\n\n| 角色 | 主要目标 |\n|------|----------|\n| 运维 / 业务 / 管理等全角色 | 同一套界面查车、看异常与档案摘要 |\n| Admin | 与普通用户相同;手机端仍无编辑(改数回 PC) |\n\n## 4. 用户故事与故事点(业务条线说明口径)\n\n合计约 **15 SP**。\n\n### Epic A · 列表查车(约 10 SP)\n\n#### A1 · 车牌搜索与卡片列表\n- **角色**:全体手机用户 \n- **起点**:从 App 进入车辆资产 H5 \n- **怎么运作**:\n 1. 顶栏左对齐「车辆资产」;搜索栏提示「搜索车牌 / VIN / 停车场 / 客户名称」,支持按这些关键词过滤;右侧扫码为占位提示 \n 2. KPI 横滑摘要卡(每屏 3 张、共 2 页;图标 + 计数 + 占比);下方固定筛选项行:品牌车型 / 运营城市(省→市,可只选省)/ 车辆状态 +「更多筛选」(证照 / 保险 / 出库 / 来源 / 在线);前三项为 App 原生底部选择器,选项来自业务导表;更多筛选面板限制在手机操作区内并可滚动 \n 3. 列表横卡含车牌、车型、来源标签、按运营状态变化的次要标签(租赁/自营→客户名省略;库存→未备车警告/已备车正常;非运营→非运营车辆;退出→三方退租/销售/报废)、营运状态圆点、里程与异常角标(不含车辆照片);标签下方展示「里程任务」分隔区(有任务:百分比 + 进度条 + 剩余公里/预计天数 + 完成预判标签;无任务:「暂无里程任务」);列表不展示总车辆数,不提供翻页按钮,滚动触底异步加载更多 \n 4. 底部 App Tab 为单条悬浮胶囊壳层预览:车辆 / 业务 / 工作台(中间圆形仅图标)/ 消息 / 我的;非车辆入口仅提示;点卡片进详情 \n- **闭环**:找到目标车并进入详情 \n- **排期**:US-01 · M \n\n#### A2 · KPI 分类与异常筛选\n- **角色**:运维 / 业务 \n- **起点**:需要按营运分类或证照/保险异常缩小范围 \n- **怎么运作**:\n 1. 横滑点选 KPI 摘要卡(全部营运/租赁/物流/库存/非运营/退出;每屏完整显示 3 张,翻页看其余 3 张;显示计数与相对占比) \n 2. 筛选项行:品牌车型(品牌→车型)、运营城市(省→市,可只选省)、车辆状态(与 PC 字典一致,含调拨中 / 替换中等;无数据时计数为 0 仍可选);点「更多筛选」可再调证照状态、保险状态、出库状态、车辆来源、在线状态 \n 3. 列表随条件刷新并从顶部重新加载;不展示列表总辆数 \n- **闭环**:得到与 PC 同口径的筛选结果 \n- **排期**:US-02 · M \n\n#### A3 · 卡片里程任务进度\n- **角色**:运维 / 业务 / 管理 \n- **起点**:采购合同已为批次车辆关联里程要求与起止时间,并通过任务工单下发 \n- **怎么运作**:\n 1. 列表每张卡片固定展示「里程任务」区 \n 2. 有工单:展示完成百分比、进度条、剩余公里;预计完成天数按近 7 个活跃日(日里程 > 0)平均日里程推算;文案后方展示完成预判标签(对照任务截止剩余日历天:预计无法完成 / 可完成 / 有完成风险) \n 3. 无工单:仍保留区块,文案「暂无里程任务」 \n- **闭环**:不进详情也能判断该车里程任务是否在轨、还差多少 \n- **排期**:US-04 · S \n\n### Epic B · 详情只读(约 5 SP)\n\n#### B1 · 多 Tab 档案浏览\n- **角色**:全体手机用户 \n- **起点**:从列表进入某车详情 \n- **怎么运作**:\n 1. 详情与列表共用一体浅蓝渐变底;顶栏透明,仅返回 + 车牌;不挂底部悬浮 Tab \n 2. 摘要为浮起白卡:车牌、品牌型号、营运状态(与列表同文案/色点)、来源·城市·项目等标签、里程、证照/保险异常角标(无车辆实景大图) \n 3. 横滑切换与 PC 一致的 11 个 Tab;选中态为浅底主色字(对齐列表 Chip) \n 4. 各 Tab 仅展示信息(白卡 Section / 记录列表),无编辑按钮;检验有效期后方展示「剩余 x 天」标签;沪牌车在档案信息额外展示等级评定时间与剩余天数;证照信息 Tab 对齐证照管理八类证照;保险记录 Tab 对齐保险采购五险种与采购历史;租赁记录为纵向时间轴(近→远,未还车标注),并支持按客户名称 / 项目名称搜索;事故等业务 Tab 无种子时展示演示用例;无数据时为空态插画 \n- **闭环**:不回 PC 也能看完档案与业务记录摘要 \n- **排期**:US-03 · M \n\n## 5. 功能模块说明(正向 / 逆向)\n\n### 5.1 列表\n\n**正向** \n1. 默认展示「全部营运」KPI 下的卡片列表(首屏一批,触底异步加载更多;不含退出运营、不含非运营车辆) \n2. 搜索、切 KPI、改筛选后列表刷新并从顶部重新加载;不展示总车辆数、无翻页按钮 \n3. 证照/保险异常在卡片上可见 \n4. 每张卡片标签下方展示「里程任务」:有工单则百分比 + 进度条 + 剩余/预计 + 完成预判标签;无工单则「暂无里程任务」 \n\n**逆向 / 边界** \n\n| 情况 | 系统表现 |\n|------|----------|\n| 无匹配 | 展示「暂无匹配车辆」+ 引导文案;可清空搜索或重置筛选 |\n| 仅筛选异常 | 只保留对应异常车辆 |\n| 更多筛选多项叠加 | 与 KPI / 搜索 / 筛选项行条件同时生效(AND) |\n| 已加载全部 | 底部提示「没有更多了」 |\n| 里程任务已完成 | 进度 100%,文案「已完成」,无预判标签 |\n| 有任务但近窗无活跃日 | 仍显示剩余公里,预计侧「暂无法预计」,预判标签为「剩余 x 天,预计有完成风险」 |\n| 按活跃日推算赶不上截止 | 预判标签「剩余 x 天,预计无法完成」 |\n\n### 5.2 详情\n\n**正向** \n1. 进入详情后背景与列表一体连续;摘要卡可见状态、标签、里程与异常 \n2. 11 个 Tab 均可切换;内容区白卡展示字段或记录 \n3. 检验有效期后方展示剩余/已过期天数标签;沪牌车额外展示等级评定时间与剩余天数 \n4. 证照信息含八类证照(行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表),字段与证照管理对齐;顶部索引单行横滑点选锚点 \n5. 保险记录对齐保险采购:五险种卡片 + 当前保单摘要 + 采购/批单历史(纵向时间轴,只读) \n6. 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审均为纵向时间轴(近→远);事故及右侧 Tab 支持时间快捷/自定义筛选(自定义为手机端原生滚轮选日);调拨卡片展示出发城市、到达城市、停车场、接收人、接收日(不展示路线与调出人);租赁可搜客户/项目,未还车显示「未还车」 \n7. 业务 Tab 无种子时展示演示用例;租赁不足 3 条时补足 \n8. 无记录时展示美化空态;无底部 App Tab,避免挡内容 \n\n**逆向 / 边界** \n\n| 情况 | 系统表现 |\n|------|----------|\n| 任意角色 | 均无编辑、删除、导入入口 |\n| 返回 | 顶栏返回或系统返回回到列表;列表底栏样式不受详情影响 |\n| 检验有效期为空 | 显示「未上传」,无剩余天数标签 |\n| 非沪牌 | 不展示等级评定时间 / 剩余天数 |\n| 未绑定加氢卡 | 显示「尚未绑定加氢卡」 |\n| 某险种无采购记录 | 显示「暂无记录」,引导回 PC 保险采购 |\n| 租赁未还车 | 还车字段显示「未还车」 |\n| 租赁搜索无匹配 | 展示「暂无匹配记录」,可清空搜索 |\n| 业务 Tab 无种子 | 展示稳定演示用例(非真实业务单据) |\n\n## 6. 关键业务逻辑\n\n### 6.1 KPI 口径(与 PC 一致)\n\n| KPI | 口径 |\n|-----|------|\n| 全部营运(原「所有营运车辆」) | 非「非运营车辆」,且运营状态不为「退出运营」 |\n| 租赁 / 物流 | 运营状态等于对应值 |\n| 库存 | 可运营或待运营 |\n| 非运营 | 台账类型为非运营车辆 |\n| 退出运营 | 运营状态为退出运营 |\n\n### 6.2 异常口径\n\n| 异常 | 口径 |\n|------|------|\n| 证照异常 | 检验有效期早于今天 |\n| 保险异常 | 保险状态为「异常」 |\n\n### 6.3 更多筛选口径\n\n| 筛选项 | 口径 |\n|--------|------|\n| 证照 / 保险 | 同 §6.2 |\n| 出库状态 | 由车辆状态推导:未出库 = 待验车/未备车/已备车/待交车;已出库 = 其余有状态车辆(原型无独立出库字段) |\n| 车辆来源 | 自有 / 外租 / 挂靠,与台账一致 |\n| 在线状态 | 在线 / 离线,与台账一致 |\n\n规格全文见 [more-filters.md](./more-filters.md)。\n\n### 6.4 里程任务进度(列表卡片)\n\n| 项 | 口径 |\n|----|------|\n| 来源 | 采购合同批次里程要求 + 时间起止 + 任务工单下发(原型为本地演示种子,未接真实 API) |\n| 完成百分比 | 任务内已完成里程 / 目标里程,封顶 100%,展示保留 1 位小数 |\n| 剩余公里 | 目标 − 已完成,下限 0 |\n| 活跃日 | 当日行驶里程 > 0 km |\n| 预计天数 D | 剩余 ÷ 近最多 7 个活跃日平均日里程,向上取整;无活跃日则「暂无法预计」 |\n| 截止剩余日历天 x | 任务时间窗相对今天的剩余自然日 |\n| 预判标签 | 用 D 对照 x:无法完成 / 有完成风险 / 可完成(见规格全文);无法完成时进度条与百分比为危险红,有完成风险为警告橙 |\n| 无工单 | 仍展示区块,文案「暂无里程任务」 |\n\n规格全文见 [mileage-task.md](./mileage-task.md)。\n\n### 6.5 检验有效期剩余天数 · 沪牌等级评定\n\n| 项 | 口径 |\n|----|------|\n| 检验有效期 | 日期后方标签:剩余 x 天 / 已过期 x 天;空为「未上传」 |\n| 沪牌判定 | 车牌以「沪」开头 |\n| 等级评定时间 | 仅沪牌展示;空为「未上传」 |\n| 剩余天数 | 仅沪牌展示;相对等级评定日计算 |\n\n规格全文见 [expire-remain-shanghai.md](./expire-remain-shanghai.md)。\n\n### 6.6 证照信息(八类)\n\n与证照管理一致:行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表。规格全文见 [license-certs.md](./license-certs.md)。\n\n### 6.7 保险记录(对齐保险采购)\n\n五险种(交强 / 商业 / 超赔 / 驾意 / 货物)+ 当前保单摘要 + 采购历史(纵向时间轴)。规格全文见 [insurance-records.md](./insurance-records.md)。\n\n### 6.8 业务记录纵向时间轴\n\n租赁 / 保险历史 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审统一近→远时间轴。\n\n| 能力 | 口径 |\n|------|------|\n| 时间筛选 | 事故及右侧 Tab:全部 / 近30天 / 近90天 / 近1年 / 自定义;按各 Tab 关键时间过滤;无匹配可重置 |\n| 自定义选日 | 手机端底部滚轮(年 / 月 / 日,精确到日);起日不得晚于止日;本模块选日入口统一该交互 |\n| 调拨卡片 | 出发城市、到达城市、停车场、接收人、接收日;不展示「路线」合并行与「调出」人 |\n\n规格全文见 [biz-timeline.md](./biz-timeline.md)、[lease-records.md](./lease-records.md)。\n\n### 6.9 业务记录演示用例与空态\n\n无种子时 H5 本地生成演示用例;真正无数据时展示美化空态。规格全文见 [demo-biz-records.md](./demo-biz-records.md)。\n\n## 7. 总览流程图\n\n```mermaid\nflowchart TB\n entry[App 打开车辆资产 H5] --> list[列表:搜索 / KPI / 筛选]\n list --> card[点车辆卡片]\n card --> detail[详情摘要]\n detail --> tabs[切换只读 Tab]\n tabs --> back[返回列表]\n```\n\n## 8. 验收清单(产品 / 测试)\n\n- [ ] 独立原型可打开,桌面 PhoneShell / 窄屏可用 \n- [ ] KPI 六类计数与同数据 PC 一致 \n- [ ] 证照异常、保险异常筛选结果与 PC 规则一致 \n- [ ] 「更多筛选」含证照 / 保险 / 出库 / 来源 / 在线;面板不超出手机操作区,内容可滚动;启用后角标点亮 \n- [ ] 出库:未出库对应待验车/未备车/已备车/待交车;来源与在线与台账字段一致 \n- [ ] 车辆状态选项与 PC 字典一致,含「调拨中」「替换中」 \n- [ ] 搜索支持车牌 / VIN / 停车场 / 客户名称过滤列表;可一键清空搜索 \n- [ ] 无匹配时有引导操作(清空搜索 / 重置筛选) \n- [ ] 列表无总辆数、无翻页按钮;滚动触底可异步加载更多,到底提示「没有更多了」 \n- [ ] 列表进详情:一体渐变底连续,无白顶栏色带;摘要为浮起白卡(状态/标签/里程/异常与列表同语义);详情无底栏 \n- [ ] 详情检验有效期后方有「剩余 x 天」或「已过期 x 天」标签;空为「未上传」 \n- [ ] 沪牌详情可见「等级评定时间」「剩余天数」;非沪牌不出现 \n- [ ] 证照信息 Tab 可见八类证照,与证照管理种类一致 \n- [ ] 保险记录可见五险种卡片、当前保单摘要与历史时间轴;整体状态口径与保险采购一致 \n- [ ] 保单附件可预览与下载(原型演示) \n- [ ] 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审为近→远时间轴;租赁未还车显示「未还车」 \n- [ ] 调拨卡片可见出发城市、到达城市、停车场、接收人、接收日;无「路线」「调出」 \n- [ ] 事故及右侧业务 Tab 可按时间快捷或自定义区间筛选;自定义起止为底部滚轮选日(精确到日),无匹配可重置 \n- [ ] 租赁记录可按客户名称 / 项目名称搜索;无匹配可清空搜索 \n- [ ] 任意车辆打开上述业务 Tab 可见列表(种子或演示用例);空态有插画与说明 \n- [ ] 详情 11 个 Tab 可进入且只读,无编辑按钮 \n- [ ] 列表卡片标签下有「里程任务」区:有任务可见百分比、进度条、剩余公里与预计天数;无任务显示「暂无里程任务」 \n- [ ] 预计天数按近 7 活跃日均里程推算;无活跃日时显示「暂无法预计」 \n- [ ] 进行中任务文案后方有完成预判标签(剩余 x 天 + 预计无法完成 / 可完成 / 有完成风险) \n- [ ] 页脚说明「修改请在 PC」可见 \n\n**交付口径** \n本期交付可嵌入 App 的车辆资产只读 H5:能查、能筛异常、能看详情全 Tab,并在列表卡片扫读里程任务进度;写操作与异常处理仍回 PC。\n" + }, + { + "type": "folder", + "id": "h5-va-doc-prd-chapters", + "title": "PRD 分章", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "h5-va-prd-ch-01", + "title": "1. 一句话与目标", + "markdown": "## 1. 一句话与目标\n\n**一句话** \n在 App / 手机浏览器提供与 PC「车辆资产」口径一致的只读查车与异常浏览能力。\n\n**要解决的问题** \n- 外勤与业务人员无法在手机上快速按车牌查车、看营运分类与证照/保险异常 \n- PC 宽表页不适合塞进 WebView,需要独立手机信息架构 \n\n**本期目标** \n1. 列表:车牌搜索、KPI 分类横滑、证照/保险异常筛选、车辆卡片(含里程任务进度) \n2. 详情:多 Tab 对齐 PC,全程只读浏览 \n3. 全角色同一套界面;手机端不做编辑 \n\n**非目标(本期不做)** \n- 任意字段编辑(含 Admin) \n- 导入 / 导出、催办、跳转证照管理或保险采购等业务处理页 \n- 里程任务工单下发 / 编辑、从卡片跳转工单详情、按截止日逾期标红 \n- 在租车型占比等 PC 尾部分析入口 \n- 将 PC 页做响应式自适应\n" + }, + { + "type": "markdown", + "id": "h5-va-prd-ch-02", + "title": "2. 模块边界", + "markdown": "## 2. 模块边界\n\n```mermaid\nflowchart LR\n App[App WebView] --> H5[车辆资产 H5]\n H5 -.口径一致.-> PC[车辆资产 PC]\n H5 -.只读.-> Data[车辆台账与关联记录]\n```\n\n| 业务线 / 子域 | 做什么 | 不做什么 |\n|---------------|--------|----------|\n| 查车列表 | 搜索、KPI、异常筛选、卡片进详情、卡片里程任务进度 | 批量操作、写操作、下发/编辑工单 |\n| 车辆详情 | 11 个 Tab 只读浏览 | 行内编辑、跳转处理 |\n\n**外部依赖(产品口径)** \n\n| 依赖方 | 交互方式 | 产品要求 |\n|--------|----------|----------|\n| 车辆资产 PC | 同一套台账与统计口径 | KPI / 证照异常 / 保险异常定义一致;台账数据同源(业务导表) |\n| 采购合同 / 任务工单 | 合同关联批次里程要求与起止;工单下发后回写进度 | 列表卡片只读展示完成百分比、剩余公里与预计天数;无工单时仍展示「暂无里程任务」 |\n| App 壳 | 内嵌打开本 H5 | 固定打开手机页,不打开 PC 宽表地址 |\n\n**关键约束** \n- 手机端明确提示:修改请在 PC 完成 \n- 筛选与 KPI 叠加:先按 KPI,再按证照/保险条件 \n- 列表车辆数据与 PC「车辆资产」共用业务导表台账(含真实车牌、客户、运营状态分布)\n" + }, + { + "type": "markdown", + "id": "h5-va-prd-ch-03", + "title": "3. 用户与角色", + "markdown": "## 3. 用户与角色\n\n| 角色 | 主要目标 |\n|------|----------|\n| 运维 / 业务 / 管理等全角色 | 同一套界面查车、看异常与档案摘要 |\n| Admin | 与普通用户相同;手机端仍无编辑(改数回 PC) |\n" + }, + { + "type": "markdown", + "id": "h5-va-prd-ch-04", + "title": "4. 用户故事与故事点(业务条线说明口径)", + "markdown": "## 4. 用户故事与故事点(业务条线说明口径)\n\n合计约 **15 SP**。\n\n### Epic A · 列表查车(约 10 SP)\n\n#### A1 · 车牌搜索与卡片列表\n- **角色**:全体手机用户 \n- **起点**:从 App 进入车辆资产 H5 \n- **怎么运作**:\n 1. 顶栏左对齐「车辆资产」;搜索栏提示「搜索车牌 / VIN / 停车场 / 客户名称」,支持按这些关键词过滤;右侧扫码为占位提示 \n 2. KPI 横滑摘要卡(每屏 3 张、共 2 页;图标 + 计数 + 占比);下方固定筛选项行:品牌车型 / 运营城市(省→市,可只选省)/ 车辆状态 +「更多筛选」(证照 / 保险 / 出库 / 来源 / 在线);前三项为 App 原生底部选择器,选项来自业务导表;更多筛选面板限制在手机操作区内并可滚动 \n 3. 列表横卡含车牌、车型、来源标签、按运营状态变化的次要标签(租赁/自营→客户名省略;库存→未备车警告/已备车正常;非运营→非运营车辆;退出→三方退租/销售/报废)、营运状态圆点、里程与异常角标(不含车辆照片);标签下方展示「里程任务」分隔区(有任务:百分比 + 进度条 + 剩余公里/预计天数 + 完成预判标签;无任务:「暂无里程任务」);列表不展示总车辆数,不提供翻页按钮,滚动触底异步加载更多 \n 4. 底部 App Tab 为单条悬浮胶囊壳层预览:车辆 / 业务 / 工作台(中间圆形仅图标)/ 消息 / 我的;非车辆入口仅提示;点卡片进详情 \n- **闭环**:找到目标车并进入详情 \n- **排期**:US-01 · M \n\n#### A2 · KPI 分类与异常筛选\n- **角色**:运维 / 业务 \n- **起点**:需要按营运分类或证照/保险异常缩小范围 \n- **怎么运作**:\n 1. 横滑点选 KPI 摘要卡(全部营运/租赁/物流/库存/非运营/退出;每屏完整显示 3 张,翻页看其余 3 张;显示计数与相对占比) \n 2. 筛选项行:品牌车型(品牌→车型)、运营城市(省→市,可只选省)、车辆状态(与 PC 字典一致,含调拨中 / 替换中等;无数据时计数为 0 仍可选);点「更多筛选」可再调证照状态、保险状态、出库状态、车辆来源、在线状态 \n 3. 列表随条件刷新并从顶部重新加载;不展示列表总辆数 \n- **闭环**:得到与 PC 同口径的筛选结果 \n- **排期**:US-02 · M \n\n#### A3 · 卡片里程任务进度\n- **角色**:运维 / 业务 / 管理 \n- **起点**:采购合同已为批次车辆关联里程要求与起止时间,并通过任务工单下发 \n- **怎么运作**:\n 1. 列表每张卡片固定展示「里程任务」区 \n 2. 有工单:展示完成百分比、进度条、剩余公里;预计完成天数按近 7 个活跃日(日里程 > 0)平均日里程推算;文案后方展示完成预判标签(对照任务截止剩余日历天:预计无法完成 / 可完成 / 有完成风险) \n 3. 无工单:仍保留区块,文案「暂无里程任务」 \n- **闭环**:不进详情也能判断该车里程任务是否在轨、还差多少 \n- **排期**:US-04 · S \n\n### Epic B · 详情只读(约 5 SP)\n\n#### B1 · 多 Tab 档案浏览\n- **角色**:全体手机用户 \n- **起点**:从列表进入某车详情 \n- **怎么运作**:\n 1. 详情与列表共用一体浅蓝渐变底;顶栏透明,仅返回 + 车牌;不挂底部悬浮 Tab \n 2. 摘要为浮起白卡:车牌、品牌型号、营运状态(与列表同文案/色点)、来源·城市·项目等标签、里程、证照/保险异常角标(无车辆实景大图) \n 3. 横滑切换与 PC 一致的 11 个 Tab;选中态为浅底主色字(对齐列表 Chip) \n 4. 各 Tab 仅展示信息(白卡 Section / 记录列表),无编辑按钮;检验有效期后方展示「剩余 x 天」标签;沪牌车在档案信息额外展示等级评定时间与剩余天数;证照信息 Tab 对齐证照管理八类证照;保险记录 Tab 对齐保险采购五险种与采购历史;租赁记录为纵向时间轴(近→远,未还车标注),并支持按客户名称 / 项目名称搜索;事故等业务 Tab 无种子时展示演示用例;无数据时为空态插画 \n- **闭环**:不回 PC 也能看完档案与业务记录摘要 \n- **排期**:US-03 · M\n" + }, + { + "type": "markdown", + "id": "h5-va-prd-ch-05", + "title": "5. 功能模块说明(正向 / 逆向)", + "markdown": "## 5. 功能模块说明(正向 / 逆向)\n\n### 5.1 列表\n\n**正向** \n1. 默认展示「全部营运」KPI 下的卡片列表(首屏一批,触底异步加载更多;不含退出运营、不含非运营车辆) \n2. 搜索、切 KPI、改筛选后列表刷新并从顶部重新加载;不展示总车辆数、无翻页按钮 \n3. 证照/保险异常在卡片上可见 \n4. 每张卡片标签下方展示「里程任务」:有工单则百分比 + 进度条 + 剩余/预计 + 完成预判标签;无工单则「暂无里程任务」 \n\n**逆向 / 边界** \n\n| 情况 | 系统表现 |\n|------|----------|\n| 无匹配 | 展示「暂无匹配车辆」+ 引导文案;可清空搜索或重置筛选 |\n| 仅筛选异常 | 只保留对应异常车辆 |\n| 更多筛选多项叠加 | 与 KPI / 搜索 / 筛选项行条件同时生效(AND) |\n| 已加载全部 | 底部提示「没有更多了」 |\n| 里程任务已完成 | 进度 100%,文案「已完成」,无预判标签 |\n| 有任务但近窗无活跃日 | 仍显示剩余公里,预计侧「暂无法预计」,预判标签为「剩余 x 天,预计有完成风险」 |\n| 按活跃日推算赶不上截止 | 预判标签「剩余 x 天,预计无法完成」 |\n\n### 5.2 详情\n\n**正向** \n1. 进入详情后背景与列表一体连续;摘要卡可见状态、标签、里程与异常 \n2. 11 个 Tab 均可切换;内容区白卡展示字段或记录 \n3. 检验有效期后方展示剩余/已过期天数标签;沪牌车额外展示等级评定时间与剩余天数 \n4. 证照信息含八类证照(行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表),字段与证照管理对齐;顶部索引单行横滑点选锚点 \n5. 保险记录对齐保险采购:五险种卡片 + 当前保单摘要 + 采购/批单历史(纵向时间轴,只读) \n6. 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审均为纵向时间轴(近→远);事故及右侧 Tab 支持时间快捷/自定义筛选(自定义为手机端原生滚轮选日);调拨卡片展示出发城市、到达城市、停车场、接收人、接收日(不展示路线与调出人);租赁可搜客户/项目,未还车显示「未还车」 \n7. 业务 Tab 无种子时展示演示用例;租赁不足 3 条时补足 \n8. 无记录时展示美化空态;无底部 App Tab,避免挡内容 \n\n**逆向 / 边界** \n\n| 情况 | 系统表现 |\n|------|----------|\n| 任意角色 | 均无编辑、删除、导入入口 |\n| 返回 | 顶栏返回或系统返回回到列表;列表底栏样式不受详情影响 |\n| 检验有效期为空 | 显示「未上传」,无剩余天数标签 |\n| 非沪牌 | 不展示等级评定时间 / 剩余天数 |\n| 未绑定加氢卡 | 显示「尚未绑定加氢卡」 |\n| 某险种无采购记录 | 显示「暂无记录」,引导回 PC 保险采购 |\n| 租赁未还车 | 还车字段显示「未还车」 |\n| 租赁搜索无匹配 | 展示「暂无匹配记录」,可清空搜索 |\n| 业务 Tab 无种子 | 展示稳定演示用例(非真实业务单据) |\n" + }, + { + "type": "markdown", + "id": "h5-va-prd-ch-06", + "title": "6. 关键业务逻辑", + "markdown": "## 6. 关键业务逻辑\n\n### 6.1 KPI 口径(与 PC 一致)\n\n| KPI | 口径 |\n|-----|------|\n| 全部营运(原「所有营运车辆」) | 非「非运营车辆」,且运营状态不为「退出运营」 |\n| 租赁 / 物流 | 运营状态等于对应值 |\n| 库存 | 可运营或待运营 |\n| 非运营 | 台账类型为非运营车辆 |\n| 退出运营 | 运营状态为退出运营 |\n\n### 6.2 异常口径\n\n| 异常 | 口径 |\n|------|------|\n| 证照异常 | 检验有效期早于今天 |\n| 保险异常 | 保险状态为「异常」 |\n\n### 6.3 更多筛选口径\n\n| 筛选项 | 口径 |\n|--------|------|\n| 证照 / 保险 | 同 §6.2 |\n| 出库状态 | 由车辆状态推导:未出库 = 待验车/未备车/已备车/待交车;已出库 = 其余有状态车辆(原型无独立出库字段) |\n| 车辆来源 | 自有 / 外租 / 挂靠,与台账一致 |\n| 在线状态 | 在线 / 离线,与台账一致 |\n\n规格全文见 [more-filters.md](./more-filters.md)。\n\n### 6.4 里程任务进度(列表卡片)\n\n| 项 | 口径 |\n|----|------|\n| 来源 | 采购合同批次里程要求 + 时间起止 + 任务工单下发(原型为本地演示种子,未接真实 API) |\n| 完成百分比 | 任务内已完成里程 / 目标里程,封顶 100%,展示保留 1 位小数 |\n| 剩余公里 | 目标 − 已完成,下限 0 |\n| 活跃日 | 当日行驶里程 > 0 km |\n| 预计天数 D | 剩余 ÷ 近最多 7 个活跃日平均日里程,向上取整;无活跃日则「暂无法预计」 |\n| 截止剩余日历天 x | 任务时间窗相对今天的剩余自然日 |\n| 预判标签 | 用 D 对照 x:无法完成 / 有完成风险 / 可完成(见规格全文);无法完成时进度条与百分比为危险红,有完成风险为警告橙 |\n| 无工单 | 仍展示区块,文案「暂无里程任务」 |\n\n规格全文见 [mileage-task.md](./mileage-task.md)。\n\n### 6.5 检验有效期剩余天数 · 沪牌等级评定\n\n| 项 | 口径 |\n|----|------|\n| 检验有效期 | 日期后方标签:剩余 x 天 / 已过期 x 天;空为「未上传」 |\n| 沪牌判定 | 车牌以「沪」开头 |\n| 等级评定时间 | 仅沪牌展示;空为「未上传」 |\n| 剩余天数 | 仅沪牌展示;相对等级评定日计算 |\n\n规格全文见 [expire-remain-shanghai.md](./expire-remain-shanghai.md)。\n\n### 6.6 证照信息(八类)\n\n与证照管理一致:行驶证、道路运输证、登记证、特种设备使用登记证、特种设备使用标识、加氢卡、安全阀、压力表。规格全文见 [license-certs.md](./license-certs.md)。\n\n### 6.7 保险记录(对齐保险采购)\n\n五险种(交强 / 商业 / 超赔 / 驾意 / 货物)+ 当前保单摘要 + 采购历史(纵向时间轴)。规格全文见 [insurance-records.md](./insurance-records.md)。\n\n### 6.8 业务记录纵向时间轴\n\n租赁 / 保险历史 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审统一近→远时间轴。\n\n| 能力 | 口径 |\n|------|------|\n| 时间筛选 | 事故及右侧 Tab:全部 / 近30天 / 近90天 / 近1年 / 自定义;按各 Tab 关键时间过滤;无匹配可重置 |\n| 自定义选日 | 手机端底部滚轮(年 / 月 / 日,精确到日);起日不得晚于止日;本模块选日入口统一该交互 |\n| 调拨卡片 | 出发城市、到达城市、停车场、接收人、接收日;不展示「路线」合并行与「调出」人 |\n\n规格全文见 [biz-timeline.md](./biz-timeline.md)、[lease-records.md](./lease-records.md)。\n\n### 6.9 业务记录演示用例与空态\n\n无种子时 H5 本地生成演示用例;真正无数据时展示美化空态。规格全文见 [demo-biz-records.md](./demo-biz-records.md)。\n" + }, + { + "type": "markdown", + "id": "h5-va-prd-ch-07", + "title": "7. 总览流程图", + "markdown": "## 7. 总览流程图\n\n```mermaid\nflowchart TB\n entry[App 打开车辆资产 H5] --> list[列表:搜索 / KPI / 筛选]\n list --> card[点车辆卡片]\n card --> detail[详情摘要]\n detail --> tabs[切换只读 Tab]\n tabs --> back[返回列表]\n```\n" + }, + { + "type": "markdown", + "id": "h5-va-prd-ch-08", + "title": "8. 验收清单(产品 / 测试)", + "markdown": "## 8. 验收清单(产品 / 测试)\n\n- [ ] 独立原型可打开,桌面 PhoneShell / 窄屏可用 \n- [ ] KPI 六类计数与同数据 PC 一致 \n- [ ] 证照异常、保险异常筛选结果与 PC 规则一致 \n- [ ] 「更多筛选」含证照 / 保险 / 出库 / 来源 / 在线;面板不超出手机操作区,内容可滚动;启用后角标点亮 \n- [ ] 出库:未出库对应待验车/未备车/已备车/待交车;来源与在线与台账字段一致 \n- [ ] 车辆状态选项与 PC 字典一致,含「调拨中」「替换中」 \n- [ ] 搜索支持车牌 / VIN / 停车场 / 客户名称过滤列表;可一键清空搜索 \n- [ ] 无匹配时有引导操作(清空搜索 / 重置筛选) \n- [ ] 列表无总辆数、无翻页按钮;滚动触底可异步加载更多,到底提示「没有更多了」 \n- [ ] 列表进详情:一体渐变底连续,无白顶栏色带;摘要为浮起白卡(状态/标签/里程/异常与列表同语义);详情无底栏 \n- [ ] 详情检验有效期后方有「剩余 x 天」或「已过期 x 天」标签;空为「未上传」 \n- [ ] 沪牌详情可见「等级评定时间」「剩余天数」;非沪牌不出现 \n- [ ] 证照信息 Tab 可见八类证照,与证照管理种类一致 \n- [ ] 保险记录可见五险种卡片、当前保单摘要与历史时间轴;整体状态口径与保险采购一致 \n- [ ] 保单附件可预览与下载(原型演示) \n- [ ] 租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审为近→远时间轴;租赁未还车显示「未还车」 \n- [ ] 调拨卡片可见出发城市、到达城市、停车场、接收人、接收日;无「路线」「调出」 \n- [ ] 事故及右侧业务 Tab 可按时间快捷或自定义区间筛选;自定义起止为底部滚轮选日(精确到日),无匹配可重置 \n- [ ] 租赁记录可按客户名称 / 项目名称搜索;无匹配可清空搜索 \n- [ ] 任意车辆打开上述业务 Tab 可见列表(种子或演示用例);空态有插画与说明 \n- [ ] 详情 11 个 Tab 可进入且只读,无编辑按钮 \n- [ ] 列表卡片标签下有「里程任务」区:有任务可见百分比、进度条、剩余公里与预计天数;无任务显示「暂无里程任务」 \n- [ ] 预计天数按近 7 活跃日均里程推算;无活跃日时显示「暂无法预计」 \n- [ ] 进行中任务文案后方有完成预判标签(剩余 x 天 + 预计无法完成 / 可完成 / 有完成风险) \n- [ ] 页脚说明「修改请在 PC」可见 \n\n**交付口径** \n本期交付可嵌入 App 的车辆资产只读 H5:能查、能筛异常、能看详情全 Tab,并在列表卡片扫读里程任务进度;写操作与异常处理仍回 PC。\n" + } + ] + }, + { + "type": "folder", + "id": "h5-va-doc-specs", + "title": "专题规格", + "defaultExpanded": true, + "children": [ + { + "type": "markdown", + "id": "h5-va-doc-biz-timeline", + "title": "业务记录时间轴", + "markdownPath": ".spec/biz-timeline.md", + "markdown": "# 业务记录 · 纵向时间轴\n\n详情中租赁 / 保险历史 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审等业务记录,统一为**近 → 远纵向时间轴**展示(与租赁记录同一视觉与交互骨架)。\n\n## 结构\n\n1. **时间筛选**(事故及右侧 Tab):快捷「全部 / 近30天 / 近90天 / 近1年 / 自定义起止」;按各 Tab 关键时间过滤;无匹配可重置 \n - **自定义起止**:底部原生滚轮日期选择(年 / 月 / 日,精确到日);起日不得晚于止日 \n - 本模块凡需选日的入口,统一用该原生日期选择器,不使用浏览器原生 `date` 控件\n2. **时间节点**:左侧圆点 + 连线;节点日期取该条业务关键时间(近 → 远) \n3. **记录卡片**:主标题(多为完整时间)+ 键值行 + 角标状态 \n4. **未闭环强调**:进行中 / 未还车 / 未解决 / 未处理等节点用强调色圆点与日期 \n\n## 各 Tab 关键时间\n\n| Tab | 节点日期 / 筛选字段 | 卡片要点 |\n|-----|---------------------|----------|\n| 租赁 | 交车/还车较近者 | 合同、项目、客户、交还车;未还车文案;客户/项目搜索 |\n| 保险历史 | 采购时间(无则到期) | 保单号、公司、状态、到期;附件预览/下载 |\n| 事故 | 事故时间 | 标题为事故时间;下侧:单号、位置、等级、类型、客户、状态 |\n| 故障 | 上报时间 | 标题为上报时间;下侧:编号、等级、类型、解决;故障说明用只读文本域 |\n| 违章 | 违章时间 | 标题为违章时间;下侧:行为、位置、缴费、处理、客户 |\n| 异动 | 开始日 | 标题为开始时间;下侧:类型、目的地、预计结束 |\n| 调拨 | 调拨日 | 标题为调拨日期;下侧:出发城市、到达城市、停车场、接收人、接收日 |\n| 年审 | 执行时间 | 标题为执行时间;下侧:有效期至、检测站、执行人 |\n\n保险 Tab 仍保留总览 + 五险种索引 + 当前摘要;**采购/批单历史**改为时间轴。\n\n## 数据源\n\n种子优先;不足时 H5 演示兜底(见 [demo-biz-records.md](./demo-biz-records.md))。\n\n## 代码路径\n\n- 通用:`components/RecordTimeline.tsx`、`TimelineDateFilter.tsx`、`NativeDatePickerSheet.tsx`\n- 租赁:`LeaseRecordsPanel.tsx`\n- 保险:`InsuranceRecordsPanel.tsx`\n- 其余:`BizTimelinePanels.tsx`\n" + }, + { + "type": "markdown", + "id": "h5-va-doc-lease", + "title": "租赁记录(时间轴)", + "markdownPath": ".spec/lease-records.md", + "markdown": "# 租赁记录 · 纵向时间轴\n\n详情「租赁记录」以**时间为主轴**展示,由近到远;支持按客户名称 / 项目名称即时搜索。\n\n## 结构\n\n1. **搜索栏**(时间轴上方):提示「搜索客户名称 / 项目名称」;可一键清空 \n2. **纵向时间轴**:左侧圆点 + 连线;节点日期取该条记录交车/还车中较近的日期 \n3. **记录卡片**(节点右侧): \n - 客户合同编码 \n - 项目名称 \n - **客户**(交车上方,始终展示) \n - 交车时间 \n - 还车时间;未还车时显示 **未还车**(强调色) \n - 业务类型角标(租赁 / 物流)\n\n## 排序\n\n按节点日期降序(近 → 远)。搜索只过滤可见项,不改变排序规则。\n\n## 搜索匹配\n\n| 顺序 | 条件 | 结果 |\n|------|------|------|\n| 1 | 关键词为空 | 展示该车全部租赁记录 |\n| 2 | 客户名称或项目名称包含关键词(不区分大小写) | 保留该条 |\n| 3 | 均不包含 | 过滤掉;全部无匹配时展示「暂无匹配记录」+ 清空搜索 |\n\n**前置条件**\n\n- 该车无任何租赁记录:只展示「暂无记录」,不展示搜索栏 \n- 仅匹配客户名称、项目名称;合同编码等不参与搜索(本期)\n\n## 数据源\n\n- `resolveVehicleLeaseRecords`(租赁记录种子;无种子时由车辆台账交还车信息推导)\n- 原型本地种子,未接真实租赁合同 API\n\n## 代码路径\n\n- UI:`components/LeaseRecordsPanel.tsx`\n- 数据:`vehicle-management/utils/leaseRecords.ts`\n" + }, + { + "type": "markdown", + "id": "h5-va-doc-demo-biz", + "title": "业务记录演示与空态", + "markdownPath": ".spec/demo-biz-records.md", + "markdown": "# 业务记录 · 演示用例数据(H5)\n\n详情中租赁 / 事故 / 故障 / 违章 / 异动 / 调拨 / 年审等业务 Tab,在**台账种子无该车记录**时,由 H5 本地按车牌稳定生成演示记录;租赁不足 3 条时补足,便于查看完整时间轴。\n\n## 判定优先级\n\n| 顺序 | 条件 | 结果 |\n|------|------|------|\n| 1 | PC 同源种子已有该车牌记录且条数足够 | 使用种子(不覆盖) |\n| 2 | 租赁可由车辆台账交还车字段推导 | 使用推导记录,不足 3 条再补历史演示 |\n| 3 | 仍为空,或条数不足 3 | 生成 / 补足演示用例(租赁至少 3 条:1 条在租 + 2 条已还车历史) |\n\n## 前置条件\n\n- 仅 H5「车辆资产」详情生效;不写入真实 API\n- 演示字段取自该车台账(客户、项目、城市、停车场、里程等)+ 车牌哈希,换车内容会变、同车刷新稳定\n\n## 空状态\n\n无任何可展示记录时(极少):插画空态 + 标题「暂无记录」+ 引导回 PC 录入。\n\n## 代码路径\n\n- 兜底:`utils/demo-biz-records.ts`\n- 空态:`components/RecordsEmpty.tsx`\n" + }, + { + "type": "markdown", + "id": "h5-va-doc-insurance", + "title": "保险记录", + "markdownPath": ".spec/insurance-records.md", + "markdown": "# 保险记录 · 与保险采购对齐\n\n详情「保险记录」Tab 数据与险种口径对齐 **保险采购 / PC 车辆资产保险记录**。\n\n## 结构\n\n1. **车辆保险档案总览**:整体保险状态(正常 / 临期 / 异常)及说明(交强险 + 商业险判定,与保险采购联动口径一致)\n2. **五险种索引**(单行横滑密排):交强险、商业险、超赔险、驾意险、货物险 \n - 展示险种名 + 简短状态(正常/临期/未购等)+ 历史条数;到期日在下方详情区 \n3. **当前险种详情** \n - 摘要:保单号、保险公司、到期日期、保费 \n - 历史:纵向时间轴(近→远);节点取采购时间;卡片含保单号、公司、状态、到期、保单附件(预览 / 下载);不展示导入时间与付款时间 \n\n手机端保单附件:\n- **预览**:壳内底部弹层展示文件名与演示影像(正式环境预览真实 PDF)\n- **下载**:触发本地下载演示文件并 Toast「已开始下载」\n- 无附件显示「无附件」\n\n## 状态口径(摘要)\n\n| 整体状态 | 条件 |\n|----------|------|\n| 正常 | 交强险、商业险均在有效期内(含临期、停保仍可交车语义与 PC 一致时按 PC) |\n| 临期 | 交强或商业有效但临期 |\n| 异常 | 交强或商业未购买 / 已到期 / 已退保等 |\n\n手机端只读录入;保单附件支持预览与下载(原型演示)。某险种无历史时展示带插画的紧凑空态,引导回 PC「保险采购」录入。\n\n## 数据源\n\n- `resolveVehicleInsuranceOverview`(保险采购记录 + 车辆保险台账种子)\n- 原型本地种子,未接正式保险采购写接口\n\n## 代码路径\n\n- UI:`components/InsuranceRecordsPanel.tsx`\n- 数据:`vehicle-management/utils/vehicleInsuranceRecords.ts`\n" + }, + { + "type": "markdown", + "id": "h5-va-doc-license-certs", + "title": "证照信息(八类)", + "markdownPath": ".spec/license-certs.md", + "markdown": "# 证照信息 · 与证照管理对齐\n\n详情「证照信息」Tab 证照种类、字段与状态口径对齐 **证照管理 / PC 车辆资产证照 Tab**(八类)。\n\n## 八类证照\n\n| 顺序 | 证照 | 展示内容 | 状态角标 |\n|------|------|----------|----------|\n| 1 | 行驶证 | 影像 + 注册/发证/强制报废/检验有效期至;沪牌另显下次等级评定时间与剩余天数 | 已上传 / 未上传 |\n| 2 | 道路运输证 | 影像 + 经营许可证号、核发时间、证件有效期、审验有效期 | 已上传 / 未上传 |\n| 3 | 登记证 | 影像 | 已上传 / 未上传 |\n| 4 | 特种设备使用登记证 | 影像 | 已上传 / 未上传 |\n| 5 | 特种设备使用标识 | 影像 + 下次检验日期 | 已上传 / 未上传 |\n| 6 | 加氢卡 | 卡号/余额/配发信息;未绑定文案「尚未绑定加氢卡」 | 已绑定 / 未绑定 |\n| 7 | 安全阀 | 影像 + 本次检验日期、下次检测日期 | 已上传 / 未上传 |\n| 8 | 压力表 | 影像 + 本次检验日期、下次检验日期 | 已上传 / 未上传 |\n\n顶部提供八类名称**单行横向滚动**索引;点选后自动锚点滚动到对应证照区块,选中项高亮。\n\n## 日期与剩余天数\n\n有明确到期/检验日的字段,日期后方可展示「剩余 x 天 / 已过期 x 天」(与检验有效期规则一致)。\n\n## 数据源\n\n- 与 PC 车辆资产同一套证照包解析(原型种子 + 台账回填)\n- 影像为原型演示样例图;正式环境以证照管理上传为准\n- 手机端只读,不提供编辑 / 跳转证照管理\n\n## 代码路径\n\n- UI:`components/LicenseCertPanel.tsx`\n- 数据:`vehicle-management/utils/vehicleLicenses.ts` → `LICENSE_CERT_NAV` / `resolveVehicleLicenseBundle`\n" + }, + { + "type": "markdown", + "id": "h5-va-doc-expire-shanghai", + "title": "检验有效期与沪牌", + "markdownPath": ".spec/expire-remain-shanghai.md", + "markdown": "# 检验有效期剩余天数 · 沪牌等级评定\n\n## 检验有效期 · 剩余天数标签\n\n| 项 | 口径 |\n|----|------|\n| 展示位置 | 基本信息「证照 / 保险概览」· 检验有效期;证照信息「行驶证」· 检验有效期 |\n| 有有效日期 | 日期后方标签:`剩余x天`;已过期则为 `已过期x天` |\n| 无日期 | 文案「未上传」,无标签 |\n| 临期着色 | 剩余 ≤ 30 天:警告色;已过期:危险色;其余:主色浅底 |\n| 计算 | 到期日相对今天的日历天数差(与 PC 一致) |\n\n## 沪牌等级评定(仅车牌以「沪」开头)\n\n| 字段 | 展示 |\n|------|------|\n| 等级评定时间 | 日期;空为「未上传」 |\n| 剩余天数 | `剩余x天` / `已过期x天`;无日期为「—」 |\n\n- 证照信息 Tab 在行驶证内展示沪牌等级评定(与 PC 一致);基本信息档案区亦有等级评定字段 \n- 非沪牌不展示 \n\n## 数据源\n\n- 检验有效期:台账检验有效期 / 行驶证检验有效期 \n- 等级评定:台账等级评定时间(空时证照 Tab 可回退行驶证下次评定日) \n- 原型本地计算剩余天数,未接实时服务端日历接口 \n\n## 代码路径\n\n- `components/detailTabs.tsx` → `ExpireValue` / `formatRemainDaysLabel` / `isShanghaiPlate`\n" + }, + { + "type": "markdown", + "id": "h5-va-doc-more-filters", + "title": "更多筛选", + "markdownPath": ".spec/more-filters.md", + "markdown": "# 更多筛选 · 判定规格\n\n「更多筛选」底部面板内的条件与列表筛选项行(品牌车型 / 运营城市 / 车辆状态)叠加生效;任一非「全部」即点亮更多筛选角标。\n\n## 面板项与匹配口径\n\n| 优先级 | 筛选项 | 选项 | 匹配规则 | 数据源 |\n|--------|--------|------|----------|--------|\n| 1 | 证照状态 | 全部 / 正常 / 异常 | 异常 = 检验有效期早于今天;正常 = 非异常 | 台账证照有效期 |\n| 2 | 保险状态 | 全部 / 正常 / 异常 | 异常 = 保险状态为「异常」;正常 = 非异常 | 台账保险状态 |\n| 3 | 出库状态 | 全部 / 未出库 / 已出库 | 见下表(由车辆状态推导,无独立出库字段) | 台账车辆状态 |\n| 4 | 车辆来源 | 全部 / 自有 / 外租 / 挂靠 | 精确等于台账「车辆来源」 | 台账车辆来源 |\n| 5 | 在线状态 | 全部 / 在线 / 离线 | 精确等于台账「在线状态」 | 台账在线状态 |\n\n以上条件与 KPI、搜索、品牌车型、运营城市、车辆状态为 **AND**。\n\n## 出库状态推导(原型本地)\n\n| 出库选项 | 车辆状态集合 |\n|----------|----------------|\n| 未出库 | 待验车、未备车、已备车、待交车 |\n| 已出库 | 有明确车辆状态,且不属于「未出库」集合(如已交车、待还车、异动中等) |\n\n**前置条件**\n\n- 车辆状态为空、`-`、`无`:不匹配「未出库」也不匹配「已出库」(选任一侧时该车被滤掉)\n- 正式环境若上线独立「出库」字段,以字段为准,本表仅作原型推导\n\n## 交互与布局\n\n- 面板挂在手机壳操作区内,宽度不超过壳层;内容区可滚动,底部「重置 / 完成」固定\n- 「重置」只清更多筛选五项,不清筛选项行与 KPI / 搜索\n- 「完成」关闭面板并保留当前选择;列表已随选择即时刷新\n\n## 代码路径(实现对照)\n\n- 匹配:`utils/filter.ts` → `matchOutboundFilter` / `matchVehicleSourceFilter` / `matchOnlineStatusFilter` 等\n- UI:`components/FilterSheet.tsx`\n" + }, + { + "type": "markdown", + "id": "h5-va-doc-mileage-task", + "title": "里程任务进度", + "markdownPath": ".spec/mileage-task.md", + "markdown": "# 列表卡片 · 里程任务进度\n\n> 原型本地演示种子:[`utils/mileage-task.ts`](../utils/mileage-task.ts) \n> **未接真实采购合同 / 任务工单 API**\n\n## 业务背景\n\n采购部发起车辆采购合同时,合同可关联对应批次车辆的**里程要求**与**时间起止**;里程任务通过**任务工单**下发到车辆。手机列表卡片展示该车当前里程任务完成情况,便于外勤扫一眼进度与是否赶得上截止。\n\n## 展示位置\n\n车辆卡片中,在「来源 / 客户名」等标签行**下方**,全宽分隔区:\n\n1. 标题「里程任务」(有任务时右侧展示完成百分比)\n2. 有任务:进度条\n3. 有任务:剩余公里 + 按近 7 活跃日推算的预计完成天数\n4. 进行中任务:文案后方完成预判标签(对照任务截止剩余日历天)\n\n## 判定 / 展示优先级\n\n| 顺序 | 条件 | 结果 |\n|------|------|------|\n| 1 | 车辆未挂接有效里程任务工单 | 展示标题 +「暂无里程任务」(无进度条、无剩余/预计、无预判标签) |\n| 2 | 已完成里程 ≥ 目标里程(或进度 ≥ 100%) | 进度 100%、成功色进度条与百分比;文案「已完成」;**不**展示预判标签 |\n| 3 | 有任务且未完成,且近 7 个活跃日均里程可算 | 进度条 +「剩余 N km · 预计 D 天内完成」+ 预判标签;预判为「无法完成」时进度条/百分比为危险红;「有完成风险」为警告橙 |\n| 4 | 有任务且未完成,但无活跃日(近窗内日里程均 ≤ 0) | 进度条(警告橙)+「剩余 N km · 暂无法预计」+ 预判标签「剩余 x 天,预计有完成风险」 |\n\n## 口径公式\n\n| 项 | 口径 |\n|----|------|\n| 完成百分比 | `min(100, 保留 1 位小数(任务内已完成里程 / 合同目标里程 × 100))` |\n| 剩余公里 | `max(0, 目标里程 − 已完成里程)` |\n| 活跃日 | 自然日内该车行驶里程 **> 0 km** |\n| 近 7 活跃日均 | 取最近最多 **7** 个活跃日的日里程算术平均;不足 7 个则有几个算几个 |\n| 预计天数 D | `ceil(剩余公里 / 近 7 活跃日均)`;均里程为 0 或无活跃日 → 无法预计 |\n| 截止剩余日历天 x | 任务/合同时间窗截止日相对今天的剩余自然日(下限 0) |\n\n## 完成预判标签(文案后方)\n\n用预计天数 D(近 7 活跃日推算)对照截止剩余日历天 x:\n\n| 顺序 | 条件 | 标签文案 | 色调 | 进度条 / 百分比 |\n|------|------|----------|------|----------------|\n| 1 | D 无法算出(无活跃日) | 剩余 x 天,预计有完成风险 | 警告橙 | 同警告橙 |\n| 2 | x ≤ 0 或 D > x | 剩余 x 天,预计无法完成 | 危险红 | 同危险红 |\n| 3 | D > x × 85%,或 (x − D) ≤ 7 | 剩余 x 天,预计有完成风险 | 警告橙 | 同警告橙 |\n| 4 | 其余 | 剩余 x 天,预计可完成 | 成功绿 | 默认主色蓝 |\n\n## 前置条件\n\n- 仅列表卡片只读展示;不提供下发、编辑、跳转工单详情(本期不做)\n- 筛选 / KPI / 搜索不影响本区展示规则,只影响哪些车出现在列表\n\n## 数据源(原型)\n\n| 来源 | 说明 |\n|------|------|\n| 演示种子 | 按车牌稳定 hash:约 65% 挂接演示任务(目标里程、已完成、近窗活跃日里程序列、截止剩余日历天);其余为无任务 |\n| 台账导表 | 仅借用车牌标识做稳定种子;**不**把导表「当前总里程」直接当作任务进度真相 |\n\n正式环境应由采购合同批次里程要求 + 时间起止 + 任务工单执行回写提供目标 / 已完成 / 日里程明细 / 截止日。\n\n## 用户可见文案\n\n| 场景 | 文案 |\n|------|------|\n| 无任务 | 暂无里程任务 |\n| 进行中 | 剩余 {N} km · 预计 {D} 天内完成 |\n| 进行中且无法预计 | 剩余 {N} km · 暂无法预计 |\n| 预判标签 | 剩余 {x} 天,预计无法完成 / 预计可完成 / 预计有完成风险 |\n| 已完成 | 已完成(无预判标签) |\n\n## 代码路径\n\n- 解析:`utils/mileage-task.ts` → `resolveMileageTaskCard` / `resolveMileageForecast`\n- 展示:`components/VehicleCard.tsx` + `styles/index.css`(`.h5-va-mileage-task*` / `.h5-va-mileage-forecast`)\n" + } + ] + } + ] + } + ] + } +} diff --git a/src/prototypes/oneos-h5-vehicle-assets/components/AppToast.tsx b/src/prototypes/oneos-h5-vehicle-assets/components/AppToast.tsx new file mode 100644 index 0000000..936883d --- /dev/null +++ b/src/prototypes/oneos-h5-vehicle-assets/components/AppToast.tsx @@ -0,0 +1,37 @@ +import React, { useEffect, useState } from 'react'; + +type ToastState = { id: number; message: string } | null; + +let pushToast: ((message: string) => void) | null = null; + +export function showAppToast(message: string) { + pushToast?.(message); +} + +export function AppToastHost() { + const [toast, setToast] = useState(null); + + useEffect(() => { + let timer = 0; + pushToast = (message: string) => { + const id = Date.now(); + setToast({ id, message }); + window.clearTimeout(timer); + timer = window.setTimeout(() => { + setToast((current) => (current?.id === id ? null : current)); + }, 1800); + }; + return () => { + pushToast = null; + window.clearTimeout(timer); + }; + }, []); + + if (!toast) return null; + + return ( +
    + {toast.message} +
    + ); +} diff --git a/src/prototypes/oneos-h5-vehicle-assets/components/BizTimelinePanels.tsx b/src/prototypes/oneos-h5-vehicle-assets/components/BizTimelinePanels.tsx new file mode 100644 index 0000000..be68ed2 --- /dev/null +++ b/src/prototypes/oneos-h5-vehicle-assets/components/BizTimelinePanels.tsx @@ -0,0 +1,283 @@ +import React, { useMemo } from 'react'; +import type { VehicleRecord } from '../utils/filter'; +import { resolveVehicleAccidentRecords } from '../../vehicle-management/utils/accidentRecords'; +import { resolveVehicleFaultRecords } from '../../vehicle-management/utils/faultRecords'; +import { resolveVehicleViolationRecords } from '../../vehicle-management/utils/violationRecords'; +import { resolveVehicleMovementRecords } from '../../vehicle-management/utils/movementRecords'; +import { resolveVehicleTransferRecords } from '../../vehicle-management/utils/transferRecords'; +import { resolveVehicleAnnualReviewRecords } from '../../vehicle-management/utils/annualReviewRecords'; +import { + ensureAccidentRecords, + ensureAnnualReviewRecords, + ensureFaultRecords, + ensureMovementRecords, + ensureTransferRecords, + ensureViolationRecords, +} from '../utils/demo-biz-records'; +import { RecordsEmpty } from './RecordsEmpty'; +import { + RecordTimeline, + sortByDateDesc, + timelineDatePart, + type TimelineItem, +} from './RecordTimeline'; +import { + matchTimelineDate, + useTimelineDateFilter, +} from './TimelineDateFilter'; + +function PanelShell({ + label, + emptyDesc, + items, + dateFieldLabel, +}: { + label: string; + emptyDesc: string; + items: TimelineItem[]; + dateFieldLabel: string; +}) { + const { range, filterActive, filterNode, reset } = useTimelineDateFilter(dateFieldLabel); + + const filtered = useMemo( + () => items.filter((item) => matchTimelineDate(item.date, range)), + [items, range], + ); + + if (!items.length) { + return ; + } + + return ( +
    + {filterNode} + {!filtered.length ? ( +
    +

    暂无匹配记录

    +

    调整时间范围,或重置筛选查看全部

    + {filterActive ? ( +
    + +
    + ) : null} +
    + ) : ( + + )} +
    + ); +} + +export function AccidentRecordsPanel({ record }: { record: VehicleRecord }) { + const items = useMemo(() => { + const rows = sortByDateDesc( + ensureAccidentRecords(record, resolveVehicleAccidentRecords(record)), + (row) => timelineDatePart(row.accidentTime), + ); + return rows.map((row): TimelineItem => ({ + id: row.id, + date: timelineDatePart(row.accidentTime) || '—', + accent: row.status === '处理中', + badge: row.status, + title: row.accidentTime || '—', + meta: [ + { label: '单号', value: row.accidentCode || '—' }, + { label: '位置', value: row.accidentLocation || '—' }, + { label: '等级', value: row.accidentLevel || '—' }, + { label: '类型', value: row.accidentType || '—' }, + ...(row.customerName ? [{ label: '客户', value: row.customerName }] : []), + ], + })); + }, [record]); + + return ( + + ); +} + +export function FaultRecordsPanel({ record }: { record: VehicleRecord }) { + const items = useMemo(() => { + const rows = sortByDateDesc( + ensureFaultRecords(record, resolveVehicleFaultRecords(record)), + (row) => timelineDatePart(row.reportedAt), + ); + return rows.map((row): TimelineItem => { + const open = row.resolutionStatus !== '已解决'; + const description = (row.faultDescription || '').trim() || '—'; + return { + id: row.id, + date: timelineDatePart(row.reportedAt) || '—', + accent: open, + badge: row.resolutionStatus || '未解决', + title: row.reportedAt || '—', + meta: [ + { label: '编号', value: row.faultNo || '—' }, + { label: '等级', value: row.faultLevel || '—' }, + { label: '类型', value: row.faultType || '—' }, + ...(row.resolvedAt + ? [{ label: '解决', value: row.resolvedAt }] + : [{ label: '解决', value: '未解决', pending: true }]), + ], + footer: ( +
    +
    故障说明
    +