Compare commits

..

2 Commits

299 changed files with 107673 additions and 10094 deletions

View File

@@ -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`

View File

@@ -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
```

View File

@@ -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]
```

View File

@@ -2,6 +2,7 @@
"server": { "server": {
"host": "localhost", "host": "localhost",
"allowLAN": true, "allowLAN": true,
"lanHost": "192.168.0.102",
"enableCommandAPI": false "enableCommandAPI": false
}, },
"projectDefaults": { "projectDefaults": {

View File

@@ -1,78 +1,23 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-07-20T05:34:45.927Z", "updatedAt": "2026-07-21T08:13:15.367Z",
"prototypes": [ "prototypes": [
{
"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", "id": "folder-1782874576229-r8atbu",
"kind": "folder", "kind": "folder",
"title": "OneOS", "title": "OneOS",
"children": [ "children": [
{ {
"id": "item:prototypes:oneos-web-workbench", "id": "item:prototypes:oneos-prototype-demo",
"kind": "item",
"title": "原型演示",
"itemKey": "prototypes/oneos-prototype-demo"
},
{
"id": "item:prototypes:oneos-web-workbench-new",
"kind": "item", "kind": "item",
"title": "工作台", "title": "工作台",
"itemKey": "prototypes/oneos-web-workbench" "itemKey": "prototypes/oneos-web-workbench-new"
}, },
{ {
"id": "folder-oneos-approval", "id": "folder-oneos-approval",
@@ -141,6 +86,12 @@
"title": "租赁合同", "title": "租赁合同",
"itemKey": "prototypes/lease-contract-management" "itemKey": "prototypes/lease-contract-management"
}, },
{
"id": "item:prototypes:self-operated-contract",
"kind": "item",
"title": "自营合同",
"itemKey": "prototypes/self-operated-contract"
},
{ {
"id": "item:prototypes:insurance-procurement", "id": "item:prototypes:insurance-procurement",
"kind": "item", "kind": "item",
@@ -173,12 +124,6 @@
"title": "站点信息", "title": "站点信息",
"itemKey": "prototypes/oneos-web-h2-station-site" "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", "id": "item:prototypes:oneos-web-h2-station",
"kind": "item", "kind": "item",
@@ -192,9 +137,9 @@
"itemKey": "prototypes/oneos-web-h2-station-weekly" "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", "kind": "item",
"title": "oneos web h2 station analysis", "title": "加氢站分析",
"itemKey": "prototypes/oneos-web-h2-station-analysis" "itemKey": "prototypes/oneos-web-h2-station-analysis"
} }
] ]
@@ -296,6 +241,12 @@
"kind": "item", "kind": "item",
"title": "任务工单", "title": "任务工单",
"itemKey": "prototypes/task-work-order" "itemKey": "prototypes/task-work-order"
},
{
"id": "item:prototypes:self-operated-dispatch-task",
"kind": "item",
"title": "调度任务",
"itemKey": "prototypes/self-operated-dispatch-task"
} }
] ]
}, },
@@ -303,22 +254,101 @@
"id": "folder-1784341576489-spa7ms", "id": "folder-1784341576489-spa7ms",
"kind": "folder", "kind": "folder",
"title": "审批中心", "title": "审批中心",
"children": [ "children": []
{
"id": "item:prototypes:oneos-web-approval",
"kind": "item",
"title": "审批中心",
"itemKey": "prototypes/oneos-web-approval"
}
]
}, },
{ {
"id": "item:prototypes:lease-business-line-overview", "id": "item:prototypes:lease-business-line-overview",
"kind": "item", "kind": "item",
"title": "业务条线说明", "title": "业务条线说明",
"itemKey": "prototypes/lease-business-line-overview" "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": [], "docs": [],

View File

@@ -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 迭代名称.
---
# AutoVULOneOS 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)。

View File

@@ -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 |
| 部门自然嵌入 | `运维部人员可…`,无「供…条线使用」 |

View File

@@ -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修复】
(粘贴)
```

View File

@@ -0,0 +1,60 @@
# 模块 → 业务条线 / 责任部门对照
> 数据源OneOS「业务条线说明」页责任部门定义租赁 / 能源 / 运维 / 安全 / 自营)
> 生成更新日志时,**适用对象**须从此表取 `roles`,不得自造部门名。
## 五条业务条线
| ID | 条线名称 |
|----|----------|
| lease | 租赁业务条线 |
| energy | 能源业务条线 |
| ops | 运维管理条线 |
| safety | 安全管理条线 |
| self-operated | 自营业务条线 |
## 本版变更模块对照
| 更新日志模块名 | 业务条线 | lines.ts 模块 | 责任部门roles原文 |
|----------------|----------|---------------|-------------------------|
| 加氢站管理 | 能源业务条线 | 加氢站管理 | 采购部、加氢站 |
| 车辆管理 | 运维管理条线 | 验车入库 / 车辆管理(资产底座) | 运维部、采购部 |
| 审批中心 | 跨条线 | 租赁合同 + 还车应结款(审批场景) | 业务员、法务部、业务管理组、安全部、运维部、业务管理组-能源部 |
| 保险采购 | 运维管理条线 | 证照 / 保险(车辆资产关联) | 运维部、安全部 |
| 故障管理 | 运维管理条线 | 故障管理 | 运维部、司机 |
| 租赁合同 | 租赁业务条线 | 租赁合同 | 业务员、法务部 |
| 证照管理 | 运维管理条线 | 证照管理 | 运维部、安全部 |
| 全局显示 | 全系统 | 系统介绍(五条条线) | 业务管理组、运维部、采购部、安全部、法务部、财务部、业务管理组-能源部、业务服务组 |
| 交还车管理 | 运维管理条线 | 交车管理 + 还车管理 | 运维部、业务管理组、司机 |
| 还车应结款 | 租赁业务条线 | 还车应结款 | 业务管理组、安全部、运维部、业务管理组-能源部 |
| 还车管理 | 运维管理条线 | 还车管理 | 运维部、业务管理组 |
| 车辆氢费明细 | 能源业务条线 | 车辆氢费明细 | 业务管理组-能源部 |
| 业务管理 · 租赁统计 | 租赁业务条线 | 租赁业务台账 / 客户管理 | 业务管理组、业务员 |
| 业务管理 · 物流统计 | 自营业务条线 | 自营台账 | 业务服务组、财务部 |
## 写法规则
- 部门名与 `roles` **完全一致**`业务管理组-能源部` 对外可简写「能源部人员」)
- **禁止**「供…业务条线…使用」「供…人员使用」
- **推荐**`{部门}人员可…` / `{部门}可…` / `{部门}审批时可…`
- 同模块合并后,多部门用顿号或分号自然串联
- 找不到模块时:查 `lines.ts`;仍无则标「待产品确认」不进成稿
## 自然表述示例
| 模块 | 生硬(禁止) | 自然(推荐) |
|------|--------------|--------------|
| 车辆管理 | 供运维管理条线运维部使用 | 运维部人员可快速辨认车辆所属运营区域 |
| 加氢站管理 | 供能源业务条线采购部、加氢站使用 | 采购部可统一维护站点信息,加氢站可便捷管理日常运营 |
| 还车应结款 | 供租赁业务条线业务管理组…使用 | 业务管理组、安全部、运维部、能源部还车结算金额更准确 |
## 常见错误(禁止)
| 错误写法 | 正确写法 |
|----------|----------|
| 氢能运营业务人员 | 采购部、加氢站(能源 · 加氢站管理) |
| 车辆运营及资产管理业务人员 | 运维部、采购部(运维 · 车辆管理) |
| 各业务条线审批人员 | 业务员、法务部、…(按审批场景枚举) |
| 保险采购业务人员 | 运维部、安全部 |
| 还车结算业务人员 | 业务管理组、安全部、运维部、业务管理组-能源部 |
| 经营分析及管理部门 | 业务管理组、业务服务组(分租赁 / 自营) |

View File

@@ -0,0 +1,81 @@
# 云效迭代 → 需求清单AutoVUL 取数)
测试人员只提供**迭代名称**Agent 负责在云效定位迭代、拉取关联需求、抽出更新素材。
## 安全边界(强制)
- **禁止**在对话、Skill、日志、成稿中写入 Token、密码、Cookie、私钥。
- 优先复用**已登录浏览器会话**操作云效 Projex`yunxiao-requirement-lifecycle` 一致)。
- 若团队已配置本机环境变量访问 OpenAPI可走 APIToken 只读自环境,永不回显。
建议环境变量(可选,名称可按团队调整):
| 变量 | 用途 |
|------|------|
| `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

View File

@@ -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/<id>/` 下页面、交互、文案、判定、验收相关内容
- 同一轮交付内同步更新 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/<prototype-id>/`
2. **读上下文(只取产品语义)**
- 用户说明、已确认口径、原型标注、`.spec/`、业务条线说明(`lines.ts`
- 忽略实现细节;字段名/接口改写成业务语言。
3. **收敛边界**:做什么 / 不做什么、外部依赖、与其它模块关系。
4. **按模板成文**:下方「输出结构」;缺关键信息最多问 12 个问题,其余写「假设」。
5. **落盘 + 标注同步(强制)** — 见 [references/annotation-sync.md](references/annotation-sync.md)。
| 顺序 | 动作 |
|------|------|
| A | 写/更新 `src/prototypes/<id>/.spec/requirements-prd.md` |
| B | 写/更新 `src/resources/prd/<id>-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>
## 需求说明
<AutoPRD 正文或交付口径 + 必要章节>
## 更新内容
<第 10 章本次定稿块;无则「首版定稿」或「本轮无功能/逻辑增量」>
## 更新内容·历史
<以往更新内容倒序,勿删除>
```
### 云效阶段任务(状态推进时强制)
见 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)。摘要:
| 需求状态 | 任务标签 | 负责人 | 标题 |
|----------|----------|--------|------|
| 分析中 | 分析 | 需求创建人 | 与需求同名 |
| 设计中 | 设计 | 需求创建人 | 与需求同名 |
| 待开发 | 交付(或开发) | 何斐 | 与需求同名 |
正式关联需求;创建时间口径沿用需求;同阶段未取消任务不重复建。
## 写作硬约束
**必须写**
- 一句话定位 + 目标 / 非目标
- 模块边界(含 mermaid 总览更好)
- 角色与目标(角色名优先对齐业务条线说明)
- **用户故事**(业务条线说明口径)+ Epic 级**故事点SP**粗估
- 分功能**正向**与**逆向/边界**
- 至少 12 个 **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

View File

@@ -0,0 +1,98 @@
# Axhub 标注目录同步(强制)
使用 OneOS AutoPRD 时,除资源库归档外,**必须**把 PRD 挂到 Axhub Make 标注工具的「原型目录」里,用户才能在右侧面板直接打开。
## 落点(三份必须一致)
| # | 路径 | 作用 |
|---|------|------|
| 1 | `src/prototypes/<prototype-id>/.spec/requirements-prd.md` | **主真相**:产品可读 PRD 全文 |
| 2 | `src/prototypes/<prototype-id>/annotation-source.json`**顶层** `directory.nodes` | **标注目录入口**:面板里可见的 Markdown/PRD |
| 3 | `src/resources/prd/<prototype-id>-autoprd.md` | 资源库归档(可选但默认写) |
可选:复杂判定另写 `.spec/<topic>.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": "<prefix>-doc-prd",
"title": "产品需求说明PRD",
"markdownPath": ".spec/requirements-prd.md",
"markdown": "<与 .spec/requirements-prd.md 相同的全文>"
}
```
### 分章节 PRD推荐便于右侧目录浏览
在「产品需求说明PRD」旁增加文件夹 **「PRD 分章」**,按 PRD 的 `##` 一级标题拆成多个 `markdown` 子节点(标题用章节名,正文为该章全文)。专题规格(`.spec/<topic>.md`)放在 **「专题规格」** 文件夹。
参考结构:
```text
folder <模块>说明
markdown 产品需求说明PRD ← 全文
folder PRD 分章
markdown 1. 一句话与目标
markdown 2. 模块边界
folder 专题规格
markdown <topic 标题>
```
`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. 根据改动的文件路径确定 `<prototype-id>`
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 分章」:各章可单独打开
- [ ] 用户故事仍为业务条线说明口径(起点 / 怎么运作 / 闭环)

View File

@@ -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. 交付口径
## 刻意不写
- 表结构、接口、代码路径
- 用实现指令代替业务闭环描述

View File

@@ -0,0 +1,78 @@
# 功能变更记录(定稿触发)
用户回复**需求定稿关键字**后,必须生成「自上次定稿以来」的功能变更日志,追加到 PRD **最下方**,并更新定稿基线。
## 触发关键字(命中任一)
`需求定稿` · `定稿` · `确认定稿` · `本次定稿`
(可带模块名,如「保险采购需求定稿」。)
## 写什么 / 不写什么
| 要写(产品发版视角) | 不写 |
|----------------------|------|
| 哪个功能做了什么修改 | 布局、页面样式、UI 设计优化 |
| 变更了什么业务逻辑 / 流程 / 验收口径 | 「优化了哪些表」「改了哪些字段/接口」 |
| 用户可感知的交互结果变化 | 纯样式、动效、无语义文案微调 |
条目口吻示例:
- 「识别失败」:失败数可点击查看失败文件与原因;仅失败时也可打开明细
- 「比价单」:审批通过后仍须在保单管理另行录入正式保单(强调不自动同步)
## 落点
1. **PRD 文末章节**(强制)
`src/prototypes/<id>/.spec/requirements-prd.md``## 10. 功能变更记录`
新定稿块插在该章**最上方**(倒序:最新在上);历史块保留。
2. **定稿基线**(强制)
`src/prototypes/<id>/.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. 确认 `<prototype-id>`(从对话 / 打开文件 / 用户指名推断)。
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 主流程增量更新第 19 章;**不必**每改一次就写第 10 章。
- **用户说定稿时**:才汇总写入第 10 章并刷新基线。

View File

@@ -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/<id>/.spec/requirements-prd.md`
2. `annotation-source.json` → 目录节点「产品需求说明PRD`markdownPath` + `markdown`
3. `src/resources/prd/<id>-autoprd.md`
4. 定稿时另写:`src/prototypes/<id>/.spec/autoprd-baseline.json`
## 用户故事写法(对照业务条线说明)
业务条线说明页字段 → AutoPRD 字段:
| 页面 | AutoPRD |
|------|---------|
| 责任部门标签 | **角色** |
| 起点 | **起点** |
| 怎么运作(有序列表) | **怎么运作** |
| 结果色签 | **关键结果**(可选) |
| 闭环 | **闭环** |
示例口吻(摘自业务条线风格,非照抄某模块):
- **起点**:采购部维护交强与商业险台账,作为交车合规的前置数据。
- **怎么运作**1. 运营录入或识别保单… 2. 运维交车时校验…
- **闭环**:核心险种有效则允许交车;异常则拦截并提示原因,过程可追溯。
**不要**用下面这种宽表作为第 4 章唯一形态:
| 编号 | 作为…我希望… | SP |
|------|--------------|----|
「作为…我想…以便…」只允许作为**排期可选一行**,主叙述必须是起点/怎么运作/闭环。
## 正逆向写法约定
- **正向**:用户成功完成任务的最短路径
- **逆向**:取消、关闭、禁用、冲突状态、不可操作项
- 审批类:写清「本页只读 / 在外部系统办理」
## 规模与故事点
| 规模 | 建议含义 | 约 SP |
|------|----------|-------|
| S | 轻交互 / 列表展示 | 12 |
| M | 标准流程 + 筛选详情 | 35 |
| L | 状态机 / 跨模块 / 批量识别 | 6+ |

View File

@@ -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 文档,再改状态,再按本文件建任务(负责人何斐)。

View File

@@ -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/<id>-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`
- 建与需求**同名**任务并正式关联
- 标签:分析 / 设计 / 交付(或开发)
- 创建时间口径沿用需求
- 分析中、设计中负责人=创建人;待开发负责人=**何斐**
- 同阶段已有未取消关联任务则复用,不重复建

View File

@@ -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`);浏览器仅作标签失败或状态连跳兜底;少截图、同失败不循环重试。

View File

@@ -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 迭代名称.
---
# AutoVULOneOS 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)。

View File

@@ -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 |
| 部门自然嵌入 | `运维部人员可…`,无「供…条线使用」 |

View File

@@ -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修复】
(粘贴)
```

View File

@@ -0,0 +1,60 @@
# 模块 → 业务条线 / 责任部门对照
> 数据源OneOS「业务条线说明」页责任部门定义租赁 / 能源 / 运维 / 安全 / 自营)
> 生成更新日志时,**适用对象**须从此表取 `roles`,不得自造部门名。
## 五条业务条线
| ID | 条线名称 |
|----|----------|
| lease | 租赁业务条线 |
| energy | 能源业务条线 |
| ops | 运维管理条线 |
| safety | 安全管理条线 |
| self-operated | 自营业务条线 |
## 本版变更模块对照
| 更新日志模块名 | 业务条线 | lines.ts 模块 | 责任部门roles原文 |
|----------------|----------|---------------|-------------------------|
| 加氢站管理 | 能源业务条线 | 加氢站管理 | 采购部、加氢站 |
| 车辆管理 | 运维管理条线 | 验车入库 / 车辆管理(资产底座) | 运维部、采购部 |
| 审批中心 | 跨条线 | 租赁合同 + 还车应结款(审批场景) | 业务员、法务部、业务管理组、安全部、运维部、业务管理组-能源部 |
| 保险采购 | 运维管理条线 | 证照 / 保险(车辆资产关联) | 运维部、安全部 |
| 故障管理 | 运维管理条线 | 故障管理 | 运维部、司机 |
| 租赁合同 | 租赁业务条线 | 租赁合同 | 业务员、法务部 |
| 证照管理 | 运维管理条线 | 证照管理 | 运维部、安全部 |
| 全局显示 | 全系统 | 系统介绍(五条条线) | 业务管理组、运维部、采购部、安全部、法务部、财务部、业务管理组-能源部、业务服务组 |
| 交还车管理 | 运维管理条线 | 交车管理 + 还车管理 | 运维部、业务管理组、司机 |
| 还车应结款 | 租赁业务条线 | 还车应结款 | 业务管理组、安全部、运维部、业务管理组-能源部 |
| 还车管理 | 运维管理条线 | 还车管理 | 运维部、业务管理组 |
| 车辆氢费明细 | 能源业务条线 | 车辆氢费明细 | 业务管理组-能源部 |
| 业务管理 · 租赁统计 | 租赁业务条线 | 租赁业务台账 / 客户管理 | 业务管理组、业务员 |
| 业务管理 · 物流统计 | 自营业务条线 | 自营台账 | 业务服务组、财务部 |
## 写法规则
- 部门名与 `roles` **完全一致**`业务管理组-能源部` 对外可简写「能源部人员」)
- **禁止**「供…业务条线…使用」「供…人员使用」
- **推荐**`{部门}人员可…` / `{部门}可…` / `{部门}审批时可…`
- 同模块合并后,多部门用顿号或分号自然串联
- 找不到模块时:查 `lines.ts`;仍无则标「待产品确认」不进成稿
## 自然表述示例
| 模块 | 生硬(禁止) | 自然(推荐) |
|------|--------------|--------------|
| 车辆管理 | 供运维管理条线运维部使用 | 运维部人员可快速辨认车辆所属运营区域 |
| 加氢站管理 | 供能源业务条线采购部、加氢站使用 | 采购部可统一维护站点信息,加氢站可便捷管理日常运营 |
| 还车应结款 | 供租赁业务条线业务管理组…使用 | 业务管理组、安全部、运维部、能源部还车结算金额更准确 |
## 常见错误(禁止)
| 错误写法 | 正确写法 |
|----------|----------|
| 氢能运营业务人员 | 采购部、加氢站(能源 · 加氢站管理) |
| 车辆运营及资产管理业务人员 | 运维部、采购部(运维 · 车辆管理) |
| 各业务条线审批人员 | 业务员、法务部、…(按审批场景枚举) |
| 保险采购业务人员 | 运维部、安全部 |
| 还车结算业务人员 | 业务管理组、安全部、运维部、业务管理组-能源部 |
| 经营分析及管理部门 | 业务管理组、业务服务组(分租赁 / 自营) |

View File

@@ -0,0 +1,81 @@
# 云效迭代 → 需求清单AutoVUL 取数)
测试人员只提供**迭代名称**Agent 负责在云效定位迭代、拉取关联需求、抽出更新素材。
## 安全边界(强制)
- **禁止**在对话、Skill、日志、成稿中写入 Token、密码、Cookie、私钥。
- 优先复用**已登录浏览器会话**操作云效 Projex`yunxiao-requirement-lifecycle` 一致)。
- 若团队已配置本机环境变量访问 OpenAPI可走 APIToken 只读自环境,永不回显。
建议环境变量(可选,名称可按团队调整):
| 变量 | 用途 |
|------|------|
| `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

View File

@@ -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/<id>/` 下页面、交互、文案、判定、验收相关内容
- 同一轮交付内同步更新 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/<prototype-id>/`
2. **读上下文(只取产品语义)**
- 用户说明、已确认口径、原型标注、`.spec/`、业务条线说明(`lines.ts`
- 忽略实现细节;字段名/接口改写成业务语言。
3. **收敛边界**:做什么 / 不做什么、外部依赖、与其它模块关系。
4. **按模板成文**:下方「输出结构」;缺关键信息最多问 12 个问题,其余写「假设」。
5. **落盘 + 标注同步(强制)** — 见 [references/annotation-sync.md](references/annotation-sync.md)。
| 顺序 | 动作 |
|------|------|
| A | 写/更新 `src/prototypes/<id>/.spec/requirements-prd.md` |
| B | 写/更新 `src/resources/prd/<id>-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>
## 需求说明
<AutoPRD 正文或交付口径 + 必要章节>
## 更新内容
<第 10 章本次定稿块;无则「首版定稿」或「本轮无功能/逻辑增量」>
## 更新内容·历史
<以往更新内容倒序,勿删除>
```
### 云效阶段任务(状态推进时强制)
见 [references/yunxiao-stage-tasks.md](references/yunxiao-stage-tasks.md)。摘要:
| 需求状态 | 任务标签 | 负责人 | 标题 |
|----------|----------|--------|------|
| 分析中 | 分析 | 需求创建人 | 与需求同名 |
| 设计中 | 设计 | 需求创建人 | 与需求同名 |
| 待开发 | 交付(或开发) | 何斐 | 与需求同名 |
正式关联需求;创建时间口径沿用需求;同阶段未取消任务不重复建。
## 写作硬约束
**必须写**
- 一句话定位 + 目标 / 非目标
- 模块边界(含 mermaid 总览更好)
- 角色与目标(角色名优先对齐业务条线说明)
- **用户故事**(业务条线说明口径)+ Epic 级**故事点SP**粗估
- 分功能**正向**与**逆向/边界**
- 至少 12 个 **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

View File

@@ -0,0 +1,98 @@
# Axhub 标注目录同步(强制)
使用 OneOS AutoPRD 时,除资源库归档外,**必须**把 PRD 挂到 Axhub Make 标注工具的「原型目录」里,用户才能在右侧面板直接打开。
## 落点(三份必须一致)
| # | 路径 | 作用 |
|---|------|------|
| 1 | `src/prototypes/<prototype-id>/.spec/requirements-prd.md` | **主真相**:产品可读 PRD 全文 |
| 2 | `src/prototypes/<prototype-id>/annotation-source.json`**顶层** `directory.nodes` | **标注目录入口**:面板里可见的 Markdown/PRD |
| 3 | `src/resources/prd/<prototype-id>-autoprd.md` | 资源库归档(可选但默认写) |
可选:复杂判定另写 `.spec/<topic>.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": "<prefix>-doc-prd",
"title": "产品需求说明PRD",
"markdownPath": ".spec/requirements-prd.md",
"markdown": "<与 .spec/requirements-prd.md 相同的全文>"
}
```
### 分章节 PRD推荐便于右侧目录浏览
在「产品需求说明PRD」旁增加文件夹 **「PRD 分章」**,按 PRD 的 `##` 一级标题拆成多个 `markdown` 子节点(标题用章节名,正文为该章全文)。专题规格(`.spec/<topic>.md`)放在 **「专题规格」** 文件夹。
参考结构:
```text
folder <模块>说明
markdown 产品需求说明PRD ← 全文
folder PRD 分章
markdown 1. 一句话与目标
markdown 2. 模块边界
folder 专题规格
markdown <topic 标题>
```
`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. 根据改动的文件路径确定 `<prototype-id>`
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 分章」:各章可单独打开
- [ ] 用户故事仍为业务条线说明口径(起点 / 怎么运作 / 闭环)

View File

@@ -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. 交付口径
## 刻意不写
- 表结构、接口、代码路径
- 用实现指令代替业务闭环描述

View File

@@ -0,0 +1,78 @@
# 功能变更记录(定稿触发)
用户回复**需求定稿关键字**后,必须生成「自上次定稿以来」的功能变更日志,追加到 PRD **最下方**,并更新定稿基线。
## 触发关键字(命中任一)
`需求定稿` · `定稿` · `确认定稿` · `本次定稿`
(可带模块名,如「保险采购需求定稿」。)
## 写什么 / 不写什么
| 要写(产品发版视角) | 不写 |
|----------------------|------|
| 哪个功能做了什么修改 | 布局、页面样式、UI 设计优化 |
| 变更了什么业务逻辑 / 流程 / 验收口径 | 「优化了哪些表」「改了哪些字段/接口」 |
| 用户可感知的交互结果变化 | 纯样式、动效、无语义文案微调 |
条目口吻示例:
- 「识别失败」:失败数可点击查看失败文件与原因;仅失败时也可打开明细
- 「比价单」:审批通过后仍须在保单管理另行录入正式保单(强调不自动同步)
## 落点
1. **PRD 文末章节**(强制)
`src/prototypes/<id>/.spec/requirements-prd.md``## 10. 功能变更记录`
新定稿块插在该章**最上方**(倒序:最新在上);历史块保留。
2. **定稿基线**(强制)
`src/prototypes/<id>/.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. 确认 `<prototype-id>`(从对话 / 打开文件 / 用户指名推断)。
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 主流程增量更新第 19 章;**不必**每改一次就写第 10 章。
- **用户说定稿时**:才汇总写入第 10 章并刷新基线。

View File

@@ -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/<id>/.spec/requirements-prd.md`
2. `annotation-source.json` → 目录节点「产品需求说明PRD`markdownPath` + `markdown`
3. `src/resources/prd/<id>-autoprd.md`
4. 定稿时另写:`src/prototypes/<id>/.spec/autoprd-baseline.json`
## 用户故事写法(对照业务条线说明)
业务条线说明页字段 → AutoPRD 字段:
| 页面 | AutoPRD |
|------|---------|
| 责任部门标签 | **角色** |
| 起点 | **起点** |
| 怎么运作(有序列表) | **怎么运作** |
| 结果色签 | **关键结果**(可选) |
| 闭环 | **闭环** |
示例口吻(摘自业务条线风格,非照抄某模块):
- **起点**:采购部维护交强与商业险台账,作为交车合规的前置数据。
- **怎么运作**1. 运营录入或识别保单… 2. 运维交车时校验…
- **闭环**:核心险种有效则允许交车;异常则拦截并提示原因,过程可追溯。
**不要**用下面这种宽表作为第 4 章唯一形态:
| 编号 | 作为…我希望… | SP |
|------|--------------|----|
「作为…我想…以便…」只允许作为**排期可选一行**,主叙述必须是起点/怎么运作/闭环。
## 正逆向写法约定
- **正向**:用户成功完成任务的最短路径
- **逆向**:取消、关闭、禁用、冲突状态、不可操作项
- 审批类:写清「本页只读 / 在外部系统办理」
## 规模与故事点
| 规模 | 建议含义 | 约 SP |
|------|----------|-------|
| S | 轻交互 / 列表展示 | 12 |
| M | 标准流程 + 筛选详情 | 35 |
| L | 状态机 / 跨模块 / 批量识别 | 6+ |

View File

@@ -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 文档,再改状态,再按本文件建任务(负责人何斐)。

View File

@@ -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终态模型、`【交付】`主任务、多开发子任务、迭代测试/发版、Y01Y40、A01A10、旧规则迁移 | [references/oneos-terminal-flow.md](references/oneos-terminal-flow.md) |
| Lifecycle statuses, requirement labels, related tasks, R01R12, stage entry/completion | [references/lifecycle-rules.md](references/lifecycle-rules.md) |
| Design/development/test task creation, task states, rollups, TK01TK08 | [references/task-lifecycle.md](references/task-lifecycle.md) |
| Test plans, case results, failed cases, bugs, retest, TC01TC03, DF01DF03 | [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, CI02CI03, L01L02 | [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 R01R12 and TK01TK08; 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 <profile.json>
python scripts/profile_tool.py render <profile.json> --output <execution-plan.md>
python scripts/oneos_profile_tool.py validate <oneos-profile.json>
python scripts/oneos_profile_tool.py render <oneos-profile.json> --output <execution-plan.md>
```
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 R01R12 `rule_instances`, TK01TK08 `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 530 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 R01R12 and TK01TK08 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.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "云效需求生命周期自动化"
short_description: "使用口语化短口令驱动云效需求开发测试发布闭环并自动核查"
default_prompt: "使用 $yunxiao-requirement-lifecycle把当前项目设为统一运营管理平台PC端然后通过“记录需求、开始开发、提交测试、验收通过”等短口令协作。"

View File

@@ -0,0 +1,209 @@
{
"schema_version": 2,
"verified_at": "2026-07-21",
"verified_by_example": {
"requirement_serial": "ONEOS-88",
"requirement_identifier": "a34e700286d456ee74ddc16e58",
"note": "标签 APIPATCH /workitem/workitem/{id} propertyKey=tag operateType=COVER全量标签来自 space/tag/searchspaceType=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": "<html>", "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"]
}
}

View File

@@ -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 对照表。

View File

@@ -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":"请填写观察证据"}
]
}
]
}

View File

@@ -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": []
}
]
}

View File

@@ -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「同名」标签负责人计划开始已正式关联需求
查重:新建 / 复用
```
## 负向(不得误做)
- 需求仅到 `已确认` / `待处理`:不自动建分析/设计/待开发任务。
- 不得把样式类口头变更当成建任务触发。
- 不得在未查重时重复创建同阶段同名任务。
- 不得把负责人设错:分析中/设计中≠何斐(除非创建人就是何斐);待开发必须何斐(除非用户当次明确改派)。

View File

@@ -0,0 +1,60 @@
# Codeup研发资产集成
## 目录
1. 仓库覆盖
2. 分支策略
3. 正式关联
4. 提交与合并请求
5. CI01多仓库门禁
6. 验证场景
## 1. 仓库覆盖
前端、后端均可触发研发自动化,前提是仓库接入正确项目,分支/提交/MR正式关联需求规则没有错误限定到单一仓库。
逐仓库记录仓库名、角色、项目集成状态、Webhook、实际集成分支、是否纳入全部MR门禁和观察证据。
## 2. 分支策略
- 检查真实集成分支:存在`dev`则优先`dev`,否则使用存在的`develop`
- 从集成分支创建`feature/<WORK-ITEM-ID>``fix/<WORK-ITEM-ID>`
- 一个需求涉及多个仓库时,每个仓库可有一个同名活动分支。
- 快速迭代仍使用短生命周期分支;合并后是否删除按仓库策略和用户授权处理。
- 不直接推送保护分支,不为自动化方便创建不存在的`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指向非真实集成分支 | 阻塞并修正,不伪造通过 |

View File

@@ -0,0 +1,103 @@
# 需求生命周期与项目规则
## 目录
1. 状态参数化
2. R01R12规则矩阵
3. 阶段入口和阶段完成
4. 开发开始与开发完成
5. 标签、任务和关键字
6. 规则实例与限制
## 1. 状态参数化
典型流程为:
```text
待处理 → 已确认 → 分析中 → 分析完成 → 设计中 → 设计完成 → 待开发
→ 开发中 → 开发完成 → 测试中 → 测试完成 → 完成发布/发布完成 → 已完成/已关闭
```
逐项目读取真实状态。`完成发布``发布完成``已完成``已关闭`是候选映射,不能静默互换或创建近义重复状态。
## 2. R01R12规则矩阵
| ID | 流转 | 触发事件 | 必要条件 | 动作 | 方式 |
|---|---|---|---|---|---|
| R01 | 待处理 → 已确认 | 负责人确认 | 范围、负责人、优先级、验收口径明确 | 改为已确认 | 人工 |
| R02 | 已确认 → 分析中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`分析` | 改为分析中 | 原生规则 |
| R03 | 分析中 → 分析完成 | 关联任务状态变化 | 当前状态;本阶段分析任务全部完成 | 改为分析完成 | 原生/条件化 |
| R04 | 分析完成 → 设计中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`设计` | 改为设计中 | 原生规则 |
| R05 | 设计中 → 设计完成 | 关联任务状态变化 | 当前状态;本阶段设计任务全部完成 | 改为设计完成 | 原生/条件化 |
| R06 | 设计完成 → 待开发 | 添加关联任务工作项 | 当前状态;需求自身标签包含`开发` | 改为待开发 | 原生规则 |
| R07 | 待开发 → 开发中 | 添加关联分支或正式关联代码资产 | 当前状态;仓库已集成;资产正式关联 | 改为开发中 | 自动兜底 |
| R08 | 开发中 → 开发完成 | 关联合并请求状态变化 | 当前状态全部相关MR已合并 | 改为开发完成 | 原生规则 |
| R09 | 开发完成 → 测试中 | 添加关联任务工作项 | 当前状态;需求自身标签包含`测试` | 改为测试中 | 原生规则 |
| R10 | 测试中 → 测试完成 | 测试验收任务或获批回调 | 当前状态;用例、失败复测、缺陷均闭环 | 改为测试完成 | 人工/桥接 |
| R11 | 测试完成 → 发布完成状态 | 发布变更完成或获批回调 | 当前状态;目标环境发布证据有效 | 改为真实发布状态 | 原生/桥接 |
| R12 | 发布完成状态 → 项目终态 | 业务验收 | 当前状态;验收证据有效 | 改为真实终态 | 默认人工 |
R05R10与阶段任务自身状态的双向联动见`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. 规则实例与限制
每个项目分别记录R01R12的实际规则显示名/人工门禁、精确触发、源状态、目标状态、全部条件、动作、顺序、启用状态、执行账号和证据。
已知限制:
- 规则通常异步执行530秒内不要过早判失败。
- “全部关联项”只能看到已集成并正式关联的资产。
- 提前创建未来阶段任务可能阻塞“全部任务完成”。
- 需求状态规则不会自动更新阶段任务状态必须同时配置TK01TK08。
- 平台能力因模板、版本和权限不同以当前UI和执行日志为准。

View File

@@ -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. 触发最小事件等待530秒。
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天无活动的需求分支未经授权不删除。
- 分开统计自动流转时间与真实业务开始时间。
- 持续检查回调签名、时间戳、防重放、幂等和失败重试。

View File

@@ -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. 标准执行顺序(目标 13 分钟)
```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 后执行,结束解锁。

View File

@@ -0,0 +1,139 @@
# 统一运营管理平台 · 记录需求到云效(完整提示词)
复制下方「用户口令」整段发给 AgentAgent 须先弹出 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 查 identifierPATCH propertyKey=tag operateType=COVERAPI 失败 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`

View File

@@ -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 任务状态
- 主任务:首选与需求同名镜像状态。
- 开发任务:`待处理 → 处理中 → 已完成`,另有取消态。
- 测试任务:`待处理/待测试 → 测试中 → 已完成`
- 发版任务:`待处理 → 发布中 → 发布完成/发布失败`
若任务工作流不能承载主任务全量状态不配置Y02Y16原生镜像由桥接更新需求并记录主任务显示口径。
## 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+A02BAutoPRD**;进入待开发时按 [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入口恢复旧规则启用状态和顺序再恢复状态流转边保留已创建任务、关系和执行日志删除任何资产需单独授权。

View File

@@ -0,0 +1,66 @@
# 流水线、发布与最终验收
## 目录
1. 环境证据边界
2. R11发布完成
3. CI02回调POC
4. CI03证据分离
5. R12最终验收
6. L01L02历史规则
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. L01L02历史规则
- `L01``发布完成 + 全部关联任务完成/关闭 → 项目终态`。默认停用或收紧为独立验收任务,避免早期阶段任务误满足。
- `L02`:监听`测试完成 → 发布完成`状态路径并立即进入项目终态。默认停用,防止发布完成成为瞬时状态。
逐项目记录两条规则是否存在、是否启用、真实条件、顺序和证据;不存在也要有盘点证据。
## 7. 连锁触发验证
- 不按规则名称推断行为,逐条读取触发、条件、动作、顺序和执行账号。
- 每个目标状态都检查后继规则和重复规则。
- R11后至少观察两个时点刚进入发布完成时以及等待530秒后。
- 未经验收自动进入终态时,记录实际后继规则并判定`误触发`
- 跨两级以上流转必须有独立负向测试,不能只验证最终状态。

View File

@@ -0,0 +1,97 @@
# 云效自动化执行报告模板
```markdown
# 云效产品需求全生命周期自动化执行报告
## 1. 基本信息
- 组织:
- 项目:
- 工作项类型:
- 流程模型stage_tasks / oneos_delivery
- 迁移阶段仅oneos_deliveryP0 / 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
- 测试资产清理(需授权):
```

View File

@@ -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】。改动摘要【摘要】本地验证【命令和结果】。
请先拉取并检查冲突再按仓库规范提交、推送并创建MRMR必须正式关联需求和开发任务。不要直接推保护分支不要因为提交成功就把开发标为完成。
```
### 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
项目:
流程模型:
工作项:
源状态 → 实际状态:
本次真实事件:
执行动作:
触发方式:原生规则/桥接/人工门禁/未触发
验证结果:通过/异步通过/未触发/误触发/阻塞
证据:
未完成项:
下一责任角色:
允许的下一动作:
回滚入口:
```

View File

@@ -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`的MRMR同时关联需求和开发任务。提交成功不等于开发完成。
### 开发完成
开发人员或技术负责人输入:
```text
开发完成ONEOSP-123
```
Codex执行汇总该需求所有非取消开发任务、前后端仓库和正式关联MR。只有全部任务完成、全部相关MR已合并才确认需求进入`开发完成`;少一个仓库也不提前完成。
条件满足后,如果项目已配置并验证唯一测试任务桥接,则查重创建测试任务;否则提示测试负责人输入`提交测试`
## 5. 测试和缺陷
### 提交测试
测试负责人或开发负责人输入:
```text
提交测试ONEOSP-123test流水线=oneos-web-test
```
Codex执行确认开发完成触发或核查明确的test流水线。部署成功后查重创建并正式关联唯一测试任务和需求用例包进入`待测试/测试中`的项目真实状态。
test成功只允许开始测试不能变成发布完成。
### 开始测试
测试人员输入:
```text
开始测试ONEOSP-123负责人=赵六
```
Codex执行核查test部署和测试任务把测试任务改为`测试中/处理中`,需求进入`测试中`
### 发现Bug
测试人员输入:
```text
发现BugONEOSP-123用例=CASE-12现象=车辆来源显示0期望=显示中文来源
```
Codex执行查重后创建缺陷正式关联需求、原失败用例和必要开发任务用例保持未通过需求保持`测试中`
### Bug修复完成
开发人员输入:
```text
Bug修复完成BUG-123MR=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-900prod流水线=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. 自动规则执行后等待530秒再回看状态、正式关联和执行日志。
9. 不用手工改最终状态冒充自动化通过。
10. 不修改生产流水线定义不删除规则、分支、MR、任务或测试资产除非用户单独明确授权。

View File

@@ -0,0 +1,87 @@
# 阶段任务生命周期与自动创建
## 目录
1. 目标链路
2. TK01TK08规则
3. 关系与代码资产
4. 原生规则和桥接边界
5. 验证与回滚
## 1. 目标链路
```text
需求设计中 + 设计任务处理中
→ 设计任务已完成
→ 需求设计完成
→ 创建并关联开发任务
→ 需求待开发 + 开发任务待处理
→ 正式关联需求分支
→ 需求开发中 + 开发任务处理中
→ 全部相关MR合并到真实dev/develop
→ 需求开发完成 + 开发任务已完成
→ test流水线部署成功
→ 创建并关联测试任务
→ 需求测试中 + 测试任务处理中
→ 用例全部通过且缺陷复测关闭
→ 需求测试完成 + 测试任务已完成
```
分支只能合并到集成分支不能“合并到测试环境”。test环境由流水线部署部署成功用于允许测试开始不用于证明生产发布。
## 2. TK01TK08规则
| 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. 创建/修复后先核对标签与可见需求关系,再报告成功。

View File

@@ -0,0 +1,62 @@
# 测试用例与缺陷复测闭环
## 目录
1. 测试完成门槛
2. TC01TC03用例控制
3. DF01DF03缺陷控制
4. 实现方式
5. 验证场景
## 1. 测试完成门槛
R10只有同时满足以下条件才能完成
1. 范围内用例均已执行并有明确结果。
2. 不存在`未执行/待测试``阻塞``未通过`用例;获批暂缓/不适用项记录批准人与原因。
3. 每个未通过用例均正式创建或关联缺陷。
4. 修复后重新执行原用例并通过。
5. 范围内缺陷均由测试复测并进入项目真实关闭态。
6. 测试计划或测试验收任务由测试负责人确认。
流水线成功只能证明部署/自动检查成功,不能替代用例结果和缺陷复测。
## 2. TC01TC03用例控制
| ID | 控制 | 完成口径 |
|---|---|---|
| TC01 | 用例执行 | 每条范围内用例有真实执行结果;未执行、待测试、阻塞、未通过均阻塞测试完成 |
| TC02 | 失败用例关联缺陷 | 每条未通过用例正式创建或关联缺陷;仅写编号或链接不算关联 |
| TC03 | 用例复测更新 | 修复后重新执行原用例;通过才改为已通过,失败保持未通过 |
## 3. DF01DF03缺陷控制
| ID | 控制 | 完成口径 |
|---|---|---|
| DF01 | 已修复交接 | `已修复`只表示开发交给测试复测,不是终态,不解除测试门禁 |
| DF02 | 复测通过 | 缺陷进入项目真实关闭态,原失败用例同时更新为已通过 |
| DF03 | 复测失败 | 缺陷进入项目真实重开/处理中状态,原用例保持未通过 |
不得关闭缺陷却不更新原用例,也不得在缺陷仅为`已修复`时把用例标为通过。
## 4. 实现方式
先检查当前规则编辑器是否真的暴露Testhub执行用例和缺陷聚合条件
- 支持时:使用原生聚合规则,并保存条件和执行日志证据。
- 不支持时:使用测试验收任务/人工门禁。
- 需要全自动时单独评审Testhub OpenAPI/Webhook桥接要求签名、幂等、失败重试和审计记录。
不能把`not_supported`写成“已自动化”;必须指定负责人和回退方案。
## 5. 验证场景
| 场景 | 操作 | 期望 | 负向检查 |
|---|---|---|---|
| 用例未执行 | 保留一条未执行用例 | 保持测试中 | 不得完成测试 |
| 用例失败 | 记录未通过并正式关联缺陷 | 保持测试中 | 仅建缺陷不得完成 |
| 缺陷已修复 | 开发改为已修复 | 等待测试复测 | 不得关闭或通过用例 |
| 复测通过 | 重跑原用例并通过 | 用例通过、缺陷关闭 | 两者状态必须一致 |
| 复测失败 | 重跑仍失败 | 用例未通过、缺陷重开 | 不得保持已修复/关闭 |
| 全部闭环 | 全部用例通过且缺陷关闭 | 允许R10推进 | 检查是否误触发后继规则 |

View File

@@ -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())

View File

@@ -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([
"",
"### R01R12实际规则实例",
"",
"| 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([
"",
"### TK01TK08任务规则实例",
"",
"| 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())

View File

@@ -1,3 +1,7 @@
## 项目信息
- 项目名称OneOS1.2
# Agent 工作流程 # Agent 工作流程
## 🧭 核心顺序 ## 🧭 核心顺序
@@ -21,6 +25,7 @@
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` | | 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
| 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw``canvas-assets/` | 原型画布和画布素材 | | 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw``canvas-assets/` | 原型画布和画布素材 |
| 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` | | 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.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` | | 对象存储发布 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` | | 原型标注布局 | `src/common/prototype-annotation-host.tsx` | `rules/prototype-annotation-layout-guide.md` |

View File

@@ -1,13 +1,20 @@
# Agents 工作流程说明 ## 项目信息
## 🧭 工作流程 - 项目名称OneOS1.2
| 步骤 | 说明 | 参考文档 | # Agent 工作流程
|------|------|----------|
| ① 读取上下文 | 系统规则、用户资料、相关规范、已有原型与资源目录 | — | ## 🧭 核心顺序
| ② 产品需求对齐 | 新建原型、明显重构或需求模糊时,先收敛目标用户、核心任务、范围、功能清单、内容来源和验收重点 | `rules/requirements-alignment-guide.md` |
| ③ 设计方案对齐 | 产品需求确认后,先让用户从 3-4 个匹配的 `DESIGN.md` 候选中确认设计基底,再把布局、交互、视觉和内容呈现收敛为设计决策 | `rules/requirements-alignment-guide.md` | ```text
| ④ 原型开发与验收 | 根据已确认方案实现原型;遇到问题按错误信息定位修复,并完成预览验收 | `rules/prototype-development-guide.md` | 产品需求确认 -> 设计方案确认 -> 原型实现
```
| 阶段 | 继续前必须确认 | 参考文档 |
|------|----------------|----------|
| 产品需求 | 目标用户、核心任务、范围、功能清单、内容来源和验收重点 | `rules/requirements-alignment-guide.md` |
| 设计方案 | `DESIGN.md` 设计基底、信息架构、交互路径、关键组件取舍和视觉方向 | `rules/requirements-alignment-guide.md` |
| 原型实现 | 根据已确认的需求和设计方案实现原型 | `rules/prototype-development-guide.md` |
## 额外产物 ## 额外产物
@@ -16,39 +23,35 @@
| vm-page 列表分页 | `src/common/TablePagination.tsx` + `vm-pagination.css` | `src/prototypes/vm-shared/DESIGN.md` | | vm-page 列表分页 | `src/common/TablePagination.tsx` + `vm-pagination.css` | `src/prototypes/vm-shared/DESIGN.md` |
| 主题 | `src/themes/<theme-key>/` | `rules/theme-guide.md` | | 主题 | `src/themes/<theme-key>/` | `rules/theme-guide.md` |
| 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` | | 项目资料和文档 | `src/resources/` | `rules/resource-management-guide.md` |
| UI Review 结论 | `src/prototypes/<prototype-name>/.spec/ui-review.md` | `rules/ui-review-guide.md` | | 画布 | `src/prototypes/<prototype-name>/canvas.excalidraw``canvas-assets/` | 原型画布和画布素材 |
| 原型 Review 结论 | `src/prototypes/<prototype-name>/.spec/prototype-review.md` | `rules/prototype-review-guide.md` |
| 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` | | 业务逻辑规格 | `src/prototypes/<prototype-name>/.spec/<topic>.md` | `rules/business-logic-documentation-guide.md` |
| 对象存储发布 URL | `.axhub/make/axhub.config.json``cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` | | 对象存储发布 URL | `.axhub/make/axhub.config.json``cloudPublishing.s3` | `rules/cloud-publish-url-guide.md` |
| ACP 对话缓存 | `src/prototypes/<prototype-name>/.spec/acp/` | 本地私有运行数据,不提交、不导出、不发布 | | 原型标注布局 | `src/common/prototype-annotation-host.tsx` | `rules/prototype-annotation-layout-guide.md` |
## ⚠️ 重要原则 ## ⚠️ 重要原则
1. **产品需求设计方案分阶段对齐** 1. **需求设计是继续工作的门禁**
- 先确认做什么,再确认怎么表达;读取资料、规格/计划确认和开发验收过程中,发现影响方向的问题都要回到相应阶段继续对齐 - 产品需求未确认时,禁止继续找视觉参考、拆页面结构、整理 `DESIGN.md` 候选、写设计方案或开发实现;先用简短摘要让用户确认目标用户、核心任务、范围/功能清单、内容来源和验收重点
- 设计方案未确认时,禁止继续写规格/计划或开发实现;先确认设计基底、信息架构、交互路径、关键组件取舍和视觉方向
- 发现目标、范围、内容来源、验收重点、信息架构、交互路径、视觉方向或设计基底存在多种合理选择时,立即停在对应门禁补充对齐
| 阶段 | 需要对齐的情况 | 2. **设计要判断何时收敛、何时发散**
|------|----------------|
| 读取资料 | 目标、边界、素材、参考或约束不清 |
| 产品需求 | 出现不同目标用户、功能范围、内容来源或验收标准 |
| 设计方案 | 出现不同信息架构、交互路径、视觉方向或设计基底 |
| 开发验收 | 实现结果、体验取舍或验收标准发生变化 |
2. **优先创建和维护 task/todo**
- 多步骤、高风险、需求对齐、方案确认或跨文件任务,优先用 task/todo 记录当前步骤、状态和下一步
- 简单局部修改可以保持轻量,但要清楚说明当前正在处理什么、完成后如何验收
3. **设计要判断何时收敛、何时发散**
- AI 应自行判断当前需要收拢需求还是探索解法:需求不清先收敛;需要改善体验或创新表达时再发散。发散是为了帮助用户选择最终方向 - AI 应自行判断当前需要收拢需求还是探索解法:需求不清先收敛;需要改善体验或创新表达时再发散。发散是为了帮助用户选择最终方向
3. **原型按生产级界面处理**
- 本项目中的「原型」默认是可运行、接近正式产品的前端页面不是黑白灰线框图或低保真草稿只有用户明确要求时才使用低保真、wireframe、placeholder 等表达
4. **不要把截图当唯一真相** 4. **不要把截图当唯一真相**
- 截图用于视觉参考;有代码、组件、设计系统、业务资料或用户说明时,要结合上下文判断 - 截图用于视觉参考;有代码、组件、设计系统、业务资料或用户说明时,要结合上下文判断
5. **早展示,早反馈** 5. **早展示,早反馈**
- 产品需求、设计方案或原型应尽早交给用户确认,不要等到全部完成后才暴露方向问题 - 产品需求、设计方案或原型应尽早交给用户确认,不要等到全部完成后才暴露方向问题
- 涉及页面意图、组件取舍或多方案比稿时,优先用低成本、快速的 Markdown ASCII Wireframe/Diagram 或 Mermaid 展示方案,先对齐需求
6. **讲人话,用户不懂技术** 6. **讲人话,用户不懂技术**
- 用用户能理解的方式说明取舍、风险和结果;用户无法执行 CLI 命令,不得省略验收流程 - 用用户能理解的方式说明取舍、风险和结果;用户无法执行 CLI 命令,不得省略验收流程
- 向用户请求反馈或验收时,提醒用户尽量提供截图、预览链接、页面路径或具体问题位置,便于准确定位和复现
7. **复杂业务逻辑必须文档化** 7. **复杂业务逻辑必须文档化**
- 多步判定、跨模块校验、状态/公式推导、导入规则等,除代码外必须写入 `.spec/*.md``requirements-prd.md`,并同步标注目录(见 `rules/business-logic-documentation-guide.md` - 多步判定、跨模块校验、状态/公式推导、导入规则等,除代码外必须写入 `.spec/*.md``requirements-prd.md`,并同步标注目录(见 `rules/business-logic-documentation-guide.md`
- 不得默认「先实现、后补文档」;逻辑变更与 PRD/标注更新同一轮交付
8. **对象存储发布 URL 与 Make 工具一致** 8. **对象存储发布 URL 与 Make 工具一致**
- `{baseUrl}/{prototype-id}/index.html`;禁止擅自改路径后缀(见 `rules/cloud-publish-url-guide.md` - 发布或写链接时格式为 `{baseUrl}/{prototype-id}/index.html`;禁止`prototypes/` 前缀或去掉 `index.html`(见 `rules/cloud-publish-url-guide.md`
## 项目结构 ## 项目结构

736
DESIGN.md
View File

@@ -1,736 +0,0 @@
---
version: alpha
name: Vercel-design-analysis
description: An inspired interpretation of Vercel's design language — a developer-platform brand whose surface is a stark black-and-ink duet on near-white canvas, broken at hero scale by a multi-color mesh gradient (cyan / blue / magenta / amber) that acts as the entire decorative system, paired with a custom geometric sans for headlines and a monospaced caption face for technical labels.
colors:
primary: "#171717"
on-primary: "#ffffff"
ink: "#171717"
body: "#4d4d4d"
mute: "#888888"
hairline: "#ebebeb"
hairline-strong: "#a1a1a1"
canvas: "#ffffff"
canvas-soft: "#fafafa"
canvas-soft-2: "#f5f5f5"
link: "#0070f3"
link-deep: "#0761d1"
link-bg-soft: "#d3e5ff"
success: "#0070f3"
error: "#ee0000"
error-soft: "#f7d4d6"
error-deep: "#c50000"
warning: "#f5a623"
warning-soft: "#ffefcf"
warning-deep: "#ab570a"
violet: "#7928ca"
violet-soft: "#d8ccf1"
violet-deep: "#4c2889"
cyan: "#50e3c2"
cyan-soft: "#aaffec"
cyan-deep: "#29bc9b"
highlight-pink: "#ff0080"
highlight-magenta: "#eb367f"
gradient-develop-start: "#007cf0"
gradient-develop-end: "#00dfd8"
gradient-preview-start: "#7928ca"
gradient-preview-end: "#ff0080"
gradient-ship-start: "#ff4d4d"
gradient-ship-end: "#f9cb28"
selection-bg: "#171717"
selection-fg: "#f2f2f2"
typography:
display-xl:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 48px
fontWeight: 600
lineHeight: 48px
letterSpacing: -2.4px
display-lg:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 32px
fontWeight: 600
lineHeight: 40px
letterSpacing: -1.28px
display-md:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 24px
fontWeight: 600
lineHeight: 32px
letterSpacing: -0.96px
display-sm:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 20px
fontWeight: 600
lineHeight: 28px
letterSpacing: -0.6px
body-lg:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 18px
fontWeight: 400
lineHeight: 28px
letterSpacing: 0px
body-md:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 16px
fontWeight: 400
lineHeight: 24px
body-md-strong:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 16px
fontWeight: 500
lineHeight: 24px
body-sm:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 14px
fontWeight: 400
lineHeight: 20px
letterSpacing: -0.28px
body-sm-strong:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 14px
fontWeight: 500
lineHeight: 20px
letterSpacing: -0.28px
caption:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 12px
fontWeight: 400
lineHeight: 16px
caption-mono:
fontFamily: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, monospace
fontSize: 12px
fontWeight: 400
lineHeight: 16px
code:
fontFamily: Geist Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, monospace
fontSize: 13px
fontWeight: 400
lineHeight: 20px
button-md:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 14px
fontWeight: 500
lineHeight: 20px
button-lg:
fontFamily: Geist, Inter, system-ui, -apple-system, sans-serif
fontSize: 16px
fontWeight: 500
lineHeight: 24px
rounded:
none: 0px
xs: 4px
sm: 6px
md: 8px
lg: 12px
xl: 16px
pill-sm: 64px
pill: 100px
full: 9999px
spacing:
xxs: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 32px
2xl: 40px
3xl: 48px
4xl: 64px
5xl: 96px
6xl: 128px
section: 192px
components:
nav-bar:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
height: 64px
padding: "{spacing.sm} {spacing.lg}"
nav-link:
textColor: "{colors.body}"
typography: "{typography.body-sm}"
rounded: "{rounded.full}"
padding: "{spacing.xs} {spacing.sm}"
nav-cta-signup:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.body-sm-strong}"
rounded: "{rounded.sm}"
padding: "0px {spacing.xs}"
height: 28px
nav-cta-login:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm-strong}"
rounded: "{rounded.sm}"
padding: "0px {spacing.xs}"
height: 28px
nav-cta-ask-ai:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
borderColor: "{colors.hairline}"
typography: "{typography.body-sm-strong}"
rounded: "{rounded.sm}"
padding: "0px {spacing.xs}"
height: 28px
button-primary:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.button-lg}"
rounded: "{rounded.pill}"
padding: "0px {spacing.sm}"
button-secondary:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.button-lg}"
rounded: "{rounded.pill}"
padding: "0px {spacing.sm}"
button-primary-sm:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.button-md}"
rounded: "{rounded.pill}"
padding: "0px {spacing.xs}"
button-secondary-sm:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.button-md}"
rounded: "{rounded.pill}"
padding: "0px {spacing.xs}"
tab-ghost:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-sm}"
rounded: "{rounded.pill-sm}"
padding: "0px {spacing.md}"
icon-button-circular:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
borderColor: "{colors.hairline}"
rounded: "{rounded.full}"
card-marketing:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.md}"
padding: "{spacing.lg}"
card-marketing-large:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.lg}"
padding: "{spacing.xl}"
card-soft:
backgroundColor: "{colors.canvas-soft}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.md}"
padding: "{spacing.lg}"
template-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.md}"
padding: "{spacing.md}"
code-editor-mockup:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.code}"
rounded: "{rounded.md}"
padding: "{spacing.lg}"
form-input:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
borderColor: "{colors.hairline}"
typography: "{typography.body-sm}"
rounded: "{rounded.sm}"
padding: "0px {spacing.sm}"
height: 40px
form-input-sm:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
borderColor: "{colors.hairline}"
typography: "{typography.body-sm}"
rounded: "{rounded.sm}"
padding: "0px {spacing.sm}"
height: 32px
form-input-lg:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
borderColor: "{colors.hairline}"
typography: "{typography.body-md}"
rounded: "{rounded.sm}"
padding: "0px {spacing.sm}"
height: 48px
badge-secondary:
backgroundColor: "{colors.canvas-soft}"
textColor: "{colors.body}"
typography: "{typography.caption}"
rounded: "{rounded.full}"
padding: "0px {spacing.xs}"
pricing-card:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.body-md}"
rounded: "{rounded.lg}"
padding: "{spacing.xl}"
pricing-card-featured:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.body-md}"
rounded: "{rounded.lg}"
padding: "{spacing.xl}"
logo-strip:
backgroundColor: "{colors.canvas}"
textColor: "{colors.body}"
typography: "{typography.body-sm}"
padding: "{spacing.lg} {spacing.xl}"
hero-band:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.display-xl}"
padding: "{spacing.4xl} {spacing.lg}"
feature-mesh-band:
backgroundColor: "{colors.canvas}"
textColor: "{colors.ink}"
typography: "{typography.display-lg}"
padding: "{spacing.5xl} {spacing.lg}"
showcase-band-light:
backgroundColor: "{colors.canvas-soft}"
textColor: "{colors.ink}"
typography: "{typography.display-lg}"
padding: "{spacing.5xl} {spacing.lg}"
showcase-band-dark:
backgroundColor: "{colors.primary}"
textColor: "{colors.on-primary}"
typography: "{typography.display-lg}"
padding: "{spacing.5xl} {spacing.lg}"
footer:
backgroundColor: "{colors.canvas}"
textColor: "{colors.body}"
typography: "{typography.body-sm}"
padding: "{spacing.4xl} {spacing.lg}"
link-inline:
textColor: "{colors.link}"
typography: "{typography.body-md}"
banner-marketing:
backgroundColor: "{colors.canvas-soft}"
textColor: "{colors.body}"
typography: "{typography.body-sm}"
rounded: "{rounded.full}"
padding: "{spacing.xs} {spacing.sm}"
# ─── Examples (illustrative) — auto-derived; resolve any TO_FILL markers below ───
ex-pricing-tier:
description: "Default tier card. Mirrors pricing-card chrome on canvas-soft surface with a hairline border."
backgroundColor: "{colors.canvas-soft}"
textColor: "{colors.ink}"
borderColor: "{colors.hairline}"
rounded: "{rounded.lg}"
padding: "{spacing.xl}"
ex-pricing-tier-featured:
description: "Featured tier — polarity-flipped to ink primary with white text and white CTA."
backgroundColor: "{colors.ink}"
textColor: "{colors.on-primary}"
rounded: "{rounded.lg}"
padding: "{spacing.xl}"
ex-product-selector:
description: "What's Included summary card — repurposed for the brand's GPU / inference / Pro feature tiers."
backgroundColor: "{colors.canvas-soft}"
rounded: "{rounded.md}"
padding: "{spacing.lg}"
ex-cart-drawer:
description: "Subscription summary — line items per add-on (NOT a literal e-commerce cart)."
backgroundColor: "{colors.canvas}"
rounded: "{rounded.md}"
padding: "{spacing.lg}"
item-divider: "{colors.hairline}"
ex-app-shell-row:
description: "Sidebar nav row. Active state uses brand primary as a left-edge indicator bar."
backgroundColor: "{colors.canvas}"
activeIndicator: "{colors.primary}"
rounded: "{rounded.sm}"
padding: "{spacing.xs} {spacing.sm}"
ex-data-table-cell:
description: "Mirrors the brand's table chrome. Header uses caption-mono uppercase mono; body uses body-sm."
headerBackground: "{colors.canvas-soft}"
headerTypography: "{typography.caption-mono}"
bodyTypography: "{typography.body-sm}"
cellPadding: "{spacing.xs} {spacing.sm}"
rowBorder: "{colors.hairline}"
ex-auth-form-card:
description: "Sign-in / sign-up card. Mirrors card-marketing-large chrome with form-input primitives inside."
backgroundColor: "{colors.canvas-soft}"
rounded: "{rounded.lg}"
padding: "{spacing.xl}"
ex-modal-card:
description: "Modal dialog surface — same chrome as card-marketing-large with Level 5 modal shadow."
backgroundColor: "{colors.canvas}"
rounded: "{rounded.lg}"
padding: "{spacing.xl}"
ex-empty-state-card:
description: "Empty-state illustration frame. Generous padding on canvas-soft."
backgroundColor: "{colors.canvas-soft}"
rounded: "{rounded.lg}"
padding: "{spacing.3xl}"
captionTypography: "{typography.body-md}"
ex-toast:
description: "Toast notification surface — flat-cornered card-marketing chrome with Level 4 shadow."
backgroundColor: "{colors.canvas}"
rounded: "{rounded.md}"
padding: "{spacing.sm} {spacing.md}"
typography: "{typography.body-sm}"
---
## Overview
Vercel is a developer-platform brand — the page is a deployment dashboard's marketing surface, written for engineers who already know the syntax. It earns that posture with one of the cleanest stark systems on the web: near-white `{colors.canvas-soft}` body background, ink-near-black `{colors.ink}` text, a 200-step gray scale that gives every divider, border, and disabled state its own deliberate step. The only place the brand introduces colour at marketing scale is the multi-stop mesh gradient (`{colors.gradient-develop-start}``{colors.gradient-preview-end}``{colors.gradient-ship-start}` → cyan / magenta / amber) that floats in atmospheric backdrops, never miniaturised to a swatch. That gradient is the entire decoration system.
Type is the second decisive voice. The brand's own custom geometric sans (Geist) carries display, body, button — everything narrative — at weight 600 for display, 500 for buttons, 400 for body. A matching monospaced face (Geist Mono) carries technical labels: terminal mockups, code blocks, sometimes filename captions. Headlines are sentence-case with aggressive negative letter-spacing (`-2.4px` at 48 px hero) — the brand never letter-spaces positively, never goes uppercase outside of mono labels.
Surfaces use a four-step ladder: `{colors.canvas}` (pure white for cards), `{colors.canvas-soft}` 98% (the page body), `{colors.canvas-soft-2}` 95% (occasional inset region), `{colors.primary}` (the deep ink-near-black used as the polarity-flipped band when a section needs the dark mode treatment). Shadows are exceptionally subtle — every elevated card carries a stacked shadow built from `0px 1px 1px #00000005` + `0px 2px 2px #0000000a` + an inset border. Cards never float on heavy drop-shadow; they sit on the page held by hairline + soft glow.
**Key Characteristics:**
- A single black-ink primary CTA `{colors.primary}` carries every conversion target, paired with white-on-white `button-secondary` for the secondary action. The brand uses 100 px pill shape for marketing CTAs and a tight 6 px square shape for in-app nav buttons.
- A multi-stop mesh gradient (cyan-blue-magenta-amber) is the only decorative chrome — used at hero scale and inside feature-band atmospheric backdrops. It is the brand.
- Every section eyebrow and small label uses the monospace face `{typography.caption-mono}` or `{typography.code}`; everything else is in the geometric sans.
- Subtle stacked-shadow elevation — three offsets layered with 4-12 % black opacity — never a single heavy drop-shadow.
- A complete 1001000 gray + blue + red + amber + green + teal + purple + pink colour scale exists as a system token set, but the marketing surface uses only the `100`, `1000`, and `700`-level tones; the rest stay in the design-system tokens for in-product surfaces.
- An "Active CPU" pricing rhythm: `pricing-card` lays out 3-up on the pricing page with `pricing-card-featured` (Pro tier) polarity-flipped to `{colors.primary}` against white-card siblings.
## Colors
### Brand & Accent
- **Ink** (`{colors.primary}``#171717`): The single primary CTA color. Black-near-pure ink that carries every Sign Up pill, every footer CTA, the dark-band polarity-flip. Used as text color throughout the page on light surfaces. (Resolved from `--ds-gray-1000`.)
- **Cyan** (`{colors.cyan}``#50e3c2`): A signature mint-cyan used in the brand gradient and inside Geist-system spotlight tokens. Visible inside the hero gradient stops.
- **Highlight Pink** (`{colors.highlight-pink}``#ff0080`): The brand's highlight magenta, used as the high-saturation stop in the preview-gradient pair.
- **Violet** (`{colors.violet}``#7928ca`): The deep purple used as the start of the preview-gradient and inside developer-console highlights.
- **Link Blue** (`{colors.link}``#0070f3`): The brand's primary link color and the legacy `--geist-success` semantic.
### Surface
- **Canvas** (`{colors.canvas}``#ffffff`): The pure-white card / dialog / modal surface.
- **Canvas Soft** (`{colors.canvas-soft}``#fafafa`): The default page background — 98 % white. Almost every section sits on this tone.
- **Canvas Soft 2** (`{colors.canvas-soft-2}``#f5f5f5`): A slightly deeper inset surface for "code editor inner background", template-card hover states, and dropdown menus.
- **Hairline** (`{colors.hairline}``#ebebeb`): 1 px dividers — table rows, card borders, input borders.
- **Hairline Strong** (`{colors.hairline-strong}``#a1a1a1`): The 500-level gray, used as the slightly-stronger divider on light bands and as the deemphasised text color.
### Text
- **Ink** (`{colors.ink}``#171717`): Every heading and body paragraph on light surfaces.
- **Body** (`{colors.body}``#4d4d4d`): Secondary text — sub-headings, body captions, nav-link inactive text, footer column body.
- **Mute** (`{colors.mute}``#888888`): Lowest-priority text — placeholder text, fine print, low-key labels.
- **On Primary** (`{colors.on-primary}``#ffffff`): All text on `{colors.primary}` surfaces.
### Semantic
- **Success / Link** (`{colors.success}``#0070f3`): The brand's legacy success indicator doubles as the primary link color. Visible underline-on-hover for inline body links.
- **Link Deep** (`{colors.link-deep}``#0761d1`): The pressed / visited tone for inline links.
- **Link Bg Soft** (`{colors.link-bg-soft}``#d3e5ff`): Soft pastel blue fill for "what's new" pill banners and informational badges.
- **Error** (`{colors.error}``#ee0000`): Validation red for destructive actions and form errors.
- **Error Soft** (`{colors.error-soft}``#f7d4d6`): Soft pastel red for destructive-state backgrounds.
- **Error Deep** (`{colors.error-deep}``#c50000`): Pressed / deep destructive state.
- **Warning** (`{colors.warning}``#f5a623`): Caution / pending status indicator.
- **Warning Soft** (`{colors.warning-soft}``#ffefcf`) / **Warning Deep** (`{colors.warning-deep}``#ab570a`): Background + pressed variants.
### Brand Gradient
The brand's signature decoration is a three-pair gradient stack:
- **Develop** (`{colors.gradient-develop-start}` `#007cf0``{colors.gradient-develop-end}` `#00dfd8`) — the blue-to-teal pair used to mark the "deploy" / "develop" rhythm.
- **Preview** (`{colors.gradient-preview-start}` `#7928ca``{colors.gradient-preview-end}` `#ff0080`) — the violet-to-pink pair used for "preview" surfaces.
- **Ship** (`{colors.gradient-ship-start}` `#ff4d4d``{colors.gradient-ship-end}` `#f9cb28`) — the coral-to-amber pair used for "ship" surfaces.
The three pairs collapse into a single multi-color mesh gradient when used as the hero atmospheric backdrop. Treat the gradient as one unified object — do not crop down to a single colour, do not reorder the stops, and do not miniaturise. Used at hero scale only.
## Typography
### Font Family
Two custom faces carry the entire system:
1. **A custom geometric sans** (extracted as `Geist`) for every display, body, button, link, and label. Weights 400 / 500 / 600 are the working set; the face never appears in 700 or heavier. Display sizes are tracked aggressively negative (`-2.4 px` at 48 px hero, `-1.28 px` at 32 px section); body stays at neutral or slightly-negative tracking.
2. **A custom monospaced face** (extracted as `Geist Mono`) for terminal mockups, code blocks, and small mono-caption labels — anything that wants to signal "technical." Weight 400 only at 12 13 px. Tracking neutral.
A condensed display sans (`Space Grotesk`) is loaded as a third face for occasional editorial moments but does not render as the primary face anywhere in the captured surfaces.
### Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---|---|---|---|---|
| `{typography.display-xl}` | 48px | 600 | 48px | -2.4px | Hero headline ("Build and deploy on the AI Cloud."). |
| `{typography.display-lg}` | 32px | 600 | 40px | -1.28px | Section headlines ("Your frontend, delivered.", "A compute model for all workloads."). |
| `{typography.display-md}` | 24px | 600 | 32px | -0.96px | Card-cluster headlines, pricing-tier names. |
| `{typography.display-sm}` | 20px | 600 | 28px | -0.6px | Inline display micro-headings. |
| `{typography.body-lg}` | 18px | 400 | 28px | 0 | Lead paragraphs under section headlines. |
| `{typography.body-md}` | 16px | 400 | 24px | 0 | Default body paragraph. |
| `{typography.body-md-strong}` | 16px | 500 | 24px | 0 | Bolded inline body. |
| `{typography.body-sm}` | 14px | 400 | 20px | -0.28px | Secondary body, nav-link text, button-md labels. |
| `{typography.body-sm-strong}` | 14px | 500 | 20px | -0.28px | Nav CTA labels, table-row emphasis. |
| `{typography.caption}` | 12px | 400 | 16px | 0 | Footer secondary lines, badge labels. |
| `{typography.caption-mono}` | 12px | 400 | 16px | 0 | Section eyebrows and label captions that want a technical voice. |
| `{typography.code}` | 13px | 400 | 20px | 0 | Inline code, terminal mockups, command snippets. |
| `{typography.button-md}` | 14px | 500 | 20px | 0 | Small / nav-scale button labels. |
| `{typography.button-lg}` | 16px | 500 | 24px | 0 | Marketing-scale pill button labels. |
### Principles
- **Negative tracking is part of the voice.** Display sizes use aggressive `-2.4` to `-0.6` px tracking. Reverting to default tracking breaks the brand.
- **Sentence-case headlines, period-terminated.** Headlines like "Build and deploy on the AI Cloud." end with a deliberate period — that punctuation is part of the brand's voice.
- **Mono for the technical layer only.** Section eyebrows, code blocks, terminal mockups. Body paragraphs never set in mono.
- **Weight 600 is the display ceiling.** The geometric sans never appears at 700 / 800. The brand reads as a calmer system because of this.
### Note on Font Substitutes
The two primary faces are proprietary (custom-cut for the brand). Open-source substitutes:
- **Geometric sans** — *Inter* (400 / 500 / 600) is the closest stylistic match; `font-feature-settings: "ss01", "ss02"` enables the geometric alternates. *Satoshi* is a passable second choice.
- **Monospace** — *JetBrains Mono* (400) at 12 13 px matches the technical voice. *IBM Plex Mono* is the second-best option.
## Layout
### Spacing System
- **Base unit**: 4 px. The brand's `--geist-space` token is exactly 4 px and every captured value is a multiple of 4.
- **Tokens**: `{spacing.xxs}` 4 px · `{spacing.xs}` 8 px · `{spacing.sm}` 12 px · `{spacing.md}` 16 px · `{spacing.lg}` 24 px · `{spacing.xl}` 32 px · `{spacing.2xl}` 40 px · `{spacing.3xl}` 48 px · `{spacing.4xl}` 64 px · `{spacing.5xl}` 96 px · `{spacing.6xl}` 128 px · `{spacing.section}` 192 px.
- **Section padding**: marketing bands use `{spacing.4xl}` to `{spacing.5xl}` top/bottom. Hero bands stretch to `{spacing.section}` to give the mesh gradient room to breathe.
- **Card interior padding**: marketing cards sit at `{spacing.lg}` to `{spacing.xl}`; template-grid cards stay tighter at `{spacing.md}` because they sit in a denser grid.
- **Inline gap**: button rows, nav rows, and chip rows use `{spacing.sm}` to `{spacing.md}` between siblings. The brand's `--geist-gap` is exactly 24 px.
### Grid & Container
- **Max width**: ~1400 px (`--ds-page-width`); the legacy `--geist-page-width` is 1200 px and still appears on some marketing surfaces. Content centres with horizontal gutters of `{spacing.lg}` 24 px on desktop, `{spacing.md}` 16 px on mobile.
- **Column patterns**:
- Three-feature row: 3-up at desktop, 1-up at mobile (rows like "Web Apps / Composable Commerce / Multi-tenant Platforms").
- Tab pill row: 5-up centred row of `tab-ghost` pills.
- Template-grid cluster: 5-up at desktop, scaling to 1-up at mobile.
- Pricing tier grid: 3-up at desktop with the middle tier polarity-flipped.
- Logo strip: ~5 logos wide, single row.
### Whitespace Philosophy
The mesh gradient does most of the heavy decorative lifting; whitespace separates the bands. Section spacing is generous — `{spacing.4xl}` to `{spacing.5xl}` between bands lets the gradient breathe. Inside a card, the headline/paragraph stack is tight (`{spacing.xs}` 8 px gap), then a wider gap before the CTA cluster. The page reads as engineered — large gaps + tight interior, never the other way around.
### Responsive Strategy
#### Breakpoints
| Name | Width | Key Changes |
|---|---|---|
| Mobile | < 600px | Hero stacks; nav collapses to hamburger; 3-up feature grids drop to 1-up; tab pill row enables horizontal scroll. |
| Tablet | 600959px | 3-up grids drop to 2-up; nav still horizontal. |
| Desktop | 9601199px | Full 3-up grids; pricing 3-up. |
| Wide | 12001399px | Container caps at 1400 px content width. |
| Ultra-wide | ≥ 1400px | Content stays centred at 1400 px; bands stretch edge-to-edge in colour but content holds the max-width. |
#### Touch Targets
The `button-primary` pill renders at ~32 px tall in nav and ~48 px tall in marketing contexts. Marketing CTAs comfortably meet WCAG AAA at all breakpoints; nav buttons inflate touch area through `{spacing.xs}` padding on mobile to meet the 44 × 44 px floor.
#### Collapsing Strategy
- **Nav**: full link row + Ask AI / Log In / Sign Up pills at desktop. Collapses to logo + hamburger at mobile with the menu opening as a full-overlay.
- **Hero**: mesh gradient stays centred; headline + body stack vertically at all breakpoints (the brand doesn't use a split-hero pattern).
- **Three-feature row**: 3-up → 2-up → 1-up at the breakpoints above; cards keep their `{rounded.md}` 8 px shape across all viewports.
- **Pricing card grid**: 3-up at desktop, vertical stack at mobile with `pricing-card-featured` always sitting in the middle.
- **Template grid**: 5-up → 3-up → 2-up → 1-up. Each `template-card` keeps its 16:9 aspect on the image.
#### Image Behavior
- **Mesh gradient**: rendered as inline SVG or canvas-painted gradient; scales fluidly with the hero container; never crops, never tiles.
- **Customer logos**: rendered as monochrome SVGs in the logo strip; consistent 24 px height.
- **Code editor mockup**: dark `{colors.primary}` rectangle with mono text rendered inside; treated as an image at the layout level.
- **Template thumbnails**: 16:9 landscape inside `{rounded.md}` card chrome; lazy-loaded; consistent grayscale palette in the placeholder state.
## Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| Level 0 — Flat | No shadow, no border. | Full-bleed hero bands and the polarity-flipped dark sections. |
| Level 1 — Inset Hairline | `0 0 0 1px #00000014` inset 1 px border. | Default card chrome — the brand's universal "you can see this card" cue. |
| Level 2 — Subtle Drop | `0px 1px 1px #00000005, 0px 2px 2px #0000000a` plus inset hairline. | Slightly elevated cards (template-grid, marketing-card). |
| Level 3 — Soft Stack | `0px 2px 2px #0000000a, 0px 8px 8px -8px #0000000a` plus inset hairline. | The "medium" elevation — feature-grid cards. |
| Level 4 — Float Stack | `0px 2px 2px #0000000a, 0px 8px 16px -4px #0000000a` plus inset hairline. | "Large" elevation — pricing cards, callout panels. |
| Level 5 — Modal | `0px 1px 1px #00000005, 0px 8px 16px -4px #0000000a, 0px 24px 32px -8px #0000000f` plus inset hairline. | Modal / dialog surfaces and dropdown menus. |
The brand uses STACKED shadows — multiple small offsets layered to fake natural light — never a single 8-px-blur generic drop. Inset hairline rings are always added so the card edge stays crisp.
### Decorative Depth
- **Mesh gradient as atmospheric depth**: the hero's multi-stop gradient is the brand's only "atmospheric" effect — applied as a flat 2-D backdrop rather than a 3-D illustration.
- **Polarity-flipped dark band as section-depth**: switching the surface from `{colors.canvas-soft}` to `{colors.primary}` (the deep ink) is the brand's chief depth cue between bands.
- **Inset-shadow + drop-shadow combo**: the cards' combination of an inset 1 px ring and a multi-stop drop produces a "card sits on the page" effect without ever feeling material-heavy.
## Shapes
### Border Radius Scale
| Token | Value | Use |
|---|---|---|
| `{rounded.none}` | 0px | Full-bleed hero / footer bands. |
| `{rounded.xs}` | 4px | Tightest inline pill — the `nav-cta-signup` 6-px-radius button (mapped to `xs/sm`). |
| `{rounded.sm}` | 6px | The brand's `--geist-radius` token — base UI radius for in-app buttons, form inputs, dropdown menus. |
| `{rounded.md}` | 8px | The brand's `--geist-marketing-radius` token — feature cards, template cards. |
| `{rounded.lg}` | 12px | Slightly larger card chrome (pricing-card variants). |
| `{rounded.xl}` | 16px | Largest card chrome — when a card hosts a hero image cap. |
| `{rounded.pill-sm}` | 64px | Tab-ghost pills inside the "AI Apps / Web Apps / Ecommerce / Marketing / Platforms" row. |
| `{rounded.pill}` | 100px | The marketing CTA pill — `button-primary`, `button-secondary`, "Start Deploying" pill. |
| `{rounded.full}` | 9999px | Icon-button circular containers, nav-link ghost pills. |
### Photography Geometry
- **Mesh gradient**: full-bleed 2-D atmospheric backdrop, never cropped to a frame; treated as the page's wallpaper.
- **Customer logos**: monochrome SVG, consistent 24 px height in a flex row.
- **Code editor mockup**: 16:10 dark rectangle, `{rounded.md}` corners.
- **Template thumbnails**: 16:9 landscape inside `{rounded.md}` chrome.
- **Showcase imagery**: 2:1 or 16:9 inside `{rounded.lg}` to `{rounded.xl}` chrome with a stacked shadow.
## Components
### Buttons
**`button-primary`** — the canonical 100-px-radius black pill, marketing scale.
- Background `{colors.primary}`, text `{colors.on-primary}`, label set in `{typography.button-lg}`, padding `0px {spacing.sm}` 12 px, shape `{rounded.pill}` 100 px. Renders ~48 px tall when paired with the marketing flex layout.
**`button-secondary`** — the white pill paired with the black primary inside marketing bands.
- Background `{colors.canvas}`, text `{colors.ink}`, same typography + padding as `button-primary`, shape `{rounded.pill}`.
**`button-primary-sm`** — the smaller-scale primary pill used inside nav and pricing-card CTAs.
- Background `{colors.primary}`, text `{colors.on-primary}`, label set in `{typography.button-md}` (14 px / 500), shape `{rounded.pill}`.
**`button-secondary-sm`** — the smaller-scale white pill paired with `button-primary-sm`.
- Background `{colors.canvas}`, text `{colors.ink}`, same typography + shape as `button-primary-sm`.
**`tab-ghost`** — the centred-row tab pill ("AI Apps / Web Apps / Ecommerce / Marketing / Platforms").
- Background `{colors.canvas}`, text `{colors.ink}`, label set in `{typography.body-sm}`, padding `0px {spacing.md}`, shape `{rounded.pill-sm}` 64 px.
**`icon-button-circular`** — the circular icon container (often a "?" or arrow inside).
- Background `{colors.canvas}`, dark icon, 1 px solid hairline border, shape `{rounded.full}`.
**Nav CTAs:**
**`nav-cta-signup`** — the small black "Sign Up" button in the nav row.
- Background `{colors.primary}`, text `{colors.on-primary}`, label `{typography.body-sm-strong}`, padding `0px {spacing.xs}`, height 28 px, shape `{rounded.sm}` 6 px (the brand's `--geist-radius`).
**`nav-cta-login`** — the white "Log In" button in the nav.
- Background `{colors.canvas}`, text `{colors.ink}`, same typography / height / shape as `nav-cta-signup`.
**`nav-cta-ask-ai`** — the small "Ask AI" button with a faint border.
- Background `{colors.canvas}`, text `{colors.ink}`, 1 px solid `{colors.hairline}` border (extracted as `0px solid rgb(235, 235, 235)`), same typography / height / shape.
### Cards & Containers
**`card-marketing`** — the canonical marketing feature card (3-up section cards).
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.lg}` 24 px, shape `{rounded.md}` 8 px (the `--geist-marketing-radius`). Carries Level 3 soft-stack shadow.
**`card-marketing-large`** — the larger marketing card used for "compute model" / "AI Gateway" callouts.
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.xl}`, shape `{rounded.lg}` 12 px. Carries Level 4 float-stack shadow.
**`card-soft`** — the soft-tinted card used inside cluster groups (lighter than canvas-soft).
- Background `{colors.canvas-soft}`, text `{colors.ink}`, padding `{spacing.lg}`, shape `{rounded.md}`.
**`template-card`** — the deploy-template card in the "Deploy your first app" grid.
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.md}` 16 px, shape `{rounded.md}` 8 px. Hosts a 16:9 thumbnail at the top.
**`code-editor-mockup`** — the dark code-preview surface inside marketing bands.
- Background `{colors.primary}`, text `{colors.on-primary}`, body in `{typography.code}` (13 px / Geist Mono), padding `{spacing.lg}` 24 px, shape `{rounded.md}` 8 px.
**`pricing-card`** — the default pricing-tier card.
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.xl}` 32 px, shape `{rounded.lg}` 12 px. Inside: tier name in `{typography.display-md}`, price in `{typography.display-xl}`, feature list in `{typography.body-md}` rows, CTA at the bottom.
**`pricing-card-featured`** — the polarity-flipped "Pro" tier card.
- Background `{colors.primary}`, text `{colors.on-primary}`, same shape + padding as `pricing-card`. CTA inverts to `button-secondary-sm` (white pill on black card).
### Inputs & Forms
**`form-input`** — the canonical text input.
- Background `{colors.canvas}`, text `{colors.ink}`, 1 px solid `{colors.hairline}` border, body in `{typography.body-sm}` (14 px), padding `0px {spacing.sm}`, height 40 px (the brand's `--geist-form-height`), shape `{rounded.sm}` 6 px.
**`form-input-sm`** — small-height variant (32 px tall) for tight forms.
- Same as `form-input` but height 32 px (the `--geist-form-small-height`).
**`form-input-lg`** — large-height variant (48 px tall) for hero CTAs.
- Same as `form-input` but height 48 px (the `--geist-form-large-height`); body in `{typography.body-md}` 16 px.
### Navigation
**`nav-bar`** — the sticky top nav.
- Background `{colors.canvas}`, text `{colors.ink}`, height 64 px (the brand's `--header-height`), padding `{spacing.sm} {spacing.lg}`. Layout: logo left, link row centre, "Ask AI / Log In / Sign Up" cluster right.
**`nav-link`** — the centred link row inside `nav-bar`.
- Text `{colors.body}`, set in `{typography.body-sm}`, padding `{spacing.xs} {spacing.sm}`, shape `{rounded.full}` (ghost pill — visible only on hover or active, but the radius is documented).
**`footer`** — the bottom 4-column nav.
- Background `{colors.canvas}`, text `{colors.body}`, padding `{spacing.4xl} {spacing.lg}`. Eyebrow column labels in `{typography.caption-mono}` (uppercase mono effect); link rows in `{typography.body-sm}`.
### Signature Components
**`hero-band`** — the white hero with the mesh gradient backdrop.
- Background `{colors.canvas}` (or `{colors.canvas-soft}` on some surfaces), text `{colors.ink}`, padding `{spacing.4xl} {spacing.lg}`. Inside: a small mono badge above the headline, the headline in `{typography.display-xl}` (sentence-case, period-terminated), a body lead in `{typography.body-lg}`, then a CTA row with `button-primary` + `button-secondary`. The mesh gradient sits behind, scaled to occupy roughly the top half of the band.
**`feature-mesh-band`** — the secondary section that hosts a mesh-gradient atmospheric backdrop with feature copy on top.
- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.5xl} {spacing.lg}`. Section headline in `{typography.display-lg}`; supporting body in `{typography.body-md}`.
**`showcase-band-light`** — a soft-canvas section ("Deploy your first app in seconds").
- Background `{colors.canvas-soft}`, text `{colors.ink}`, padding `{spacing.5xl} {spacing.lg}`.
**`showcase-band-dark`** — the polarity-flipped dark band ("A compute model for all workloads").
- Background `{colors.primary}`, text `{colors.on-primary}`, padding `{spacing.5xl} {spacing.lg}`. Section headline in `{typography.display-lg}` (white on black). Often contains a `code-editor-mockup` flush with the band.
**`logo-strip`** — the customer-logo wrapping row near the top of the page.
- Background `{colors.canvas}`, text `{colors.body}`, padding `{spacing.lg} {spacing.xl}`. Logos rendered as monochrome SVGs at consistent height.
**`badge-secondary`** — the small inline metadata pill ("New", "Beta", "Live").
- Background `{colors.canvas-soft}`, text `{colors.body}`, body in `{typography.caption}`, padding `0px {spacing.xs}`, shape `{rounded.full}`.
**`banner-marketing`** — the "Introducing X" announcement pill at the top of pages.
- Background `{colors.canvas-soft}`, text `{colors.body}`, body in `{typography.body-sm}`, padding `{spacing.xs} {spacing.sm}`, shape `{rounded.full}`.
**`link-inline`** — body-copy inline links.
- Text `{colors.link}` (`#0070f3`), body in `{typography.body-md}`, underlined.
### Examples (illustrative)
> Auto-derived kit-mirror demonstration surfaces (`scripts/derive-examples-block.mjs`). Each `ex-*` entry references brand-native primitives so downstream consumers (`/preview-design`, `/generate-kit`) re-skin the same 10 surfaces consistently. `TO_FILL` markers indicate missing primitives — resolve in the LLM judgment pass.
**`ex-pricing-tier`** — Default Pricing tier card. Re-uses feature-card chrome with brand canvas-soft surface.
- Properties: `backgroundColor`, `textColor`, `borderColor`, `rounded`, `padding`
**`ex-pricing-tier-featured`** — Featured/highlighted tier — polarity-flipped surface (dark fill + light text in light mode, light fill + dark text in dark mode).
- Properties: `backgroundColor`, `textColor`, `rounded`, `padding`
**`ex-product-selector`** — What's Included summary card — re-purposed for SaaS / B2B verticals (NOT a literal product gallery).
- Properties: `backgroundColor`, `rounded`, `padding`
**`ex-cart-drawer`** — Subscription summary — re-purposed for SaaS / B2B (line items per add-on, not literal cart).
- Properties: `backgroundColor`, `rounded`, `padding`, `item-divider`
**`ex-app-shell-row`** — Sidebar nav row inside the App Shell example. Active state uses brand primary as the indicator.
- Properties: `backgroundColor`, `activeIndicator`, `rounded`, `padding`
**`ex-data-table-cell`** — Default data-table th + td chrome. Header uses mono-caps eyebrow typography; body uses body-sm.
- Properties: `headerBackground`, `headerTypography`, `bodyTypography`, `cellPadding`, `rowBorder`
**`ex-auth-form-card`** — Sign-in / sign-up card. Re-uses feature-card chrome with text-input primitives inside.
- Properties: `backgroundColor`, `rounded`, `padding`
**`ex-modal-card`** — Modal dialog surface — same chrome as feature-card with elevated shadow.
- Properties: `backgroundColor`, `rounded`, `padding`
**`ex-empty-state-card`** — Empty-state illustration frame.
- Properties: `backgroundColor`, `rounded`, `padding`, `captionTypography`
**`ex-toast`** — Toast notification surface — feature-card shape + medium shadow.
- Properties: `backgroundColor`, `rounded`, `padding`, `typography`
## Do's and Don'ts
### Do
- Reserve `{colors.primary}` (`#171717`) for primary CTAs across the page. Black ink IS the conversion target.
- Use `{rounded.pill}` 100 px for every marketing-scale CTA and `{rounded.sm}` 6 px for nav-scale buttons. The two pill scales coexist deliberately.
- Set every headline in `{typography.display-*}` weight 600, sentence-case, often period-terminated. Aggressive negative tracking is part of the voice.
- Use the brand mesh gradient as atmospheric decoration at hero scale only — never miniaturise it to an icon, never reduce to a single colour.
- Layer stacked shadows (multiple small offsets with inset hairline) rather than single heavy drops. The brand's elevation is calmer than Material.
- Cycle page surfaces in `{colors.canvas-soft}``{colors.canvas}``{colors.primary}` polarity-flipped bands; the dark band IS the depth cue.
- Set every code block and technical eyebrow in `{typography.code}` / `{typography.caption-mono}`. Mono is the voice of the platform.
### Don't
- Don't introduce a sixth accent colour. The brand operates with ink + gray + the four-pair gradient palette; new accents flatten the voice.
- Don't render headlines in all-caps. Sentence-case + negative tracking is non-negotiable.
- Don't drop a single heavy drop-shadow on cards. The brand's elevation is built from stacked small offsets + inset hairline rings.
- Don't render the brand gradient at icon scale or in a single-colour reduced form. The gradient lives at hero scale only.
- Don't promote the geometric sans to weight 700. The brand's display ceiling is 600.
- Don't pair the marketing 100-px pill CTA shape with the 6-px nav radius on the same screen — pick a scale and stay there.
- Don't set body paragraphs in the mono face. The mono is for code + technical labels only.

View File

@@ -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`(及各记录 utilsUI 不复用 `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 | 12 |
| Nav + PRD | 3 |
| KPI/anomaly口径 = PC | 1 (shared utils) |

View File

@@ -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` 及关联记录 JSONKPI/异常规则与 PC 同源工具函数(可抽共享或复制后标注同源) |
| 壳层 | PhoneShell 桌面预览;真机/WebView 全屏内容区 |
| 导航 | 注册到原型导航「运维管理」下,与 PC 车辆资产并列区分如「车辆资产H5 |
### 3.2 第一期不做
- 任意字段编辑、运维负责人修改、运营城市修改
- 批量导入 / 导出真实能力(不做入口,或仅隐藏)
- 催办、抄送、跳转证照管理/保险采购等业务页(不做;外链 Toast 占位也不做,避免误导)
- 在租车型占比弹窗PC 尾部 KPIH5 第一期可不做,避免干扰主路径)
- 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正文 400500标签 600 |
| 壳层约定 | **列表**底部为悬浮胶囊 Tab毛玻璃「工作台/任务/消息/我的」Toast 占位;本页「车辆」选中。**详情**不挂底栏,仅顶栏返回 + 车牌 |
| 触控 | ≥ 44×44pxChip/图标区可视觉更紧,点击热区保留);`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 BoutiqueElectric 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 | 需求访谈确认后首版设计规格 |

View File

@@ -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

View File

@@ -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 |
| 区块纵向间距 | **1216** | 搜索→KPI、Chip→列表 |
## 2. 顶栏
| 元素 | 尺寸 | 备注 |
|------|------|------|
| 标题行高 | **40** | 「车辆资产」+ 右侧操作 |
| 标题字号 | **≈2224 / Semibold** | SF Pro Display 观感 |
| 通知铃 / 加号 | **36×36** | 触控建议扩到 44 |
| 右侧图标间距 | **8** | |
## 3. 搜索栏
| 元素 | 尺寸 | 备注 |
|------|------|------|
| 高度 | **44** | 满足触控下限 |
| 宽度 | **358** | 满宽 页边距 |
| 圆角 | **12** | |
| 左右内边距 | **12** | 左放大镜 / 右扫码 |
| 占位字号 | **1517** | |
## 4. KPI 横滑卡片
| 元素 | 尺寸 | 备注 |
|------|------|------|
| 卡片宽 × 高 | **90 × 100** | 约露出 4 张 |
| 卡片间距 | **8** | |
| 圆角 | **1216** | |
| 主数字 | **≈2022 / Bold** | tabular-nums |
| 分类标题 | **12** | |
| 底部分页点 | **Ø6 / 间距 6** | |
## 5. 筛选 Chip 行
| 元素 | 尺寸 | 备注 |
|------|------|------|
| Chip 高 | **32** | 若偏难点可提到 36 |
| Chip 宽 | **≈7890** | 随文案 |
| Chip 间距 | **8** | |
| 圆角 | **8** | |
| 字号 | **1314** | 选中态浅蓝底 |
## 6. 车辆列表卡片
| 元素 | 尺寸 | 备注 |
|------|------|------|
| 卡片宽 × 高 | **358 × 118** | |
| 卡片圆角 | **16** | |
| 卡片垂直间距 | **12** | |
| 卡片内边距 | **12** | 四边一致 |
| 车辆缩略图 | **100 × 74** | 圆角 ≈8 |
| 图文间距 | **12** | 缩略图 ↔ 文案 |
| 车牌字号 | **≈1718 / Semibold** | |
| 车型副文 | **1314** | 次要灰 |
| Tag 高 | **≈22** | 圆角 6 |
| Tag 间距 | **68** | |
| 右侧状态区 | **≈72 宽** | 状态点 + 里程 + 异常标 |
| 右箭头 | **1216** | |
## 7. 底栏
| 元素 | 尺寸 | 备注 |
|------|------|------|
| 底栏总高 | **≈83** | 含 Home Indicator ≈34 |
| 可点区域高 | **≈49** | |
| Tab 等分 | **5** | |
| 图标 | **24×24** | |
| 文案 | **10** | |
| 中心「工作台」 | **⌀56** | 略浮起 |
| 消息红点 | **≈1618** | |
## 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。

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

9
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.11", "version": "0.1.11",
"dependencies": { "dependencies": {
"@axhub/annotation": "^1.0.10", "@axhub/annotation": "^1.0.10",
"@axhub/commentary-react": "^1.0.0",
"docx-preview": "^0.3.7", "docx-preview": "^0.3.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
@@ -535,6 +536,14 @@
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
} }
}, },
"node_modules/@axhub/commentary-react": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@axhub/commentary-react/-/commentary-react-1.0.0.tgz",
"integrity": "sha512-2wyrXZxowCr48fo+lLRImd2Vzzvt+IdAT+xXRwq8BIoat0HQXi6vl+MTXvKWQjMS54UVfrGkLdFgAQeEMB7YXQ==",
"peerDependencies": {
"react": "^18.2.0"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.7", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",

View File

@@ -50,6 +50,7 @@
}, },
"dependencies": { "dependencies": {
"@axhub/annotation": "^1.0.10", "@axhub/annotation": "^1.0.10",
"@axhub/commentary-react": "^1.0.0",
"docx-preview": "^0.3.7", "docx-preview": "^0.3.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",

View File

@@ -12,6 +12,12 @@
"sourceType": "github", "sourceType": "github",
"skillPath": "skills/engineering/to-prd/SKILL.md", "skillPath": "skills/engineering/to-prd/SKILL.md",
"computedHash": "5a733e19d825f22e83de52e68b8840c9fd2fa7501282277a122c1aa28a922059" "computedHash": "5a733e19d825f22e83de52e68b8840c9fd2fa7501282277a122c1aa28a922059"
},
"user-story-mapping": {
"source": "deanpeters/product-manager-skills",
"sourceType": "github",
"skillPath": "skills/user-story-mapping/SKILL.md",
"computedHash": "e4ffdd47016435d03ba1bc3bcfae5e13ac3bd7a7e81569a026c11066f5c6c174"
} }
} }
} }

View File

@@ -47,6 +47,7 @@ const MORE_ACTION_ICONS: Record<string, LucideIcon> = {
manage: Settings2, manage: Settings2,
preview: Eye, preview: Eye,
download: Download, download: Download,
failDetail: FileText,
}; };
function renderMenuItemLabel(item: OperationActionItem) { function renderMenuItemLabel(item: OperationActionItem) {
@@ -95,7 +96,7 @@ function ViewIcon() {
} }
function MoreIcon() { function MoreIcon() {
return <MoreHorizontal className="vm-op-action__icon" aria-hidden />; return <MoreHorizontal className="vm-op-more-btn__icon" aria-hidden strokeWidth={1.75} />;
} }
const PrimaryActionButton = React.forwardRef< const PrimaryActionButton = React.forwardRef<
@@ -121,18 +122,22 @@ const PrimaryActionButton = React.forwardRef<
}, },
ref, ref,
) { ) {
const isMore = icon === 'more';
return ( return (
<button <button
ref={ref} ref={ref}
type="button" type="button"
className={['vm-op-action', className].filter(Boolean).join(' ')} className={[
isMore ? 'vm-op-more-btn' : 'vm-op-action',
className,
].filter(Boolean).join(' ')}
aria-label={ariaLabel} aria-label={ariaLabel}
aria-haspopup={ariaHasPopup ? 'menu' : undefined} aria-haspopup={ariaHasPopup ? 'menu' : undefined}
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
> >
{icon === 'view' ? <ViewIcon /> : <MoreIcon />} {isMore ? <MoreIcon /> : <ViewIcon />}
<span className="vm-op-action__label">{label}</span> {isMore ? null : <span className="vm-op-action__label">{label}</span>}
</button> </button>
); );
}); });
@@ -236,9 +241,9 @@ export function OperationActions({
<PrimaryActionButton <PrimaryActionButton
icon="more" icon="more"
label={moreLabel} label={moreLabel}
ariaLabel={moreLabel + '操作'} ariaLabel="更多操作"
ariaHasPopup ariaHasPopup
className={moreOpen ? 'vm-op-action--open' : undefined} className={moreOpen ? 'vm-op-more-btn--open' : undefined}
/> />
</Dropdown> </Dropdown>
) : null} ) : null}

View File

@@ -11,6 +11,41 @@ var H2_VERIFY_UNVERIFIED = 'unverified';
var H2_RECORD_SOURCE_STATION = 'station'; var H2_RECORD_SOURCE_STATION = 'station';
var H2_RECORD_SOURCE_LINGNIU = 'lingniu'; var H2_RECORD_SOURCE_LINGNIU = 'lingniu';
/**
* 原型本地「系统车辆列表」车牌(不含尾缀 F。命中 → 羚牛车辆,否则 → 非羚牛车辆。
* 正式环境应改为车辆管理接口;未接真实 API。
*/
var H2_FLEET_PLATE_KEYS = [
'浙A55666', '浙A77888', '浙A99001', '浙A12345', '浙A67890', '浙A88888', '浙A03561',
'浙B23456', '浙B99999', '浙B58888',
'沪A88888', '沪BDB9161', '沪ADB9161',
'苏E33333',
'浙A88H201', '浙F00688', '浙F07588', '浙F06618',
'粤AGR8556', '粤AGP5156',
/* 车辆氢费明细演示车牌 */
'浙AD12345', '浙AH55660', '浙BK33210', '粤BK33210', '沪AD12345', '苏EF99887', '京CN88771', '川AL55602'
];
var H2_FLEET_PLATE_SET = (function () {
var set = {};
H2_FLEET_PLATE_KEYS.forEach(function (p) {
set[String(p).toUpperCase()] = true;
});
return set;
})();
/** 车牌规范化键:去尾缀 F、大写、去空白 */
function h2BridgeNormalizePlateKey(plateNo) {
return String(plateNo || '').trim().toUpperCase().replace(/F$/u, '');
}
/** 是否羚牛车辆(系统车辆列表命中) */
function h2BridgeIsLingniuVehicle(plateNo) {
var key = h2BridgeNormalizePlateKey(plateNo);
if (!key) return false;
return Boolean(H2_FLEET_PLATE_SET[key]);
}
var H2_STATION_CODE_MAP = { var H2_STATION_CODE_MAP = {
'中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001', '中国石油中油高新能源牙谷加油加氢站': 'JX-H2-001',
'杭州临平加氢站': 'HZ-H2-002', '杭州临平加氢站': 'HZ-H2-002',
@@ -41,7 +76,9 @@ var H2_CANONICAL_LEDGER_SEED = [
{ key: 'rf-7', id: 'rf-7', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costTotal: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer', mileageKm: 110340, creatorName: '李四', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-05-25 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-05-25 10:00:00' }, { key: 'rf-7', id: 'rf-7', stationId: 'HZ-H2-002', stationName: '杭州临平加氢站', hydrogenTime: '2026-05-24 11:05:40', plateNo: '浙B58888F', customerName: '杭州临平城配中心', hydrogenKg: 11.8, costUnitPrice: 43.0, costTotal: 507.4, customerUnitPrice: 46, customerAmount: 542.8, settlementStatus: 'customer', mileageKm: 110340, creatorName: '李四', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-05-25 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-05-25 10:00:00' },
{ key: 'rf-8', id: 'rf-8', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costTotal: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal', mileageKm: 88420, creatorName: '王静', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-21 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-04-21 10:00:00' }, { key: 'rf-8', id: 'rf-8', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-20 16:45:18', plateNo: '沪A88888F', customerName: '上海羚牛氢运', hydrogenKg: 8.0, costUnitPrice: 44.0, costTotal: 352.0, customerUnitPrice: 47, customerAmount: 376.0, settlementStatus: 'internal', mileageKm: 88420, creatorName: '王静', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-21 10:00:00', reconcileStatus: H2_RECONCILE_RECONCILED, reconcileDate: '2026-04-21 10:00:00' },
{ key: 'rf-9', id: 'rf-9', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costTotal: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self', mileageKm: 76500, creatorName: '张三', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-09 10:00:00', reconcileStatus: H2_RECONCILE_PENDING, reconcileDate: null }, { key: 'rf-9', id: 'rf-9', stationId: 'SH-H2-003', stationName: '上海宝山加氢站', hydrogenTime: '2026-04-08 09:12:55', plateNo: '沪BDB9161F', customerName: '宝山园区试运车队', hydrogenKg: 9.5, costUnitPrice: 44.0, costTotal: 418.0, customerUnitPrice: 47, customerAmount: 446.5, settlementStatus: 'customer_self', mileageKm: 76500, creatorName: '张三', verifyStatus: H2_VERIFY_VERIFIED, verifiedAt: '2026-04-09 10:00:00', reconcileStatus: H2_RECONCILE_PENDING, reconcileDate: null },
{ key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING } { key: 'rf-10', id: 'rf-10', stationId: 'SZ-H2-004', stationName: '苏州工业园区备用站', hydrogenTime: '2026-03-15 10:00:00', plateNo: '苏E33333F', customerName: '苏州试运客户', hydrogenKg: 6.2, costUnitPrice: 41.0, costTotal: 254.2, customerUnitPrice: 44, customerAmount: 272.8, settlementStatus: 'customer', mileageKm: 45210, creatorName: '赵敏', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING },
/* 非羚牛车辆演示:车牌不在系统车辆列表 → 列表核对/对账字段应显示为空 */
{ key: 'rf-ext-1', id: 'rf-ext-1', stationId: 'JX-H2-001', stationName: '中国石油中油高新能源牙谷加油加氢站', hydrogenTime: '2026-07-13 14:22:00', plateNo: '浙C77801F', customerName: '中国石油中油高新能源牙谷加油加氢站·站端上报', hydrogenKg: 8.5, costUnitPrice: 42.5, costTotal: 361.25, customerUnitPrice: 42.5, customerAmount: 361.25, settlementStatus: 'customer', mileageKm: 88000, creatorName: '站端账号', verifyStatus: H2_VERIFY_UNVERIFIED, verifiedAt: null, reconcileStatus: H2_RECONCILE_PENDING }
]; ];
function h2BridgeFormatDateTime(value) { function h2BridgeFormatDateTime(value) {
@@ -263,26 +300,28 @@ function h2BridgeResolveStationId(stationId, stationName) {
return a || ''; return a || '';
} }
/** 手工台账种子:日已上传;日默认未上传,便于演示拦截新增 */ /** 手工台账种子:日已上传;日默认未上传,便于演示「前一日缺失则今日禁新增 */
function h2BridgeBuildInitialManualLedgers() { function h2BridgeBuildInitialManualLedgers() {
var yesterday = h2BridgeYesterdayDateKey(); var d = new Date();
d.setDate(d.getDate() - 2);
var dayBeforeYesterday = h2BridgeTodayDateKey(d);
return [ return [
{ {
id: 'ml-seed-yesterday', id: 'ml-seed-day-before-yesterday',
stationId: 'JX-H2-001', stationId: 'JX-H2-001',
stationName: '中国石油中油高新能源牙谷加油加氢站', stationName: '中国石油中油高新能源牙谷加油加氢站',
ledgerDate: yesterday, ledgerDate: dayBeforeYesterday,
images: [ images: [
{ {
id: 'ml-img-seed-1', id: 'ml-img-seed-1',
name: '日手工台账.jpg', name: '日手工台账.jpg',
dataUrl: '', dataUrl: '',
placeholder: true, placeholder: true,
archived: true, archived: true,
uploadedAt: yesterday + ' 18:30:00' uploadedAt: dayBeforeYesterday + ' 18:30:00'
} }
], ],
updatedAt: yesterday + ' 18:30:00' updatedAt: dayBeforeYesterday + ' 18:30:00'
} }
]; ];
} }
@@ -507,6 +546,9 @@ function h2BridgeMapToStationLedgerRow(row, index) {
costTotal: costTotal, costTotal: costTotal,
settlementStatus: row.settlementStatus, settlementStatus: row.settlementStatus,
orderNo: h2BridgeBuildRefuelOrderNo(row, index), orderNo: h2BridgeBuildRefuelOrderNo(row, index),
/* 对账单筛选依赖:须透出核对状态,否则站点侧会把全部行当成未核对 */
verifyStatus: row.verifyStatus === H2_VERIFY_VERIFIED ? H2_VERIFY_VERIFIED : H2_VERIFY_UNVERIFIED,
verifiedAt: row.verifiedAt || null,
reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING), reconcileStatus: row.reconcileStatus || (isReconciled ? H2_RECONCILE_RECONCILED : H2_RECONCILE_PENDING),
statementRecordId: row.statementRecordId || null, statementRecordId: row.statementRecordId || null,
reconcileDate: row.reconcileDate || null, reconcileDate: row.reconcileDate || null,
@@ -732,6 +774,7 @@ function h2BridgeInit() {
totalAmountMismatch: h2BridgeTotalAmountMismatch, totalAmountMismatch: h2BridgeTotalAmountMismatch,
lookupStationUnitPrice: h2BridgeLookupStationUnitPrice, lookupStationUnitPrice: h2BridgeLookupStationUnitPrice,
todayDateKey: h2BridgeTodayDateKey, todayDateKey: h2BridgeTodayDateKey,
yesterdayDateKey: h2BridgeYesterdayDateKey,
getManualLedger: function (stationIdOrName, ledgerDate) { getManualLedger: function (stationIdOrName, ledgerDate) {
return store.getManualLedger(stationIdOrName, ledgerDate); return store.getManualLedger(stationIdOrName, ledgerDate);
}, },
@@ -744,9 +787,30 @@ function h2BridgeInit() {
upsertManualLedger: function (input) { upsertManualLedger: function (input) {
return store.upsertManualLedger(input); return store.upsertManualLedger(input);
}, },
/** 前一日台账未上传(且昨日≥本站首笔加氢日)→ 今日禁新增 */
isManualLedgerGateBlocked: function (stationIdOrName) {
var station = String(stationIdOrName || '').trim();
if (!station) return false;
var todayKey = h2BridgeTodayDateKey();
var yesterdayKey = h2BridgeYesterdayDateKey();
var rows = store.getRows() || [];
var earliest = null;
rows.forEach(function (row) {
var sid = String(row.stationId || '').trim();
var sn = String(row.stationName || '').trim();
if (sid !== station && sn !== station) return;
var m = String(row.hydrogenTime || '').match(/^(\d{4}-\d{2}-\d{2})/);
if (!m) return;
if (!earliest || m[1] < earliest) earliest = m[1];
});
if (!earliest || yesterdayKey > todayKey || yesterdayKey < earliest) return false;
return !store.hasManualLedgerForDate(station, yesterdayKey);
},
upsertRow: function (row) { return store.upsertRow(row); }, upsertRow: function (row) { return store.upsertRow(row); },
removeRow: function (id) { return store.removeRow(id); }, removeRow: function (id) { return store.removeRow(id); },
subscribe: function (fn) { return store.subscribe(fn); } subscribe: function (fn) { return store.subscribe(fn); },
isLingniuVehicle: h2BridgeIsLingniuVehicle,
normalizePlateKey: h2BridgeNormalizePlateKey
}; };
} }
return store; return store;
@@ -776,5 +840,7 @@ export {
h2BridgeFindDuplicateRow, h2BridgeFindDuplicateRow,
h2BridgeTotalAmountMismatch, h2BridgeTotalAmountMismatch,
h2BridgeLookupStationUnitPrice, h2BridgeLookupStationUnitPrice,
h2BridgeInferRecordSource h2BridgeInferRecordSource,
h2BridgeIsLingniuVehicle,
h2BridgeNormalizePlateKey
}; };

View File

@@ -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<typeof buildShellMenuFromNav>[0]),
[],
);
const path = useMemo(() => findMenuPath(menu, activeHref) || [], [menu, activeHref]);
const [collapsed, setCollapsed] = useState(false);
const [openKeys, setOpenKeys] = useState<string[]>(() => collectOpenKeys(path));
const [theme, setTheme] = useState<OneOsTheme>(() => readStoredOneOsTheme());
const [shellNotices, setShellNotices] = useState<ShellNoticeItem[]>([]);
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<HTMLIFrameElement>('.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 (
<li key={item.key} className={`oneos-shell-menu__item oneos-shell-menu__item--lv${level}`}>
<button
type="button"
className={`oneos-shell-menu__btn${selected ? ' is-active' : ''}${hasChildren ? ' is-parent' : ''}`}
title={item.label}
aria-expanded={hasChildren ? opened : undefined}
onClick={() => {
if (hasChildren) toggleOpen(item.key);
else handleNavigate(item);
}}
>
<Icon className="oneos-shell-menu__icon" size={16} strokeWidth={1.75} aria-hidden />
{!collapsed && <span className="oneos-shell-menu__label">{item.label}</span>}
{!collapsed && hasChildren && (
<span className="oneos-shell-menu__arrow" aria-hidden>
{opened ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</span>
)}
</button>
{!collapsed && hasChildren && opened && (
<ul className="oneos-shell-menu__sub">{renderItems(item.children!, level + 1)}</ul>
)}
</li>
);
});
return (
<div
className={`oneos-shell${collapsed ? ' oneos-shell--collapsed' : ''}`}
data-oneos-theme={theme}
>
<aside className="oneos-shell-aside" aria-label="羚牛 OneOS 导航">
<div className="oneos-shell-brand">
<a className="oneos-shell-brand__link" href={brandHref} title="羚牛OneOS">
<span className="oneos-shell-brand__mark" aria-hidden>
<svg viewBox="0 0 32 32" width="28" height="28">
<defs>
<linearGradient id="oneosBrandGrad" x1="0" y1="0" x2="32" y2="32">
<stop offset="0%" stopColor="#3FB87C" />
<stop offset="100%" stopColor="#2B9260" />
</linearGradient>
</defs>
<rect width="32" height="32" rx="9" fill="url(#oneosBrandGrad)" />
<path
d="M8 20.5L12.2 9h3.1l4.2 11.5h-3.1l-.8-2.3h-4.7l-.8 2.3H8zm4.2-4.7h3.2L14 11.2h-.1L12.2 15.8zM21 9h2.8v11.5H21V9z"
fill="#fff"
/>
</svg>
</span>
{!collapsed && <span className="oneos-shell-brand__text">OneOS</span>}
</a>
</div>
<nav className="oneos-shell-nav">
<ul className="oneos-shell-menu">{renderItems(menu)}</ul>
</nav>
</aside>
<div className="oneos-shell-main">
<div className="oneos-shell-chrome">
<header className="oneos-shell-header">
<div className="oneos-shell-header__left">
<button
type="button"
className="oneos-shell-icon-btn"
aria-label={collapsed ? '展开侧栏' : '收起侧栏'}
onClick={() => setCollapsed((v) => !v)}
>
{collapsed ? <Menu size={18} /> : <PanelLeftClose size={18} />}
</button>
<button
type="button"
className="oneos-shell-icon-btn"
aria-label="刷新"
onClick={() => (onRefresh ? onRefresh() : window.location.reload())}
>
<RefreshCw size={16} />
</button>
<nav className="oneos-shell-breadcrumb" aria-label="面包屑">
<ol>
{crumbs.map((c, i) => (
<li key={`${c}-${i}`}>
{i > 0 && <span className="oneos-shell-breadcrumb__sep">/</span>}
<span className={i === crumbs.length - 1 ? 'is-current' : undefined}>{c}</span>
</li>
))}
</ol>
</nav>
</div>
<div className="oneos-shell-header__right">
<label className="oneos-shell-search">
<Search size={14} aria-hidden />
<input type="search" placeholder="搜索" aria-label="搜索" />
<kbd> K</kbd>
</label>
{onOpenReleaseNotes ? (
<button
type="button"
className={`oneos-shell-release-btn${releaseNotesUnread ? ' has-unread' : ''}`}
onClick={onOpenReleaseNotes}
aria-label="版本更新"
title="版本更新日志"
data-annotation-id="shell-release-notes"
>
<span className="oneos-shell-release-btn__ring" aria-hidden />
<span className="oneos-shell-release-btn__icon" aria-hidden>
<Sparkles size={16} strokeWidth={2.25} />
</span>
{releaseNotesUnread ? (
<span className="oneos-shell-release-btn__dot" aria-hidden />
) : null}
</button>
) : null}
<button
type="button"
className="oneos-shell-icon-btn"
aria-label={theme === 'dark' ? '切换浅色模式' : '切换暗色模式'}
title={theme === 'dark' ? '浅色模式' : '暗色模式'}
onClick={toggleTheme}
>
{theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
</button>
<button type="button" className="oneos-shell-icon-btn" aria-label="全屏" title="全屏(演示)">
<Maximize size={16} />
</button>
<ShellNoticeCenter
notices={shellNotices}
unreadCount={shellUnread}
onRead={onNoticeRead}
onOpen={onNoticeOpen}
onHandle={onNoticeHandle}
/>
<button type="button" className="oneos-shell-avatar" aria-label="超级管理员">
<img src={AVATAR_URL} alt="" width={28} height={28} />
</button>
</div>
</header>
{showTabs && tabs!.length > 0 && (
<div className="oneos-shell-tabs" role="tablist" aria-label="打开的页签">
{tabs!.map((tab) => {
const active = hrefPathname(tab.href) === hrefPathname(activeHref);
return (
<div
key={tab.href}
className={`oneos-shell-tab${active ? ' is-active' : ''}`}
role="tab"
aria-selected={active}
>
<button
type="button"
className="oneos-shell-tab__label"
onClick={() => onTabSelect?.(tab.href)}
>
{tab.title}
</button>
{onTabClose && (
<button
type="button"
className="oneos-shell-tab__close"
aria-label={`关闭 ${tab.title}`}
onClick={(e) => {
e.stopPropagation();
onTabClose(tab.href);
}}
>
<X size={12} />
</button>
)}
</div>
);
})}
</div>
)}
</div>
<div className="oneos-shell-content">{children}</div>
</div>
</div>
);
}
export default OneOsAppShell;

View File

@@ -0,0 +1,12 @@
# 羚牛 OneOS 应用外壳
> 实现:`src/common/oneos-app-shell/`
> 宿主页:`/prototypes/oneos-prototype-demo`(原型演示)
## 用法
- **带外壳浏览所有原型**:打开「原型演示」,从左侧菜单点选;内容区 iframe 嵌入。
- **单独打开某原型**:访问 `/prototypes/<id>`,无侧栏顶栏。
- **浅色 / 暗色**:顶栏月亮/太阳按钮切换;暗色参照原型导航页风格。对 OneOS 目录下原型生效(含直链),**不包括「旧 ONEOS」**。
菜单数据:`oneos-prototype-nav/nav-menu.json`

View File

@@ -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 (
<li className={`oneos-shell-notify__item${!notice.read ? ' is-unread' : ''}`}>
<button type="button" className="oneos-shell-notify__item-main" onClick={() => onSelect(notice)}>
<div className="oneos-shell-notify__meta">
<span className="oneos-shell-notify__dot" aria-hidden />
<span className="oneos-shell-notify__time">{notice.time}</span>
{notice.type === '催办提醒' ? (
<span className="oneos-shell-notify__chip oneos-shell-notify__chip--urge"></span>
) : null}
<span className="oneos-shell-notify__chip">{notice.bizTag}</span>
</div>
<p className="oneos-shell-notify__text">{notice.detail || notice.title}</p>
{showSummary && notice.summary ? (
<p className="oneos-shell-notify__summary">{notice.summary}</p>
) : null}
</button>
{showHandle && canHandle ? (
<button
type="button"
className="oneos-shell-notify__handle"
onClick={() => onHandle(notice)}
>
</button>
) : null}
</li>
);
}
/** 顶栏铃铛 + 新消息闪烁提示 + 下拉通知中心(对齐工作台 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<AllTab>('unread');
const [detailNotice, setDetailNotice] = useState<ShellNoticeItem | null>(null);
const wrapRef = useRef<HTMLDivElement>(null);
const allModalRef = useRef<HTMLDivElement>(null);
const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(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 (
<div className="oneos-shell-notify" ref={wrapRef} data-annotation-id="wb-notice">
{unreadCount > 0 && !open && !detailNotice && !allOpen ? (
<span className="oneos-shell-notify__callout" aria-live="polite">
{formatNewMessageTip(unreadCount)}
</span>
) : null}
<button
type="button"
className={`oneos-shell-icon-btn oneos-shell-icon-btn--notify${unreadCount > 0 ? ' has-unread' : ''}${open || detailNotice || allOpen ? ' is-open' : ''}`}
aria-label={unreadCount > 0 ? formatNewMessageTip(unreadCount) : '通知'}
aria-expanded={open || !!detailNotice || allOpen}
aria-haspopup="dialog"
onClick={() => {
if (detailNotice || allOpen) {
setDetailNotice(null);
setAllOpen(false);
setOpen(false);
return;
}
setOpen((v) => !v);
}}
>
<Bell size={16} />
</button>
{open && !detailNotice && !allOpen ? (
<div className="oneos-shell-notify__panel" role="dialog" aria-label="通知中心">
<div className="oneos-shell-notify__header">
<h3 className="oneos-shell-notify__title">
{unreadCount > 0 ? (
<span className="oneos-shell-notify__tag">{formatNewMessageTip(unreadCount)}</span>
) : null}
</h3>
<button
type="button"
className="oneos-shell-notify__link"
onClick={() => {
setAllTab(unreadCount > 0 ? 'unread' : 'read');
setOpen(false);
setAllOpen(true);
}}
>
</button>
</div>
<div className="oneos-shell-notify__body">
{pool.length ? (
<ul className="oneos-shell-notify__list">
{pool.map((notice) => (
<NoticeListItem
key={notice.id}
notice={notice}
showHandle
onSelect={handleSelect}
onHandle={handleAction}
/>
))}
</ul>
) : (
<div className="oneos-shell-notify__empty" role="status">
<div className="oneos-shell-notify__empty-visual" aria-hidden="true">
<span className="oneos-shell-notify__empty-ring" />
<span className="oneos-shell-notify__empty-icon">
<BellOff size={22} strokeWidth={1.75} />
</span>
</div>
<p className="oneos-shell-notify__empty-title"></p>
<p className="oneos-shell-notify__empty-desc">
<br />
</p>
</div>
)}
</div>
</div>
) : null}
{allOpen && portalRoot
? createPortal(
<div className="oneos-shell-notify__all-overlay" role="presentation">
<div
className="oneos-shell-notify__all-modal"
role="dialog"
aria-modal="true"
aria-label="全部通知"
ref={allModalRef}
>
<div className="oneos-shell-notify__all-head">
<h3 className="oneos-shell-notify__all-title"></h3>
<button
type="button"
className="oneos-shell-notify__all-close"
aria-label="关闭"
onClick={() => setAllOpen(false)}
>
<X size={16} />
</button>
</div>
<div className="oneos-shell-notify__all-tabs" role="tablist" aria-label="通知分类">
<button
type="button"
role="tab"
aria-selected={allTab === 'unread'}
className={`oneos-shell-notify__all-tab${allTab === 'unread' ? ' is-active' : ''}`}
onClick={() => setAllTab('unread')}
>
<span className="oneos-shell-notify__all-count">{unreadList.length}</span>
</button>
<button
type="button"
role="tab"
aria-selected={allTab === 'read'}
className={`oneos-shell-notify__all-tab${allTab === 'read' ? ' is-active' : ''}`}
onClick={() => setAllTab('read')}
>
<span className="oneos-shell-notify__all-count">{readList.length}</span>
</button>
</div>
<div className="oneos-shell-notify__all-body" role="tabpanel">
{allTabList.length ? (
<ul className="oneos-shell-notify__list">
{allTabList.map((notice) => (
<NoticeListItem
key={notice.id}
notice={notice}
showHandle={allTab === 'unread'}
showSummary
onSelect={handleSelect}
onHandle={handleAction}
/>
))}
</ul>
) : (
<div className="oneos-shell-notify__empty" role="status">
<div className="oneos-shell-notify__empty-visual" aria-hidden="true">
<span className="oneos-shell-notify__empty-ring" />
<span className="oneos-shell-notify__empty-icon">
<BellOff size={22} strokeWidth={1.75} />
</span>
</div>
<p className="oneos-shell-notify__empty-title">
{allTab === 'unread' ? '暂无未读通知' : '暂无已读通知'}
</p>
<p className="oneos-shell-notify__empty-desc">
{allTab === 'unread'
? '当前通知均已读完,有新催办或业务提醒时会显示在这里'
: '已读通知会显示在这里'}
</p>
</div>
)}
</div>
</div>
</div>,
portalRoot,
)
: null}
{detailNotice ? (
<div className="oneos-shell-notify__detail" role="dialog" aria-modal="true" aria-label="通知详情">
<div className="oneos-shell-notify__detail-head">
<h3 className="oneos-shell-notify__detail-title">
{detailNotice.type === '催办提醒' ? (
<span className="oneos-shell-notify__chip oneos-shell-notify__chip--urge"></span>
) : null}
<span className="oneos-shell-notify__chip">{detailNotice.bizTag}</span>
<span className="oneos-shell-notify__detail-name">{detailNotice.title}</span>
</h3>
<button
type="button"
className="oneos-shell-notify__detail-close"
onClick={() => setDetailNotice(null)}
>
</button>
</div>
<div className="oneos-shell-notify__detail-body">
<span className="oneos-shell-notify__time">{detailNotice.time}</span>
<p className="oneos-shell-notify__detail-text">{detailNotice.detail || detailNotice.title}</p>
{detailNotice.summary ? (
<p className="oneos-shell-notify__detail-summary">{detailNotice.summary}</p>
) : null}
</div>
<div className="oneos-shell-notify__detail-foot">
<button type="button" className="oneos-shell-notify__detail-secondary" onClick={() => setDetailNotice(null)}>
</button>
{(detailNotice.href || detailNotice.taskId) && (
<button
type="button"
className="oneos-shell-notify__detail-primary"
onClick={() => handleAction(detailNotice)}
>
</button>
)}
</div>
</div>
) : null}
</div>
);
}

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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<NoticesSyncPayload, 'type'>) {
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 }, '*');
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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<string, string>;
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;
}
}

View File

@@ -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);
}
});
}

View File

@@ -15,7 +15,7 @@ export interface ApprovalCardProps {
function statusClassName(status: ApprovalCardItem['status']): string { function statusClassName(status: ApprovalCardItem['status']): string {
if (status === 'approved') return 'ap-status ap-status--approved'; if (status === 'approved') return 'ap-status ap-status--approved';
if (status === 'rejected') return 'ap-status ap-status--rejected'; if (status === 'rejected' || status === 'terminated') return 'ap-status ap-status--rejected';
return 'ap-status ap-status--pending'; return 'ap-status ap-status--pending';
} }

View File

@@ -32,7 +32,9 @@ export interface ApprovalDetailPlaceholderProps {
function statusBadgeClass(status: ApprovalCardItem['status']): string { function statusBadgeClass(status: ApprovalCardItem['status']): string {
if (status === 'approved') return 'ap-detail__badge ap-detail__badge--approved'; if (status === 'approved') return 'ap-detail__badge ap-detail__badge--approved';
if (status === 'rejected') return 'ap-detail__badge ap-detail__badge--rejected'; if (status === 'rejected' || status === 'terminated') {
return 'ap-detail__badge ap-detail__badge--rejected';
}
return 'ap-detail__badge ap-detail__badge--pending'; return 'ap-detail__badge ap-detail__badge--pending';
} }

View File

@@ -43,7 +43,8 @@ const STATUS_LABEL_MAP: Record<ApprovalStatus, string> = {
pending: '审批中', pending: '审批中',
processing: '审批中', processing: '审批中',
approved: '已通过', approved: '已通过',
rejected: '驳回', rejected: '驳回',
terminated: '被终止',
}; };
export function getApprovalTypeLabel(type: ApprovalTypeKey): string { export function getApprovalTypeLabel(type: ApprovalTypeKey): string {

View File

@@ -1,4 +1,4 @@
export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected'; export type ApprovalStatus = 'pending' | 'processing' | 'approved' | 'rejected' | 'terminated';
export type ApprovalTabKey = 'todo' | 'done' | 'initiated' | 'cc'; export type ApprovalTabKey = 'todo' | 'done' | 'initiated' | 'cc';

View File

@@ -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);
};
}

View File

@@ -84,6 +84,12 @@
box-shadow: 0 0 0 2px color-mix(in srgb, var(--ln-primary, #32a06e) 35%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ln-primary, #32a06e) 35%, transparent);
} }
.vm-op-more-btn--open,
.vm-op-more-btn[aria-expanded="true"] {
color: var(--ln-ink, #0f172a);
background: var(--ln-canvas-soft, #f4f4f5);
}
.vm-op-more-btn__icon { .vm-op-more-btn__icon {
width: 18px; width: 18px;
height: 18px; height: 18px;

View File

@@ -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

View File

@@ -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` | 渲染「业务名 ← 来源」 |

View File

@@ -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": "主表 · 业务条线数据来源"
}
]
}
]
}
}

View File

@@ -0,0 +1,376 @@
# 保险采购 · 产品需求说明(全模块)
| 项 | 内容 |
|---|---|
| 模块名称 | 保险采购 |
| 所属系统 | ONE-OSPC |
| 交互原型 | `/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 承载**保单台账**与**比价采购**两条独立业务线:运维部、采购部可一车一档维护交强 / 商业等五类险种并办理停保复驶退保,车辆管理与交车据此校验核心险种有效性;采购部可在比价单中完成多方报价、付费预警与采购审批跟踪,审批通过后须人工补录正式保单——台账与比价互不同步,审批不在本页办理。

View File

@@ -43,7 +43,10 @@ const moment = dayjs;
const ANCHOR_TODAY = '2026-06-01'; const ANCHOR_TODAY = '2026-06-01';
const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1'; const IPC_STORAGE_KEY = 'oneos_ipc_insurance_v1';
const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1'; const IPC_COMPARE_SHEETS_KEY = 'oneos_ipc_compare_sheets_v1';
const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v2'; const IPC_POLICY_RECOGN_TASKS_KEY = 'oneos_ipc_policy_recogn_tasks_v3';
const POLICY_RECOGN_FAIL_REASON_BLUR = 'OCR 识别失败,请检查文件清晰度或重新上传';
const POLICY_RECOGN_FAIL_REASON_FORMAT = '文件格式或版式无法解析,请上传清晰 PDF / 图片保单';
const POLICY_RECOGN_FAIL_REASON_CORRUPT = '文件损坏或内容为空,请重新导出后上传';
const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1'; const IPC_INSURANCE_HISTORY_EDITS_KEY = 'oneos_ipc_insurance_history_edits_v1';
const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate'; const IPC_EDIT_PLATE_KEY = 'oneos_ipc_edit_plate';
const NO_PLATE_LABEL = '暂无车牌'; const NO_PLATE_LABEL = '暂无车牌';
@@ -1084,17 +1087,35 @@ const buildMockOcrResults = (files, mode, insuranceTypeLabel, allInsurance) => (
mode mode
); );
if (files.length >= 2 && idx === files.length - 1) { if (files.length >= 2 && idx === files.length - 1) {
return { return markRecognResultFailed(result, POLICY_RECOGN_FAIL_REASON_BLUR);
...result,
recognSuccess: false,
matched: false,
matchTip: 'OCR 识别失败,请检查文件清晰度或重新上传',
};
} }
return result; return result;
}) })
); );
/** 识别失败原因:优先 failReason回退 matchTip */
const getPolicyRecognFailReason = (result) => (
String(result?.failReason || result?.matchTip || '').trim() || '识别失败,原因未知'
);
const getPolicyRecognFailResults = (results) => (
(results || [])
.filter((r) => r.recognSuccess === false)
.map((r) => ({
id: r.id || r.fileUid || r.fileName,
fileName: r.fileName || '未命名文件',
failReason: getPolicyRecognFailReason(r),
}))
);
const markRecognResultFailed = (result, reason) => ({
...result,
recognSuccess: false,
matched: false,
matchTip: reason,
failReason: reason,
});
const recognResultToPolicyDetail = (result, taskMode = 'policy') => { const recognResultToPolicyDetail = (result, taskMode = 'policy') => {
const mode = resolvePolicyRecognEffectiveMode(taskMode, result); const mode = resolvePolicyRecognEffectiveMode(taskMode, result);
const pd = result.policyDetail || {}; const pd = result.policyDetail || {};
@@ -2561,6 +2582,13 @@ const createMockPolicyRecognTasks = () => {
]; ];
const resultsCompleted1 = buildMockOcrResults(filesCompleted1, 'policy', '交强险', insMap); const resultsCompleted1 = buildMockOcrResults(filesCompleted1, 'policy', '交强险', insMap);
if (resultsCompleted1[0]) resultsCompleted1[0].confirmed = true; if (resultsCompleted1[0]) resultsCompleted1[0].confirmed = true;
// 末条失败清晰度问题buildMockOcrResults 已标记);再给另一张不同失败原因
if (resultsCompleted1[1]) {
resultsCompleted1[1] = markRecognResultFailed(
resultsCompleted1[1],
POLICY_RECOGN_FAIL_REASON_FORMAT,
);
}
const filesCompleted2 = [ const filesCompleted2 = [
{ uid: 'demo-c4', name: '粤B88888_复驶批单.pdf', status: 'done' }, { uid: 'demo-c4', name: '粤B88888_复驶批单.pdf', status: 'done' },
@@ -2572,6 +2600,14 @@ const createMockPolicyRecognTasks = () => {
resultsCompleted2.forEach((r) => { resultsCompleted2.forEach((r) => {
if (r.recognSuccess !== false) r.confirmed = true; if (r.recognSuccess !== false) r.confirmed = true;
}); });
// 末条已是清晰度失败;确保 failReason 字段齐全
const lastResumeFail = resultsCompleted2[resultsCompleted2.length - 1];
if (lastResumeFail && lastResumeFail.recognSuccess === false) {
resultsCompleted2[resultsCompleted2.length - 1] = markRecognResultFailed(
lastResumeFail,
POLICY_RECOGN_FAIL_REASON_BLUR,
);
}
const filesSuspend = [ const filesSuspend = [
{ uid: 'demo-s1', name: '沪A06192F_商业险_停保批单.pdf', status: 'done' }, { uid: 'demo-s1', name: '沪A06192F_商业险_停保批单.pdf', status: 'done' },
@@ -2579,6 +2615,18 @@ const createMockPolicyRecognTasks = () => {
]; ];
const resultsSuspend = buildMockOcrResults(filesSuspend, 'suspend', '', insMap); const resultsSuspend = buildMockOcrResults(filesSuspend, 'suspend', '', insMap);
// 全部失败任务:便于演示「无成功仍可看失败明细」
const filesFailOnly = [
{ uid: 'demo-f1', name: '损坏文件_保单.pdf', status: 'done' },
{ uid: 'demo-f2', name: '非保单截图.jpg', status: 'done' },
];
const resultsFailOnly = buildMockOcrResults(filesFailOnly, 'policy', '商业险', insMap).map((r, idx) => (
markRecognResultFailed(
r,
idx === 0 ? POLICY_RECOGN_FAIL_REASON_CORRUPT : POLICY_RECOGN_FAIL_REASON_FORMAT,
)
));
return [ return [
buildPolicyRecognTaskRecord({ buildPolicyRecognTaskRecord({
id: 'TASK-83892906', id: 'TASK-83892906',
@@ -2616,6 +2664,18 @@ const createMockPolicyRecognTasks = () => {
totalFileCount: filesSuspend.length, totalFileCount: filesSuspend.length,
recognDoneCount: filesSuspend.length, recognDoneCount: filesSuspend.length,
}), }),
buildPolicyRecognTaskRecord({
id: 'TASK-84450001',
entry: 'ocr',
mode: 'policy',
insuranceType: '商业险',
results: resultsFailOnly,
createdAt: '2026-06-03 11:05:00',
completedAt: '2026-06-03 11:06:20',
phase: 'results',
totalFileCount: filesFailOnly.length,
recognDoneCount: filesFailOnly.length,
}),
buildPolicyRecognTaskRecord({ buildPolicyRecognTaskRecord({
id: 'TASK-84210588', id: 'TASK-84210588',
entry: 'ocr', entry: 'ocr',
@@ -3317,6 +3377,8 @@ const Component = function () {
return stored && stored.length ? stored : createMockPolicyRecognTasks(); return stored && stored.length ? stored : createMockPolicyRecognTasks();
}); });
const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false); const [policyRecognTasksOpen, setPolicyRecognTasksOpen] = useState(false);
const [policyRecognFailDetailOpen, setPolicyRecognFailDetailOpen] = useState(false);
const [policyRecognFailDetailTask, setPolicyRecognFailDetailTask] = useState(null);
const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS })); const [policyRecognTasksFilters, setPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS })); const [appliedPolicyRecognTasksFilters, setAppliedPolicyRecognTasksFilters] = useState(() => ({ ...DEFAULT_POLICY_RECOGN_TASK_FILTERS }));
const [policyAddOpen, setPolicyAddOpen] = useState(false); const [policyAddOpen, setPolicyAddOpen] = useState(false);
@@ -4602,10 +4664,27 @@ const Component = function () {
if (preferred) { if (preferred) {
setPolicyRecognActiveResultId(preferred.id); setPolicyRecognActiveResultId(preferred.id);
setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, task.mode || 'policy')); setPolicyRecognConfirmDraft(buildPolicyRecognConfirmDraft(preferred, task.mode || 'policy'));
showPolicyRecognResultPreview(preferred); } else {
setPolicyRecognActiveResultId('');
setPolicyRecognConfirmDraft({ ...EMPTY_POLICY_DETAIL });
} }
}; };
const openPolicyRecognFailDetail = (task) => {
const fails = getPolicyRecognFailResults(task?.results);
if (!fails.length) {
message.info('该任务没有识别失败记录');
return;
}
setPolicyRecognFailDetailTask(task);
setPolicyRecognFailDetailOpen(true);
};
const policyRecognFailDetailRows = useMemo(
() => getPolicyRecognFailResults(policyRecognFailDetailTask?.results),
[policyRecognFailDetailTask],
);
const closePolicyRecogn = () => { const closePolicyRecogn = () => {
const syncedResults = policyRecognPhase === 'results' const syncedResults = policyRecognPhase === 'results'
? persistActiveRecognDraft() ? persistActiveRecognDraft()
@@ -8120,13 +8199,31 @@ const Component = function () {
{ {
title: '识别失败数', title: '识别失败数',
dataIndex: 'recognFailCount', dataIndex: 'recognFailCount',
width: 96, width: 108,
align: 'center', align: 'center',
render: (val) => ( render: (val, record) => {
<span style={{ fontVariantNumeric: 'tabular-nums', color: val > 0 ? '#dc2626' : '#64748b', fontWeight: 600 }}> const count = val ?? 0;
{val ?? 0} if (count > 0) {
return (
<button
type="button"
className="lc-policy-recogn-fail-count-btn"
aria-label={`查看 ${count} 条识别失败明细`}
onClick={(e) => {
e.stopPropagation();
openPolicyRecognFailDetail(record);
}}
>
{count}
</button>
);
}
return (
<span style={{ fontVariantNumeric: 'tabular-nums', color: '#64748b', fontWeight: 600 }}>
0
</span> </span>
), );
},
}, },
{ {
title: '识别进度', title: '识别进度',
@@ -8171,31 +8268,112 @@ const Component = function () {
fixed: 'right', fixed: 'right',
render: (_, record) => { render: (_, record) => {
const recognizing = isPolicyRecognTaskRecognizing(record); const recognizing = isPolicyRecognTaskRecognizing(record);
const noSuccess = !(record.recognSuccessCount > 0); const hasSuccess = (record.recognSuccessCount || 0) > 0;
const disabled = recognizing || noSuccess; const hasFail = (record.recognFailCount || 0) > 0;
const action = (
<OperationActions
view={{
label: '确认识别结果',
disabled,
onClick: () => openPolicyRecognTaskRecord(record),
}}
/>
);
if (recognizing) { if (recognizing) {
return ( return (
<Tooltip title="请等待识别完成后操作"> <Tooltip title="请等待识别完成后操作">
<span>{action}</span> <span>
<OperationActions
view={{
label: '确认识别结果',
disabled: true,
onClick: () => {},
}}
/>
</span>
</Tooltip> </Tooltip>
); );
} }
return action; if (!hasSuccess && hasFail) {
return (
<OperationActions
view={{
label: '查看失败明细',
onClick: () => openPolicyRecognFailDetail(record),
}}
/>
);
}
return (
<OperationActions
view={{
label: '确认识别结果',
disabled: !hasSuccess,
onClick: () => openPolicyRecognTaskRecord(record),
}}
more={hasFail ? [{
key: 'failDetail',
label: '查看失败明细',
onClick: () => openPolicyRecognFailDetail(record),
}] : undefined}
/>
);
}, },
}, },
]} ]}
/> />
</Modal> </Modal>
<Modal
className="lc-policy-recogn-fail-detail-modal"
open={policyRecognFailDetailOpen}
title={policyRecognFailDetailTask
? `识别失败明细(${policyRecognFailDetailRows.length}`
: '识别失败明细'}
width={720}
centered
footer={(
<Button
type="primary"
onClick={() => {
setPolicyRecognFailDetailOpen(false);
setPolicyRecognFailDetailTask(null);
}}
>
关闭
</Button>
)}
onCancel={() => {
setPolicyRecognFailDetailOpen(false);
setPolicyRecognFailDetailTask(null);
}}
>
<Alert
type="warning"
showIcon
style={{ marginBottom: 14, borderRadius: 8 }}
message="以下文件识别未成功,不会进入确认写入台账;可核对文件后重新发起识别。"
/>
<Table
size="middle"
rowKey="id"
dataSource={policyRecognFailDetailRows}
pagination={policyRecognFailDetailRows.length > 8
? { pageSize: 8, showSizeChanger: false }
: false}
locale={{ emptyText: '暂无失败记录' }}
columns={[
{
title: '文件/保单名',
dataIndex: 'fileName',
width: 280,
ellipsis: true,
render: (val) => (
<span style={{ fontWeight: 600, color: '#0f172a' }}>{val || '—'}</span>
),
},
{
title: '失败原因',
dataIndex: 'failReason',
render: (val) => (
<span style={{ color: '#b91c1c', lineHeight: 1.45 }}>{val || '—'}</span>
),
},
]}
/>
</Modal>
<Modal <Modal
className="lc-policy-recogn-modal" className="lc-policy-recogn-modal"
title={ title={

File diff suppressed because one or more lines are too long

View File

@@ -460,6 +460,46 @@
margin-bottom: var(--oneos-space-3); margin-bottom: var(--oneos-space-3);
} }
/* 识别任务:失败数可点击入口 */
.lc-policy-recogn-fail-count-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
padding: 0 8px;
margin: 0;
border: none;
border-radius: 6px;
background: transparent;
color: #dc2626;
font-size: 13px;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.2;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
transition: background-color 0.15s ease, color 0.15s ease;
}
.lc-policy-recogn-fail-count-btn:hover,
.lc-policy-recogn-fail-count-btn:focus-visible {
color: #b91c1c;
background: color-mix(in srgb, #ef4444 10%, transparent);
outline: none;
}
.lc-policy-recogn-fail-count-btn:focus-visible {
box-shadow: 0 0 0 2px color-mix(in srgb, #ef4444 35%, transparent);
}
@media (prefers-reduced-motion: reduce) {
.lc-policy-recogn-fail-count-btn {
transition: none;
}
}
@media (max-width: 1280px) { @media (max-width: 1280px) {
.vm-page.ipc-page .lc-alert-stats-row { .vm-page.ipc-page .lc-alert-stats-row {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));

View File

@@ -6,6 +6,7 @@ import {
BookOpen, BookOpen,
Building2, Building2,
Calendar, Calendar,
CalendarClock,
Car, Car,
CircleDollarSign, CircleDollarSign,
ClipboardCheck, ClipboardCheck,
@@ -772,34 +773,53 @@ export const SELF_OPERATED_MODULES: LineModule[] = [
id: 'self-contract', id: 'self-contract',
step: 1, step: 1,
title: '自营合同', title: '自营合同',
subtitle: '关键信息录入 · 上传合同 · 自动交车', subtitle: '关键信息录入 · 业务确认生效 · 可派车',
shortLabel: '合同', shortLabel: '合同',
accent: 'blue', accent: 'blue',
icon: FileSignature, icon: FileSignature,
roles: ['业务管理组', '业务服务组'], roles: ['业务管理组', '业务服务组'],
start: '创建自营合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。', start: '创建自营运力服务合同,记录客户方、车型、车辆数、交车时间与地点等关键信息,并上传合同附件。',
process: [ process: [
'录入自营项目与客户方、车型、车辆数量等合同要素。', '录入自营项目与客户方、车型、车辆数量等合同要素。',
'填写交车时间、交车地点,上传自营合同扫描件。', '填写交车时间、交车地点,上传自营合同扫描件。',
'合同提交后自动生成交车任务,推送运维条线处理交付。', '业务确认生效后可创建轻量调度任务,并提示运维交车交付。',
], ],
closure: '合同信息与交车任务联动,自营项目从签约起即可进入交付与运营环节。', closure: '合同界定项目边界,成为调度派车与交车的上游凭证。',
prototypeHref: '/prototypes/oneos-web-lease-contract', prototypeHref: '/prototypes/self-operated-contract',
prototypeLabel: '打开车辆租赁合同', prototypeLabel: '打开自营合同',
},
{
id: 'self-dispatch',
step: 2,
title: '调度任务',
subtitle: '轻量派车 · 交还车校验 · 办结出车',
shortLabel: '调度',
accent: 'indigo',
icon: CalendarClock,
roles: ['业务服务组', '调度岗', '运维部'],
start: '在生效自营合同下创建出车任务,派车派司机;非 TMS不做配载与承运商调度。',
process: [
'选择合同、计划出车日、线路说明、是否多趟与计价口径。',
'派车派司机;车辆未交车时先完成运维交车(原型可模拟交车)。',
'办结确认实际出车后,自动生成物流业务明细一行。',
],
closure: '出车事实可追溯到合同与车牌,并驱动台账落账。',
prototypeHref: '/prototypes/self-operated-dispatch-task',
prototypeLabel: '打开调度任务',
}, },
{ {
id: 'self-ledger', id: 'self-ledger',
step: 2, step: 3,
title: '自营台账', title: '自营台账',
subtitle: '导入台账 · 成本利润核算 · 项目盈亏', subtitle: '任务自动生成 · 补录导入 · 盈亏核算',
shortLabel: '台账', shortLabel: '台账',
accent: 'emerald', accent: 'emerald',
icon: BookOpen, icon: BookOpen,
roles: ['业务服务组', '财务部'], roles: ['业务服务组', '财务部'],
start: '根据导入的自营台账与账单信息,汇总项目侧收入与成本数据。', start: '承接调度办结自动生成的出车明细,并以导入/行编辑补录异常与历史数据。',
process: [ process: [
'导入或维护自营运营台账、账单明细。', '调度办结后自动写入出车营收侧字段,并计算金额/总成本/盈亏。',
'自动计算利润侧数据,以及司机成本、车辆成本侧数据。', '氢费、ETC 等成本一期可手工补录;导入模板保留为补录通道。',
'汇集至项目盈亏表,支撑项目经营分析。', '汇集至项目盈亏表,支撑项目经营分析。',
'演进阶段关联财务「收款记录」,形成业财记录闭环。', '演进阶段关联财务「收款记录」,形成业财记录闭环。',
], ],
@@ -924,26 +944,26 @@ export const BUSINESS_LINES: BusinessLineConfig[] = [
shortLabel: '自营', shortLabel: '自营',
hero: { hero: {
title: '自营业务条线', title: '自营业务条线',
tagline: '自营物流条线合同->账单业财闭环', tagline: '自营合同 → 轻量调度 → 交还车 → 台账自动生成',
summary: summary:
'大模块串联自营合同与自营台账:合同驱动交车任务,台账导入后自动核算司机、车辆成本与项目盈亏,逐步对接财务收款形成业财闭环。', '大模块串联自营合同、轻量调度任务与自营台账:合同界定项目,调度派车办结驱动明细自动生成,运维交还车保障运营态,逐步对接财务收款形成业财闭环。',
stats: [ stats: [
{ value: '2', label: '业务模块' }, { value: '3', label: '业务模块' },
{ value: '4+', label: '协同部门' }, { value: '4+', label: '协同部门' },
{ value: '1', label: '业财闭环' }, { value: '1', label: '经营闭环' },
], ],
}, },
modules: SELF_OPERATED_MODULES, modules: SELF_OPERATED_MODULES,
loopTitle: '合同到台账,自营经营可核算', loopTitle: '合同到调度再到台账,自营经营可核算',
loopSummary: loopSummary:
'自营合同录入并生成交车任务 → 运营数据导入自营台账 → 自动汇总利润与司机、车辆成本 → 汇集项目盈亏表 → 关联收款记录完成业财闭环。', '自营合同生效 → 轻量调度派车(交车后可办结)→ 办结自动生成物流业务明细 → 自动汇总盈亏 → 汇集项目盈亏表 → 演进关联收款记录完成业财闭环。',
evolution: { evolution: {
currentLabel: '现阶段 · 人工核算台账', currentLabel: '现阶段 · 合同 + 调度办结落账',
currentText: currentText:
'人工发起自营合同,人工计算车辆成本、司机成本与自营台账,项目盈亏依赖线下表格汇总与人工核对。', '自营合同业务确认生效;轻量调度派车办结(须交车);台账以自动生成为主、导入为辅,盈亏在台账内可算。',
targetLabel: '演进方向 · 业财记录闭环', targetLabel: '演进方向 · 业财记录闭环',
targetText: targetText:
'仍人工发起合同;司机在线管理、司机成本自动计算;自营台账人工导入后自动汇总,关联财务「收款记录」模块,形成业财记录闭环。', '成本侧氢费/ETC/日成本自动回填;司机在线管理;台账关联财务「收款记录」,形成业财记录闭环;不做完整 TMS。',
}, },
}, },
]; ];

View File

@@ -1,11 +1,23 @@
# 加氢订单H5— 已确认需求与设计决策 # 加氢订单H5— 已确认需求与设计决策
> **状态:已收敛 / 部分作废2026-07-18**
> 现行权威规格以 [requirements-prd.md](./requirements-prd.md) 及同目录功能规格为准。
> 下文保留为 2026-07-14 对齐快照;下列条目**已被后续决策覆盖,勿再按本文件验收**
>
> | 快照原文 | 现行口径 |
> |---|---|
> | 顶栏演示可切换示例站 | **H5 无切站**,固定登录本站 |
> | 列表展示对账状态 / 对账时间 | 列表只展示**核对**;对账见详情 |
> | 已对账才只读 | **羚牛**已核对或已对账即锁定;非羚牛不锁 |
> | 无底 Tab | 已有底栏 Tab加氢订单 / 手工台账 |
> | 分步 OCR 向导 | 已改为单表单 + OCR 拍摄区 |
| 项 | 内容 | | 项 | 内容 |
|---|---| |---|---|
| 日期 | 2026-07-14 | | 日期 | 2026-07-14(快照);覆盖说明 2026-07-18 |
| 原型 ID | `oneos-h5-h2-order` | | 原型 ID | `oneos-h5-h2-order` |
| 菜单位置 | OneOS → 加氢站管理 → 加氢订单 | | 菜单位置 | OneOS → 加氢站管理 → 加氢订单 |
| 文档性质 | 决策快照(对齐当时确认,不随实现逐行同步 | | 文档性质 | 历史决策快照(**不随实现同步**;冲突处以 PRD 为准 |
--- ---
@@ -14,8 +26,8 @@
| 问题 | 用户选择 | | 问题 | 用户选择 |
|---|---| |---|---|
| 列表数据范围 | **A. 仅本站**(当前演示站点绑定的加氢站) | | 列表数据范围 | **A. 仅本站**(当前演示站点绑定的加氢站) |
| 登录态 | **A. 已登录态**;顶栏展示站名演示可切换示例站 | | 登录态 | **A. 已登录态**;顶栏展示站名 ~~演示可切换示例站~~**已收敛:无切站** |
| 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除 | | 已对账记录 | **A. 不可改**(只读);未对账可编辑 / 删除**已收敛:羚牛·已核对或已对账均锁** |
| OCR 演示 | **A. 拍照或选图 → 约 1s 模拟识别 → 可校正**(已改为单表单,非三步) | | OCR 演示 | **A. 拍照或选图 → 约 1s 模拟识别 → 可校正**(已改为单表单,非三步) |
### 1.1 目标与范围 ### 1.1 目标与范围
@@ -61,8 +73,8 @@
| 预览壳 | 居中 390×844 手机框;真机浏览器可全宽 | | 预览壳 | 居中 390×844 手机框;真机浏览器可全宽 |
| 首屏 | 列表为主 + 悬浮「+」 | | 首屏 | 列表为主 + 悬浮「+」 |
| 卡片优先级 | 车牌、状态、时间、量与金额;单价 / 对账时间次级 | | 卡片优先级 | 车牌、状态、时间、量与金额;单价 / 对账时间次级 |
| 无底 Tab | 本模块单入口,不套小羚羚全局 Tab | | 无底 Tab | ~~本模块单入口~~**已收敛:底栏「加氢订单 / 手工台账」** |
| 演示切换 | 顶栏站点下拉,仅演示,不代表正式鉴权 | | 演示切换 | ~~顶栏站点下拉~~**已收敛H5 固定本站、无切站** |
### 2.2 信息架构 ### 2.2 信息架构

View File

@@ -2,7 +2,7 @@
## 目标 ## 目标
查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除。 查看单笔加氢记录完整字段;在「未核对且未对账」时可编辑或删除**锁定仅适用于羚牛车辆**
## 详情展示 ## 详情展示
@@ -11,15 +11,18 @@
- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) - **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图)
- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 - 明细行:站点、车牌、里程、单价、核对/对账状态与时间
> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。
## 锁定规则 ## 锁定规则
| 条件 | 结果 | | 优先级 | 条件 | 结果 |
|------|------| |--------|------|------|
| 已对账 | 不可编辑/删除;提示已纳入站点对账单 | | 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |
| 已核对(未对账 | 不可编辑/删除;提示能源部已核对 | | 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |
| 核对未对账 | 可编辑、可删除 | | 3 | 羚牛 ∧ 已核对未对账 | 可编辑/删除;提示能源部已核对 |
| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |
判定函数:`isOrderLocked`(核对或对账任一成立即锁)。 判定函数:`isOrderLocked`先判羚牛;再核「核对或对账任一成立即锁)。
## 编辑 ## 编辑
@@ -27,11 +30,12 @@
## 删除 ## 删除
二次确认后从 Bridge 移除该行。 二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。
## 验收 ## 验收
1. 未核对未对账:底部有删除/编辑 1. 羚牛 ∧ 未核对未对账:底部有删除/编辑
2. 已核对或已对账:无操作栏,有锁定说明 2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明
3. 详情含对账信息;列表卡片不含对账 3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」
4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) 4. 详情含对账信息;列表卡片不含对账
5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图)

View File

@@ -9,14 +9,14 @@
| 项 | 规则 | | 项 | 规则 |
|---|---| |---|---|
| 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 | | 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 |
| 站点范围 | 仅展示当前登录用户角色对应站点;单站人员仅 1 个、多站可切换、超级管理员可选全部(原型用标注状态切换演示,未接真实登录) | | 站点范围 | **H5 固定本站**无切站Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |
| 上传形态 | 仅图片(拍照/相册),可多张 | | 上传形态 | 仅图片(拍照/相册),可多张 |
| 维度 | **加氢站 + 自然日(本地日历日)** 一份 | | 维度 | **加氢站 + 自然日(本地日历日)** 一份 |
| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 | | 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |
| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 | | 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |
| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 | | 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |
| 二次确认 | Web 点「确认上传/补传」先弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 | | 二次确认 | H5 / Web 确认上传弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |
| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代日校验) | | 拦截 | **前一日手工台账未上传今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史不替代日校验) |
| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API站名/编码解析见 Bridge.`h2BridgeResolveStationId` | | 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API站名/编码解析见 Bridge.`h2BridgeResolveStationId` |
## 判定顺序(新增拦截) ## 判定顺序(新增拦截)
@@ -24,8 +24,9 @@
| 优先级 | 条件 | 结果 | | 优先级 | 条件 | 结果 |
|--------|------|------| |--------|------|------|
| 1 | 操作=编辑已有记录 | 不校验手工台账 | | 1 | 操作=编辑已有记录 | 不校验手工台账 |
| 2 | 操作=新增,且目标站今日已有 ≥1 张台账 | 允许进入新增 | | 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账 | 允许进入新增 |
| 3 | 操作=新增,今日未上传 | 提示并跳转手工台账 Tab | | 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |
| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |
## 日历与补传 ## 日历与补传
@@ -36,26 +37,27 @@
| 未传计数 | 本月内 `max(月初, 首笔日)`min(月末, 今日) 中未上传的天数 | | 未传计数 | 本月内 `max(月初, 首笔日)`min(月末, 今日) 中未上传的天数 |
| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 | | 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |
| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 | | 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |
| 与拦截关系 | 仅 **** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 | | 与拦截关系 | 仅 **** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |
## 用户可见 ## 用户可见
- 今日状态双反馈Web 放在右侧「当日台账」卡片内):未上传「今日尚未上传手工台账YYYY-MM-DD」异常态已上传「今日已上传手工台账YYYY-MM-DD」正常态 - 今日状态双反馈(H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」
- 今日未上传:橙/黄提示 + Tab 红点 - 昨日缺失H5**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」
- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 - 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要
- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高 - Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高
## 代码路径 ## 代码路径
- Bridge`src/common/h2VehicleLedgerBridge.js``hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation` - Bridge`src/common/h2VehicleLedgerBridge.js``hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation`
- H5`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts``buildManualMonthCells` / `getFirstHydrogenDateKey` - H5`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts``isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey`
- Web`oneos-web-h2-station/pages/02-加氢记录.jsx`手工台账 Tab 月历 - Web`oneos-web-h2-station/pages/02-加氢记录.jsx``hrIsManualLedgerGateBlocked` / 跳转选中昨日
## 验收 ## 验收
1. 日未上传时H5/Web 点「新增」被拦截并引导上传 1. 日未上传时H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日)
2. 上传至少一张图并确认后,可新增电子记录 2. 补传昨日台账并确认后,可新增加氢记录
3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传
4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 4. 补传更早历史日、昨日仍未传时新增仍被拦截
5. 确认上传后的图显示已归档且不可删除;仅可追加补传 5. 确认上传后的图显示已归档且不可删除;仅可追加补传
6. H5 与 Web 共用同一 Store一端上传另一端可见同会话 6. H5 与 Web 共用同一 Store一端上传另一端可见同会话
7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致

View File

@@ -2,6 +2,9 @@
原型本地判定,未接真实车辆管理 API。 原型本地判定,未接真实车辆管理 API。
全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5
[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)
## 判定顺序 ## 判定顺序
| 优先级 | 条件 | 结果 | | 优先级 | 条件 | 结果 |
@@ -17,14 +20,14 @@
## 数据源 ## 数据源
- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。 - 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。
- 正式环境应改为查询车辆管理 / 系统车辆列表接口。
## 用户可见结果 ## 用户可见结果
- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。 - 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。
- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。
- 不拦截提交(仅提示归属)。 - 不拦截提交(仅提示归属)。
## 代码路径 ## 代码路径
- `getVehicleTag` / `isLingniuVehicle``CreateWizard` 车牌行右侧标签。 - `getVehicleTag` / `isLingniuVehicle``CreateWizard``OrderCard``OrderDetail`

View File

@@ -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: 提交对账单 → 已对账
```

View File

@@ -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` |

View File

@@ -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 处理全部冲突与 P0P3 项)
- 资料冲突/待确认:**无**(已对齐)
- 独立评估:`full`
## 总体点评
加氢订单 H5 主路径完整可演示。评审指出的冲突已闭环:**昨日门禁文案**、**列表顶栏橙条**、**锁定仅羚牛**(规格与实现一致)、**早期切站稿作废标注**、**删除确认「未锁定」**、历史 resources 权威顺序说明。剩余为原型边界内 MockOCR / 车机 / 无真实 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`

View File

@@ -4,6 +4,7 @@
|---|---| |---|---|
| 代码路径 | Store`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` | | 代码路径 | Store`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |
| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) | | 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
| 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** | | 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** |
--- ---
@@ -17,6 +18,8 @@
> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。 > `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。
---
## 2. 数据流 ## 2. 数据流
```text ```text
@@ -26,23 +29,29 @@
能源部 · 完成核对 能源部 · 完成核对
→ verifyStatus=verified + verifiedAt → verifyStatus=verified + verifiedAt
→ 站端同步「已核对」禁改删 → 站端同步「已核对」;羚牛车辆禁改删
站点信息 · 对账单提交(仅已核对 ∧ 未对账) 站点信息 · 对账单提交(仅已核对 ∧ 未对账)
→ reconcileStatus=reconciled + reconcileDate + statementRecordId → reconcileStatus=reconciled + reconcileDate + statementRecordId
→ 站端 / 台账同步「已对账」 → 站端 / 台账同步「已对账」;羚牛车辆禁改删
``` ```
---
## 3. 用户可见结果H5 ## 3. 用户可见结果H5
| 结果 | 行为 | | 结果 | 行为 |
|---|---| |---|---|
| 未核对 | 可编辑、删除 | | 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |
| 已核对未对账 | 只读;提示能源部已核对 | | 羚牛 · 未核对 | 可编辑、删除 |
| 对账 | 只读;提示已纳入对账单 | | 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |
| 羚牛 · 已对账 | 只读;提示已纳入对账单 |
---
## 4. 边界 ## 4. 边界
- 站端上报默认未核对、未对账 - 站端上报默认未核对、未对账
- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)
- 对账时间只认 `reconcileDate`(对账单回写) - 对账时间只认 `reconcileDate`(对账单回写)
- 跨页联动依赖同浏览器会话共享 Store - 跨页联动依赖同浏览器会话共享 Store

View File

@@ -2,7 +2,7 @@
| 项 | 内容 | | 项 | 内容 |
|---|---| |---|---|
| 文档版本 | v1.5 | | 文档版本 | v1.7 |
| 模块名称 | 加氢站管理 → 加氢订单 | | 模块名称 | 加氢站管理 → 加氢订单 |
| 所属系统 | ONE-OS加氢站手机浏览器 | | 所属系统 | ONE-OS加氢站手机浏览器 |
| 交互原型 | `/prototypes/oneos-h5-h2-order` | | 交互原型 | `/prototypes/oneos-h5-h2-order` |
@@ -15,7 +15,7 @@
采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据PC 另有导出与预约加氢)。 采购创建加氢站并绑定供应商、开放系统账号后,加氢站人员通过手机浏览器登录 OneOS在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据PC 另有导出与预约加氢)。
每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。 每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。
### 1.1 目标用户 ### 1.1 目标用户
@@ -46,14 +46,15 @@
1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 1. 菜单位于 OneOS → 加氢站管理 → 加氢订单
2. 默认已登录本站;列表仅本站(无切站) 2. 默认已登录本站;列表仅本站(无切站)
3. 底栏 Tab加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分) 3. 底栏 Tab加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分)
4. 已核对或已对账不可编辑/删除;未核对未对账可 4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删
5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间 5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛)
6. 同站同车牌同加氢时间重复保存被拦截 6. 同站同车牌同加氢时间重复保存被拦截
7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致同 Store 7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致同 Store
8. 车牌/面板拍摄预览含水印(时间、地点=本站名称里程可从车机获取Mock 8. 车牌/面板拍摄预览含水印(时间、地点=本站名称里程可从车机获取Mock
9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索
10. 详情页展示车牌与面板带水印原图 10. 详情页展示车牌与面板带水印原图
11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁
--- ---
@@ -61,11 +62,11 @@
| 主题 | 摘要 | 全文 | | 主题 | 摘要 | 全文 |
|---|---|---| |---|---|---|
| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) | | 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |
| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) | | 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |
| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) | | 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |
| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) | | 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |
| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) | | 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |
--- ---
@@ -77,4 +78,16 @@
| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 | | `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |
| `detail` | 详情字段、锁定说明、编辑/删除 | | `detail` | 详情字段、锁定说明、编辑/删除 |
右侧工具栏「原型目录」结构:
| 目录 | 内容 |
|---|---|
| PRD 全文 | requirements-prd.md |
| 需求角色 | roles.md |
| 用户故事 | user-stories.md |
| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |
| 流程图 | flows.mdMermaid |
| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |
| 按页面 | list / create / detail 要点 |
标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。 标注数据:`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。

View File

@@ -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「加氢记录」

View File

@@ -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 识别羚牛车辆
**作为** 加氢站操作人员,
**我希望** 在列表/详情看到是否羚牛车辆,
**以便** 理解后续核对对账是否适用该车。
**验收要点**
- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定)

View File

@@ -5,13 +5,13 @@
"version": 2, "version": 2,
"prototypeName": "oneos-h5-h2-order", "prototypeName": "oneos-h5-h2-order",
"pageId": "list", "pageId": "list",
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"nodes": [ "nodes": [
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-hero", "id": "h5-hero",
@@ -31,8 +31,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-summary", "id": "h5-summary",
@@ -52,8 +52,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-card", "id": "h5-card",
@@ -73,8 +73,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-fab", "id": "h5-fab",
@@ -94,8 +94,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-time", "id": "h5-time",
@@ -115,8 +115,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-station", "id": "h5-station",
@@ -136,8 +136,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-plate", "id": "h5-plate",
@@ -157,8 +157,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-mileage", "id": "h5-mileage",
@@ -178,8 +178,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-dispenser-brands", "id": "h5-dispenser-brands",
@@ -199,8 +199,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-price", "id": "h5-price",
@@ -220,8 +220,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-kg", "id": "h5-kg",
@@ -241,8 +241,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-total-label", "id": "h5-total-label",
@@ -262,8 +262,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-detail", "id": "h5-detail",
@@ -283,8 +283,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-detail-photos", "id": "h5-detail-photos",
@@ -304,8 +304,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-manual", "id": "h5-manual",
@@ -325,8 +325,8 @@
{ {
"images": [], "images": [],
"controls": [], "controls": [],
"createdAt": 1784280448632, "createdAt": 1784378211658,
"updatedAt": 1784280448632, "updatedAt": 1784378211658,
"hasMarkdown": true, "hasMarkdown": true,
"annotationText": "", "annotationText": "",
"id": "h5-manual-calendar", "id": "h5-manual-calendar",
@@ -352,16 +352,16 @@
"h5-fab": "## 底栏新增\n\n固定在列表底部不遮挡卡片滚动区。点击进入新增表单加氢时间默认=点击时刻。\n\n详见功能说明「新增加氢记录」。", "h5-fab": "## 底栏新增\n\n固定在列表底部不遮挡卡片滚动区。点击进入新增表单加氢时间默认=点击时刻。\n\n详见功能说明「新增加氢记录」。",
"h5-time": "## 加氢时间(必填)\n\n默认=点击「新增」时的当前时间;点击后底部弹层选择日期 + 时 + 分。", "h5-time": "## 加氢时间(必填)\n\n默认=点击「新增」时的当前时间;点击后底部弹层选择日期 + 时 + 分。",
"h5-station": "## 加氢站名称\n\n只读。默认显示当前登录账号对应加氢站名称不可切换。", "h5-station": "## 加氢站名称\n\n只读。默认显示当前登录账号对应加氢站名称不可切换。",
"h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别OCR或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n", "h5-plate": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5 \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别OCR或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
"h5-mileage": "## 里程km\n\n选填。支持「从车机获取」原型 Mock识别车牌后可自动回填。正式环境对接车机 / T-Box。\n\n详见 [新增加氢记录](feature-create)。", "h5-mileage": "## 里程km\n\n选填。支持「从车机获取」原型 Mock识别车牌后可自动回填。正式环境对接车机 / T-Box。\n\n详见 [新增加氢记录](feature-create)。",
"h5-dispenser-brands": "## 加氢机品牌参考\n\n展示站点信息「加氢机品牌管理」维护的品牌·型号供面板 OCR 模型判断参考。数据源:`h2DispenserBrandStore`。", "h5-dispenser-brands": "## 加氢机品牌参考\n\n展示站点信息「加氢机品牌管理」维护的品牌·型号供面板 OCR 模型判断参考。数据源:`h2DispenserBrandStore`。",
"h5-price": "## 氢气单价(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额(单价×加氢量)。", "h5-price": "## 氢气单价(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额(单价×加氢量)。",
"h5-kg": "## 加氢量(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额。", "h5-kg": "## 加氢量(必填)\n\n可 OCR 识别后校正;变更后自动重算加氢总额。",
"h5-total-label": "## 加氢总额(元)\n\n只读展示由单价×加氢量自动计算两位小数。界面标签不展示公式文案。", "h5-total-label": "## 加氢总额(元)\n\n只读展示由单价×加氢量自动计算两位小数。界面标签不展示公式文案。",
"h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单预填当前值含已存水印照片提交走更新逻辑仍校验重复键。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n", "h5-detail": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段在「未核对且未对账」时可编辑或删除**锁定仅适用于羚牛车辆**。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数`isOrderLocked`先判羚牛;再核「核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单预填当前值含已存水印照片提交走更新逻辑仍校验重复键。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
"h5-detail-photos": "## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。", "h5-detail-photos": "## 识别原图(含水印)\n\n详情展示车牌识别、加氢机面板的原始照片照片在拍摄/选图时已叠加时间与地点水印并随记录保存。种子数据无实拍时用演示水印图。\n\n详见 [详情编辑删除](feature-detail)。",
"h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge`src/common/h2VehicleLedgerBridge.js``hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation` \n- H5`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts``buildManualMonthCells` / `getFirstHydrogenDateKey` \n- Web`oneos-web-h2-station/pages/02-加氢记录.jsx`手工台账 Tab 月历 \n\n## 验收\n\n1. 日未上传时H5/Web 点「新增」被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store一端上传另一端可见同会话 \n", "h5-manual": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**无切站Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史不替代日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API;站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失H5**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge`src/common/h2VehicleLedgerBridge.js``hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation` \n- H5`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts``isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey` \n- Web`oneos-web-h2-station/pages/02-加氢记录.jsx``hrIsManualLedgerGateBlocked` / 跳转选中昨日 \n\n## 验收\n\n1. 日未上传时H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传更早历史日、昨日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store一端上传另一端可见同会话 \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
"h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。仅「今日」是否上传影响新增加氢记录。\n\n详见 [手工台账上传](feature-manual-ledger)。" "h5-manual-calendar": "## 台账日历\n\n自本站首笔加氢记录日起绿=已上传,橙=未上传;首笔之前无需补传。禁选未来日。点选范围内未传日可补传。**前一日**未上传时禁止新增加氢记录(今日台账状态不作为门禁)。\n\n详见 [手工台账上传](feature-manual-ledger)。"
}, },
"assetMap": {}, "assetMap": {},
"directory": { "directory": {
@@ -376,9 +376,90 @@
"type": "markdown", "type": "markdown",
"id": "h5-doc-prd", "id": "h5-doc-prd",
"title": "PRD 全文", "title": "PRD 全文",
"markdown": "# 加氢订单H5— 产品需求文档PRD\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.5 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS加氢站手机浏览器 |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后加氢站人员通过手机浏览器登录 OneOS在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;未上传当日手工台账时不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;未上传今日手工台账不可新增;上传后可新增;时间默认点击新增时刻(到分) \n4. 已核对或已对账不可编辑/删除;未核对未对账可 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间 \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致同 Store \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称里程可从车机获取Mock \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;未上传今日禁新增 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n标注数据`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n", "markdown": "# 加氢订单H5— 产品需求文档PRD\n\n| 项 | 内容 |\n|---|---|\n| 文档版本 | v1.7 |\n| 模块名称 | 加氢站管理 → 加氢订单 |\n| 所属系统 | ONE-OS加氢站手机浏览器 |\n| 交互原型 | `/prototypes/oneos-h5-h2-order` |\n| 设计基底 | 小羚羚 `xll-miniapp/DESIGN.md` |\n| 关联模块 | [站点信息](/prototypes/oneos-web-h2-station-site)、[加氢记录 Web](/prototypes/oneos-web-h2-station)、[车辆氢费明细](/prototypes/vehicle-h2-fee-ledger) |\n\n---\n\n## 1. 背景与目标\n\n采购创建加氢站并绑定供应商、开放系统账号后加氢站人员通过手机浏览器登录 OneOS在「加氢订单」模块上报加氢记录。Web「加氢记录」为同一业务的 PC 入口(共享 Bridge 数据PC 另有导出与预约加氢)。\n\n每日须上传**手工台账**照片,作为电子记录的核对依据;**前一日**手工台账未上传时,今日不可新增加氢记录。手工台账页提供月历,标出已传/未传日期,支持补传过往缺口。\n\n### 1.1 目标用户\n\n加氢站操作人员户外 / 站场手机使用)。\n\n### 1.2 功能清单\n\n| # | 功能 | 说明文档 |\n|---|---|---|\n| 1 | 本站列表与 KPI | [feature-list.md](./feature-list.md) |\n| 2 | 新增加氢记录 | [feature-create.md](./feature-create.md) |\n| 3 | 详情 / 编辑 / 删除 | [feature-detail.md](./feature-detail.md) |\n| 4 | **手工台账上传** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 5 | 羚牛车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 6 | 核对与对账联动 | [reconcile-linkage.md](./reconcile-linkage.md) |\n\n### 1.3 列表字段\n\n加氢时间、车牌号、氢气单价、加氢量、加氢总额、核对状态已核对时展示核对时间。里程在新增/详情展示(选填),列表不展示。列表不展示对账状态。\n\n### 1.4 不做\n\n真登录、真 OCR SDK、PLC、原生 App、后端联调。\n\n---\n\n## 2. 验收项\n\n1. 菜单位于 OneOS → 加氢站管理 → 加氢订单 \n2. 默认已登录本站;列表仅本站(无切站) \n3. 底栏 Tab加氢订单 / 手工台账;月历可区分已传/未传并补传过往日;前一日未上传时提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」且点新增直达台账页;补传昨日后可新增;时间默认点击新增时刻(到分) \n4. **羚牛车辆**已核对或已对账不可编辑/删除;非羚牛不锁定;未核对未对账(羚牛)可改删 \n5. 能源部完成核对后同步「已核对」;站点对账单提交后同步「已对账」+ 对账时间(锁定仅羚牛) \n6. 同站同车牌同加氢时间重复保存被拦截 \n7. Web「加氢记录」与 H5「加氢订单」手工台账逻辑一致同 Store \n8. 车牌/面板拍摄预览含水印(时间、地点=本站名称里程可从车机获取Mock \n9. 面板 OCR 可展示站点信息维护的加氢机品牌型号线索 \n10. 详情页展示车牌与面板带水印原图 \n11. 昨日台账缺失时:列表顶栏橙条 + 底栏提示 + Tab 红点;台账页不把「今日未传」写成新增门禁 \n\n---\n\n## 3. 复杂逻辑摘要\n\n| 主题 | 摘要 | 全文 |\n|---|---|---|\n| 对账 / 核对 | 核对≠对账;**锁定仅羚牛** | [reconcile-linkage.md](./reconcile-linkage.md)、[车辆氢费 verify-reconcile](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 重复键 | 与 Web 共用 Bridge | [Web 数据模型](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 羚牛车辆标签 | 本地系统车辆集合判定 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 总额 | 单价×加氢量自动算,界面不展示公式 | [feature-create.md](./feature-create.md) |\n| 手工台账 | 按站+自然日上传图片;月历标已传/未传可补传;**昨日未传则今日禁新增** | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n\n---\n\n## 4. 页面与标注对照\n\n| 页面 pageId | 主要能力 |\n|---|---|\n| `list` | 站点头、KPI、订单卡片、底栏新增 |\n| `create` | 时间选择、站名、车牌键盘、面板 OCR、金额字段 |\n| `detail` | 详情字段、锁定说明、编辑/删除 |\n\n右侧工具栏「原型目录」结构\n\n| 目录 | 内容 |\n|---|---|\n| PRD 全文 | requirements-prd.md |\n| 需求角色 | roles.md |\n| 用户故事 | user-stories.md |\n| 关键逻辑 | key-logic + 台账门禁 / 对账 / 车辆标签 |\n| 流程图 | flows.mdMermaid |\n| 功能说明 | 列表 / 新增 / 台账 / 详情 / 车辆 / 对账 |\n| 按页面 | list / create / detail 要点 |\n\n标注数据`annotation-source.json`(由 `scripts/build-annotation-source.mjs` 从本目录 Markdown 生成)。\n",
"markdownPath": ".spec/requirements-prd.md" "markdownPath": ".spec/requirements-prd.md"
}, },
{
"type": "folder",
"id": "h5-doc-roles",
"title": "需求角色",
"defaultExpanded": true,
"children": [
{
"type": "markdown",
"id": "h5-doc-roles-md",
"title": "角色说明",
"markdown": "# 需求角色\n\n| 项 | 说明 |\n|---|---|\n| 适用原型 | 加氢订单H5`oneos-h5-h2-order` |\n| 关联 Web | 加氢记录 `oneos-web-h2-station`(同一业务、不同端) |\n| 数据 | 原型本地种子 + 共享 Bridge未接真实登录 / API |\n\n---\n\n## 1. 角色一览\n\n| 角色 | 端 | 核心诉求 | 本原型是否为主用户 |\n|---|---|---|---|\n| **加氢站操作人员** | H5手机浏览器 | 在站场快速上报本站加氢流水;补传手工台账照片 | **是** |\n| **加氢站站长 / 多站管理员** | Web 为主H5 可看本站 | 多站台账、导出、手工台账补传与督导 | 间接H5 固定单站演示) |\n| **能源部核对人员** | 车辆氢费明细 | 完成核对,回写「已核对」 | 否(触发方在台账) |\n| **站点对账人员** | 站点信息 · 对账单 | 提交对账单,回写「已对账」 | 否(触发方在站点信息) |\n| **超级管理员** | Web | 查看全部站点流水与台账 | 否Web 标注状态演示) |\n\n---\n\n## 2. 本原型主角色:加氢站操作人员\n\n| 项 | 说明 |\n|---|---|\n| 场景 | 户外 / 站场,手机竖屏 |\n| 权限边界 | 仅本站数据;不可切站(原型固定演示站) |\n| 日常动作 | 看列表 KPI → 上传昨日手工台账(若缺失)→ 新增加氢记录 → 查看/编辑未锁定记录 |\n| 约束 | 前一日手工台账未上传则今日不可新增;已核对或已对账不可改删 |\n\n---\n\n## 3. 角色边界(本期不做)\n\n- 真登录与组织权限中心对接 \n- 能源部 / 对账人员在 H5 内操作核对或对账 \n- 跨站汇总与导出(属 Web「加氢记录」 \n",
"markdownPath": ".spec/roles.md"
}
]
},
{
"type": "folder",
"id": "h5-doc-stories",
"title": "用户故事",
"defaultExpanded": true,
"children": [
{
"type": "markdown",
"id": "h5-doc-stories-md",
"title": "用户故事",
"markdown": "# 用户故事\n\n> 以加氢站操作人员为主;验收口径见 [requirements-prd.md](./requirements-prd.md)。\n\n---\n\n## US-01 查看本站加氢订单\n\n**作为** 加氢站操作人员, \n**我希望** 打开加氢订单即看到本站列表与 KPI \n**以便** 快速了解今日/近期上报情况。\n\n**验收要点**\n\n- 默认已登录本站;无切站 \n- KPI订单数 / 加氢量 / 加氢金额随列表汇总 \n- 卡片展示加氢时间、车牌、量、额、核对状态等 \n\n---\n\n## US-02 补传缺失的手工台账后再新增\n\n**作为** 加氢站操作人员, \n**我希望** 在前一日手工台账未上传时被明确拦截,并直达台账页补传, \n**以便** 保证纸质台账作为对账依据不缺失。\n\n**验收要点**\n\n- 顶栏橙条 + 底栏提示:「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 点「新增」或顶栏橙条跳转手工台账并选中昨日 \n- 补传昨日确认后可新增 \n\n---\n\n## US-03 新增加氢记录\n\n**作为** 加氢站操作人员, \n**我希望** 用车牌键盘 + 面板拍照/OCR 快速填单, \n**以便** 在站场少打字完成上报。\n\n**验收要点**\n\n- 加氢时间默认=点击新增时刻(到分) \n- 站名只读为本站 \n- 总额 = 单价 × 加氢量(界面不展示公式) \n- 同站同车牌同时间重复保存拦截 \n\n---\n\n## US-04 查看详情并编辑/删除未锁定记录\n\n**作为** 加氢站操作人员, \n**我希望** 打开详情查看字段与识别原图,并对未核对未对账记录改删, \n**以便** 纠正录入错误。\n\n**验收要点**\n\n- 羚牛车辆已核对或已对账:不可编辑/删除,有锁定说明;非羚牛不锁定 \n- 详情展示车牌/面板含水印原图 \n\n---\n\n## US-05 按日历管理手工台账\n\n**作为** 加氢站操作人员, \n**我希望** 在月历上看已传/未传并补传过往日, \n**以便** 多日未操作后仍能补齐缺口。\n\n**验收要点**\n\n- 自首笔加氢日起绿/橙标记;首笔前不要求 \n- 禁选未来日;已归档图不可删,仅可追加 \n\n---\n\n## US-06 识别羚牛车辆\n\n**作为** 加氢站操作人员, \n**我希望** 在列表/详情看到是否羚牛车辆, \n**以便** 理解后续核对对账是否适用该车。\n\n**验收要点**\n\n- 标签:羚牛车辆 / 非羚牛车辆(本地车辆集合判定) \n",
"markdownPath": ".spec/user-stories.md"
}
]
},
{
"type": "folder",
"id": "h5-doc-logic",
"title": "关键逻辑",
"defaultExpanded": true,
"children": [
{
"type": "markdown",
"id": "h5-doc-logic-index",
"title": "逻辑索引",
"markdown": "# 关键逻辑索引\n\n本页汇总加氢订单H5复杂判定入口细则见各规格全文。原型数据均为本地种子 / 内存 Store**未接真实 API**。\n\n---\n\n## 1. 判定主题一览\n\n| 主题 | 一句话 | 全文 |\n|---|---|---|\n| 手工台账门禁 | **昨日**未传 → 今日禁新增;固定提示文案并直达台账 | [feature-manual-ledger.md](./feature-manual-ledger.md) |\n| 核对 ≠ 对账 | 能源部核对 / 站点对账单提交,触发方不同 | [reconcile-linkage.md](./reconcile-linkage.md) |\n| 记录锁定 | **仅羚牛**:已核对或已对账 → 站端不可改删;非羚牛不锁 | [feature-detail.md](./feature-detail.md)、[reconcile-linkage.md](./reconcile-linkage.md)、[fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 重复键 | 同站 + 同车牌 + 同加氢时间 | Bridge / [Web record-data-model](../oneos-web-h2-station/.spec/record-data-model.md) |\n| 总额计算 | 总额 = 单价 × 量(两位小数) | [feature-create.md](./feature-create.md) |\n| 羚牛车辆 | 本地系统车牌集合 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n\n---\n\n## 2. 手工台账门禁(摘要)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 编辑已有记录 | 不校验 |\n| 2 | 昨日早于本站首笔加氢日 | 不要求昨日台账,可新增 |\n| 3 | 昨日已有 ≥1 张台账图 | 可新增 |\n| 4 | 昨日未上传 | 提示并跳转手工台账(选中昨日) |\n\n用户可见文案`手工台账有缺失,作为对账重要依据,请先上传手工台账`\n\n---\n\n## 3. 核对 / 对账(摘要)\n\n| 状态 | 谁触发 | 站端影响 |\n|---|---|---|\n| 已核对 | 车辆氢费明细「完成核对」 | **羚牛**不可改删 |\n| 已对账 | 站点信息对账单提交 | **羚牛**不可改删;回写对账时间 |\n| 非羚牛 | — | 不参与核对/对账展示与锁定 |\n\nH5 列表主要展示核对状态(仅羚牛);对账细节见联动规格。\n\n---\n\n## 4. 代码路径\n\n| 能力 | 路径 |\n|---|---|\n| Bridge | `src/common/h2VehicleLedgerBridge.js` |\n| 门禁工具 | `utils/manual-ledger.ts` → `isManualLedgerGateBlocked` |\n| 订单 CRUD | `utils/orders.ts` |\n| 页面 | `index.tsx` / `ManualLedgerPanel` / `CreateWizard` |\n",
"markdownPath": ".spec/key-logic.md"
},
{
"type": "markdown",
"id": "h5-doc-logic-manual",
"title": "手工台账门禁",
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**无切站Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传 → 今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史日不替代昨日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站昨日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,昨日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **昨日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失H5**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge`src/common/h2VehicleLedgerBridge.js``hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation` \n- H5`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts``isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey` \n- Web`oneos-web-h2-station/pages/02-加氢记录.jsx``hrIsManualLedgerGateBlocked` / 跳转选中昨日) \n\n## 验收\n\n1. 昨日未上传时H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 仅补传更早历史日、昨日仍未传时,新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store一端上传另一端可见同会话 \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
"markdownPath": ".spec/feature-manual-ledger.md"
},
{
"type": "markdown",
"id": "h5-doc-logic-reconcile",
"title": "核对与对账联动",
"markdown": "# 加氢订单H5· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRowverify=unverifiedreconcile=pending成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果H5\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
"markdownPath": ".spec/reconcile-linkage.md"
},
{
"type": "markdown",
"id": "h5-doc-logic-fleet",
"title": "羚牛车辆标签",
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定未接真实车辆管理 API。\n\n全文含核对/对账字段空展示规则,跨 Web / 台账 / H5 \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别OCR或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
"markdownPath": ".spec/fleet-plate-tag.md"
}
]
},
{
"type": "folder",
"id": "h5-doc-flows",
"title": "流程图",
"defaultExpanded": true,
"children": [
{
"type": "markdown",
"id": "h5-doc-flows-md",
"title": "操作流程图",
"markdown": "# 流程图\n\n> 标注面板支持 Mermaid 渲染。下列流程与当前原型行为一致(本地 Store未接真实 API。\n\n---\n\n## 1. 主路径:从打开到上报\n\n```mermaid\nflowchart TD\n A[打开加氢订单 H5] --> B[查看本站列表与 KPI]\n B --> C{前一日手工台账已上传?}\n C -->|否| D[提示:手工台账有缺失…]\n D --> E[进入手工台账 · 选中昨日]\n E --> F[拍照/相册上传并确认]\n F --> C\n C -->|是| G[点新增]\n G --> H[填写时间/车牌/面板/量价]\n H --> I{同站同车牌同时间已存在?}\n I -->|是| J[拦截提示]\n J --> H\n I -->|否| K[保存写入 Bridge]\n K --> B\n```\n\n---\n\n## 2. 手工台账补传\n\n```mermaid\nflowchart LR\n A[手工台账 Tab] --> B[月历:绿已传 / 橙未传]\n B --> C[点选 ≤今日 且 ≥首笔日]\n C --> D[上传图片]\n D --> E[二次确认]\n E --> F[归档不可删 · 可追加]\n```\n\n---\n\n## 3. 编辑 / 删除与锁定\n\n```mermaid\nflowchart TD\n A[打开详情] --> L{羚牛车辆?}\n L -->|否| D[可编辑 / 可删除]\n L -->|是| B{已核对或已对账?}\n B -->|是| C[只读 · 禁改删]\n B -->|否| D\n D --> E[写回 Bridge]\n```\n\n---\n\n## 4. 与 Web / 台账联动(概念)\n\n```mermaid\nsequenceDiagram\n participant H5 as 加氢订单 H5\n participant BR as Bridge Store\n participant WEB as 加氢记录 Web\n participant LED as 车辆氢费明细\n participant SITE as 站点对账单\n H5->>BR: 新增/改删流水 · 上传台账\n WEB->>BR: 同 Store 读写 · 导出\n LED->>BR: 完成核对 → 已核对\n SITE->>BR: 提交对账单 → 已对账\n```\n",
"markdownPath": ".spec/flows.md"
}
]
},
{ {
"type": "folder", "type": "folder",
"id": "h5-doc-features", "id": "h5-doc-features",
@@ -403,28 +484,28 @@
"type": "markdown", "type": "markdown",
"id": "h5-doc-manual", "id": "h5-doc-manual",
"title": "手工台账上传", "title": "手工台账上传",
"markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 拦截 | **未上传今日手工台账 → 禁止新增加氢记录**(编辑已有记录不拦;补传历史不替代日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且目标站日已有 ≥1 张台账图 | 允许进入新增 |\n| 3 | 操作=新增,日未上传 | 提示并跳转手工台账 Tab |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **日** 影响「禁止新增」;历史补传解决核对缺口,不放宽今日门禁 |\n\n## 用户可见\n\n- 今日未上传:橙/黄提示 + Tab 红点 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- 选中日:上传区标题为该日期;已归档图显示「已归档」无删除;仅未确认草稿可删;已有台账时主按钮为「确认补传」\n\n## 代码路径\n\n- Bridge`src/common/h2VehicleLedgerBridge.js``hasManualLedgerForDate` / `upsertManualLedger` / `listManualLedgersByStation` \n- H5`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts``buildManualMonthCells` / `getFirstHydrogenDateKey` \n- Web`oneos-web-h2-station/pages/02-加氢记录.jsx`手工台账 Tab 月历 \n\n## 验收\n\n1. 日未上传时H5/Web 点「新增」被拦截并引导上传 \n2. 上传至少一张图并确认后,可新增电子记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传历史日后,该日日历变为已传;今日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store一端上传另一端可见同会话 \n", "markdown": "# 功能:手工台账上传\n\n## 目标\n\n站端每日上传纸质**手工台账照片**,作为系统电子加氢记录的核对依据。多日未操作时,可通过日历查看缺口并补传过往日期。\n\n## 产品口径\n\n| 项 | 规则 |\n|---|---|\n| 入口 | H5底部 Tab「手工台账」Web顶栏「手工台账」 |\n| 站点范围 | **H5 固定本站**无切站Web 可按角色多站/超管切换(标注状态演示,未接真实登录) |\n| 上传形态 | 仅图片(拍照/相册),可多张 |\n| 维度 | **加氢站 + 自然日(本地日历日)** 一份 |\n| 日历 | 月历自本站**首笔加氢记录日**起算:绿=已传、橙=未传;首笔之前无需补传;禁选未来日 |\n| 补传 | 允许为过往未传日期补传;已传日期仅可**追加**新图 |\n| 归档 | **确认上传后的图片进入已归档状态,不可删除**;未确认的草稿图仍可删除 |\n| 二次确认 | H5 / Web 确认上传前弹窗:「提交后将无法修改,是否确认手工台账照片无误」;确认后才写入 |\n| 拦截 | **前一日手工台账未上传今日禁止新增加氢记录**(编辑已有记录不拦;补传更早历史不替代日校验) |\n| 数据 | 共享 `H2VehicleLedgerBridge` 内存 Store未接真实 API;站名/编码解析见 Bridge.`h2BridgeResolveStationId` |\n\n## 判定顺序(新增拦截)\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 操作=编辑已有记录 | 不校验手工台账 |\n| 2 | 操作=新增,且昨日早于本站首笔加氢日(或不要求昨日台账) | 允许进入新增 |\n| 3 | 操作=新增,且目标站日已有 ≥1 张台账图 | 允许进入新增 |\n| 4 | 操作=新增,日未上传 | 顶部/Toast 提示「手工台账有缺失,作为对账重要依据,请先上传手工台账」,并**直接跳转**手工台账页且选中昨日 |\n\n## 日历与补传\n\n| 规则 | 说明 |\n|------|------|\n| 数据源 | `listManualLedgersByStation`;当日有 ≥1 张图视为已上传 |\n| 业务起点 | 取本站 Bridge 加氢记录中最早 `hydrogenTime` 自然日;无记录则历史日均不要求补传 |\n| 未传计数 | 本月内 `max(月初, 首笔日)`min(月末, 今日) 中未上传的天数 |\n| 点选 | 仅 `dateKey ≤ 今日`;选中后右侧/下方展示该日草稿图与上传按钮 |\n| 归档不可删 | Store 已有图强制保留;`upsertManualLedger` 仅追加新图,忽略删除已有图的请求 |\n| 与拦截关系 | 仅 **日** 影响「禁止新增」;历史补传解决核对缺口;今日台账仍可按日常上传,但不作为新增门禁 |\n\n## 用户可见\n\n- 今日状态双反馈H5 台账页顶部卡片 / Web 右侧「当日台账」):未上传「今日尚未上传…」异常态;已上传「今日已上传…」正常态。**日常上传反馈 ≠ 新增门禁**H5 文案须写明「今日不作为新增门禁,仅要求前一日已上传」 \n- 昨日缺失H5**列表顶栏可点橙条** + 底栏 FAB 旁提示 + Tab 红点;台账页同样展示橙条并可选中昨日;文案固定为「手工台账有缺失,作为对账重要依据,请先上传手工台账」 \n- 日历:自首笔加氢日起绿/橙标记;首笔之前无橙点(无需补传);本月未传天数摘要 \n- Web 右侧面板标题「手工台账」:空态提示;有图可点进全屏翻页预览并角标「已归档/待提交」;预览区内上传;底部确认前二次弹窗;提交后立即展示已归档缩略图;左右卡片等高\n\n## 代码路径\n\n- Bridge`src/common/h2VehicleLedgerBridge.js``hasManualLedgerForDate` / `isManualLedgerGateBlocked` / `upsertManualLedger` / `listManualLedgersByStation` \n- H5`oneos-h5-h2-order` · `ManualLedgerPanel` + `utils/manual-ledger.ts``isManualLedgerGateBlocked` / `buildManualMonthCells` / `getFirstHydrogenDateKey` \n- Web`oneos-web-h2-station/pages/02-加氢记录.jsx``hrIsManualLedgerGateBlocked` / 跳转选中昨日 \n\n## 验收\n\n1. 日未上传时H5/Web 列表顶栏提示固定文案;点「新增」被拦截并直接进入手工台账(选中昨日) \n2. 补传昨日台账并确认后,可新增加氢记录 \n3. 日历自首笔加氢日起区分已传/未传;首笔之前不标未传、不计入补传;可点选范围内未传日补传 \n4. 补传更早历史日、昨日仍未传时新增仍被拦截 \n5. 确认上传后的图显示已归档且不可删除;仅可追加补传 \n6. H5 与 Web 共用同一 Store一端上传另一端可见同会话 \n7. Web 导出 CSV 列含「车辆来源」(羚牛车辆/非羚牛车辆)、「数据来源」(站端上传/羚牛上传),与列表标签口径一致 \n",
"markdownPath": ".spec/feature-manual-ledger.md" "markdownPath": ".spec/feature-manual-ledger.md"
}, },
{ {
"type": "markdown", "type": "markdown",
"id": "h5-doc-detail", "id": "h5-doc-detail",
"title": "详情编辑删除", "title": "详情编辑删除",
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单预填当前值含已存水印照片提交走更新逻辑仍校验重复键。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n", "markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段在「未核对且未对账」时可编辑或删除**锁定仅适用于羚牛车辆**。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数`isOrderLocked`先判羚牛;再核「核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单预填当前值含已存水印照片提交走更新逻辑仍校验重复键。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n",
"markdownPath": ".spec/feature-detail.md" "markdownPath": ".spec/feature-detail.md"
}, },
{ {
"type": "markdown", "type": "markdown",
"id": "h5-doc-fleet", "id": "h5-doc-fleet",
"title": "羚牛车辆标签", "title": "羚牛车辆标签",
"markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定未接真实车辆管理 API。\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别OCR或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- `utils/fleet-plates.ts` 内本地种子车牌集合(含加氢 Bridge 演示车牌与部分车辆管理样例)。\n- 正式环境应改为查询车辆管理 / 系统车辆列表接口。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard` 车牌行右侧标签。\n", "markdown": "# 车牌「羚牛车辆 / 非羚牛车辆」标签\n\n原型本地判定未接真实车辆管理 API。\n\n全文(含核对/对账字段空展示规则,跨 Web / 台账 / H5 \n[../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md](../../oneos-web-h2-station/.spec/fleet-vehicle-verify.md)\n\n## 判定顺序\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | 车牌为空 | 不展示标签 |\n| 2 | 规范化后车牌 ∈ 本地系统车辆集合 | **羚牛车辆** |\n| 3 | 有车牌但不在集合中 | **非羚牛车辆** |\n\n## 前置条件\n\n- 识别OCR或键盘输入得到车牌后才展示。\n- 比较前去掉尾缀 `F`,统一大写。\n\n## 数据源\n\n- 优先 `H2VehicleLedgerBridge.isLingniuVehicle`;本地回退见 `utils/fleet-plates.ts`(与 Bridge `H2_FLEET_PLATE_KEYS` 同步)。\n\n## 用户可见结果\n\n- 绿色标签「羚牛车辆」或橙色标签「非羚牛车辆」。\n- **非羚牛车辆**:列表/详情中核对状态、核对时间、对账状态、对账时间显示「—」,不展示核对待办语义。\n- 不拦截提交(仅提示归属)。\n\n## 代码路径\n\n- `getVehicleTag` / `isLingniuVehicle` → `CreateWizard`、`OrderCard`、`OrderDetail`\n",
"markdownPath": ".spec/fleet-plate-tag.md" "markdownPath": ".spec/fleet-plate-tag.md"
}, },
{ {
"type": "markdown", "type": "markdown",
"id": "h5-doc-reconcile", "id": "h5-doc-reconcile",
"title": "核对与对账联动", "title": "核对与对账联动",
"markdown": "# 加氢订单H5· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRowverify=unverifiedreconcile=pending成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」\n```\n\n## 3. 用户可见结果H5\n\n| 结果 | 行为 |\n|---|---|\n| 未核对 | 可编辑、删除 |\n| 已核对未对账 | 只读;提示能源部已核对 |\n| 已对账 | 只读;提示已纳入对账单 |\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n", "markdown": "# 加氢订单H5· 对账 / 核对联动规则\n\n| 项 | 说明 |\n|---|---|\n| 代码路径 | Store`src/common/h2VehicleLedgerBridge.js`;对账单:`oneos-web-h2-station-site`PC 加氢记录:`oneos-web-h2-station`;车辆氢费明细:`vehicle-h2-fee-ledger` |\n| 规格全文 | [../vehicle-h2-fee-ledger/.spec/verify-reconcile.md](../vehicle-h2-fee-ledger/.spec/verify-reconcile.md) |\n| 车辆标签 | [fleet-plate-tag.md](./fleet-plate-tag.md) |\n| 数据源 | 原型本地种子 + 内存共享 Store**未接真实 API** |\n\n---\n\n## 1. 两套状态(判定优先级)\n\n| 优先级 | 状态 | 字段 | 谁触发 | 用户可见 |\n|---|---|---|---|---|\n| 1 | **核对** | `verifyStatus` / `verifiedAt` | 车辆氢费明细 · **完成核对** | 未核对 / 已核对 + 核对时间 |\n| 2 | **对账** | `reconcileStatus` / `reconcileDate` | 站点信息 · **对账单提交** | 未对账 / 已对账 + 对账时间 |\n\n> `reconciledAt` 仅作完成核对时的兼容写入,**不得**再用于判定「已对账」。\n\n---\n\n## 2. 数据流\n\n```text\n站端 H5 / Web 新增加氢记录\n → Bridge.upsertRowverify=unverifiedreconcile=pending成本单价/量/总额)\n → 车辆氢费明细可见\n\n能源部 · 完成核对\n → verifyStatus=verified + verifiedAt\n → 站端同步「已核对」;羚牛车辆禁改删\n\n站点信息 · 对账单提交(仅已核对 ∧ 未对账)\n → reconcileStatus=reconciled + reconcileDate + statementRecordId\n → 站端 / 台账同步「已对账」;羚牛车辆禁改删\n```\n\n---\n\n## 3. 用户可见结果H5\n\n| 结果 | 行为 |\n|---|---|\n| 非羚牛车辆 | 核对/对账展示「—」;**不锁定**改删 |\n| 羚牛 · 未核对 | 可编辑、删除 |\n| 羚牛 · 已核对未对账 | 只读;提示能源部已核对 |\n| 羚牛 · 已对账 | 只读;提示已纳入对账单 |\n\n---\n\n## 4. 边界\n\n- 站端上报默认未核对、未对账\n- **站端改删锁定仅适用于羚牛车辆**(与 Web / 台账车辆标签口径一致)\n- 对账时间只认 `reconcileDate`(对账单回写)\n- 跨页联动依赖同浏览器会话共享 Store\n",
"markdownPath": ".spec/reconcile-linkage.md" "markdownPath": ".spec/reconcile-linkage.md"
} }
] ]
@@ -433,7 +514,7 @@
"type": "folder", "type": "folder",
"id": "h5-doc-pages", "id": "h5-doc-pages",
"title": "按页面", "title": "按页面",
"defaultExpanded": true, "defaultExpanded": false,
"children": [ "children": [
{ {
"type": "folder", "type": "folder",
@@ -500,7 +581,7 @@
"type": "markdown", "type": "markdown",
"id": "h5-page-detail-md", "id": "h5-page-detail-md",
"title": "详情页要点", "title": "详情页要点",
"markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段在「未核对且未对账」时可编辑或删除。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n## 锁定规则\n\n| 条件 | 结果 |\n|------|------|\n| 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 未核对且未对账 | 可编辑、可删除 |\n\n判定函数`isOrderLocked`(核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单预填当前值含已存水印照片提交走更新逻辑仍校验重复键。\n\n## 删除\n\n二次确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 未核对未对账:底部有删除/编辑 \n2. 已核对或已对账:无操作栏,有锁定说明 \n3. 详情含对账信息;列表卡片不含对账 \n4. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n" "markdown": "# 功能:详情、编辑与删除\n\n## 目标\n\n查看单笔加氢记录完整字段在「未核对且未对账」时可编辑或删除**锁定仅适用于羚牛车辆**。\n\n## 详情展示\n\n- 顶部:车牌、加氢时间、核对标签、对账标签 \n- 指标条:加氢总额、加氢量 \n- **识别原图**:车牌照片、加氢机面板照片(均为拍摄时叠加时间/地点水印后的原图;种子数据无实拍时用演示水印图) \n- 明细行:站点、车牌、里程、单价、核对/对账状态与时间 \n\n> 非羚牛车辆:核对/对账字段展示「—」,不展示核对待办语义(见 [fleet-plate-tag.md](./fleet-plate-tag.md))。\n\n## 锁定规则\n\n| 优先级 | 条件 | 结果 |\n|--------|------|------|\n| 1 | **非羚牛车辆** | **不锁定**(不以核对/对账状态禁改删) |\n| 2 | 羚牛 ∧ 已对账 | 不可编辑/删除;提示已纳入站点对账单 |\n| 3 | 羚牛 ∧ 已核对(未对账) | 不可编辑/删除;提示能源部已核对 |\n| 4 | 羚牛 ∧ 未核对且未对账 | 可编辑、可删除 |\n\n判定函数`isOrderLocked`先判羚牛;再核「核对或对账任一成立即锁)。\n\n## 编辑\n\n进入与「新增」同一套表单预填当前值含已存水印照片提交走更新逻辑仍校验重复键。\n\n## 删除\n\n二次确认文案:「确认删除车牌 … 的未锁定记录?」;确认后从 Bridge 移除该行。\n\n## 验收\n\n1. 羚牛 ∧ 未核对未对账:底部有删除/编辑 \n2. 羚牛 ∧ 已核对或已对账:无操作栏,有锁定说明 \n3. 非羚牛:可改删(不因核对/对账锁定);核对/对账展示「—」 \n4. 详情含对账信息;列表卡片不含对账 \n5. 详情展示车牌与面板带水印原图(新上报提交后为实拍;种子行为演示图) \n"
} }
] ]
} }

Some files were not shown because too many files have changed in this diff Show More