OpenSpec实战:从零开始完整走一遍核心工作流

83 阅读14分钟

实战记录:通过 OpenSpec 完成一个油价监控小工具的全流程,重点记录每一步中文档的生成与演变过程。

项目目标: 开发一个油价监控小工具,从 showapi 获取北京、上海、广东、浙江的油价数据,存入本地 CSV,提供网页界面展示当前油价和历史趋势。

1.OpenSpec 核心命令

OpenSpec 是一个基于 AI 的变更管理系统,通过 CLI 命令驱动完整的工作流。以下是四个核心命令:

命令作用说明
/opsx:explore探索想法、调查问题纯思考模式,不写代码,用于厘清需求
/opsx:propose创建完整变更提案生成 proposal/spec/design/tasks 文档
/opsx:apply执行实现AI 按任务清单逐个完成代码,标记进度
/opsx:archive归档变更将完成后的规划文档移入 archive 目录

OpenSpec1.png

2. 项目初始化

2.1 初始目录结构

在开始之前,项目目录是这样的:

openspec-demo/
├── .claude/          # Claude Code 工作目录
├── openspec/         # OpenSpec 工作目录(初始为空)
│   └── (空)
└── 1.txt            # 需求文档(手动放置)

2.2 OpenSpec 工作目录结构

OpenSpec 在项目根目录下创建一个 openspec/ 目录,其中包含配置和变更管理文件:

openspec/
├── config.yaml       # 项目级配置,定义使用的 schema
└── changes/         # 所有变更(changes)存放目录
    └── archive/     # 已归档的变更

config.yaml 内容:

schema: spec-driven

# Project context(可选):添加技术栈、约定等背景信息
# 供 AI 在创建 artifacts 时参考
# Example:
#   context: |
#     Tech stack: Python, Flask
#     Domain: 个人工具

这个文件需要手动创建或由 OpenSpec CLI 初始化,定义了整个项目使用的 OpenSpec schema。


3. Explore 阶段:需求探索

3.1 启动 Explore

/opsx:explore

3.2 Explore 阶段做了什么

Explore 是一个自由思考阶段,不需要生成任何文档。它的核心目的是:

┌─────────────────────────────────────────────────────┐
│  Explore 阶段                                        │
│  - 提出问题,厘清模糊点                               │
│  - 画架构图,讨论方案                                 │
│  - 不生成任何文件                                     │
│  - 直到需求足够清晰,再进入 Propose                   │
└─────────────────────────────────────────────────────┘

本阶段澄清的关键问题:

问题最终答案
数据源showapi 138-46 接口(免费,支持按省份查询)
初始城市北京、上海、广东、浙江
油号显示全部显示(89#/90#/92#/95#/98#/0#),无数据显示"-"
删除确认需要弹出确认对话框
新城市数据无数据时自动触发当日获取
技术栈Flask + Bootstrap + ECharts + APScheduler
部署方式直接本地 Python 运行,不打包 Docker
查询方式串行间隔1秒,避免 QPS 限制

4. Propose 阶段:生成提案与规格文档

4.1 启动 Propose

/opsx:propose

4.2 OpenSpec 做了什么

第一步:创建变更目录

执行 openspec new change "oil-price-monitor" 后,OpenSpec 自动创建了以下结构:

openspec/changes/oil-price-monitor/
├── .openspec.yaml              # 自动生成,标记 schema 和创建时间
├── proposal.md                 # (待创建)
├── specs/                     # (待创建)
│   └── oil-price-monitor/
│       └── spec.md
├── design.md                  # (待创建)
└── tasks.md                   # (待创建)

.openspec.yaml 内容(自动生成):

schema: spec-driven
created: 2026-08-24

第二步:按依赖顺序创建 artifacts

OpenSpec 按照 schema 定义的依赖顺序依次创建文档:

proposal.md ──────┬──→ specs/oil-price-monitor/spec.md
                  │
                  └──→ design.md ──→ tasks.md

4.3 Artifact 1: proposal.md

文件位置: openspec/changes/oil-price-monitor/proposal.md

## Why
个人投资者需要一个简单、可靠的方式来监控主要城市的油价数据,
便于日常出行成本参考和简单的价格趋势观察。

## What Changes
- 新增油价监控小工具,支持查询和展示北京、上海、广东、浙江四地油价
- 支持动态增删监控城市(网页操作)
- 每日08:00自动从showapi获取最新油价数据
- 新增城市无数据时自动触发当日数据获取
- 数据存入本地CSV文件,支持历史趋势查看
- 提供ECharts折线图展示历史油价趋势
- 显示较昨日涨跌趋势
- 所有油号(92#/95#/98#/89#/90#/0#)全部展示,无数据显示"-"

## Capabilities

### New Capabilities
- `oil-price-monitor`: 油价数据获取、存储、展示全流程能力,
  包含数据模型、API接口、页面交互、定时任务等全部规格

## Impact
- 新增依赖: Flask, APScheduler, requests, Bootstrap, ECharts
- 数据存储: data/oil_prices.csv, config/cities.json
- 无外部数据库依赖,纯本地文件存储

Capabilities 是连接 proposal 和 spec 的桥梁。 这里声明了 oil-price-monitor 这个新能力,OpenSpec 据此在 specs/oil-price-monitor/ 下创建了对应的 spec.md 文件。

4.4 Artifact 2: specs/oil-price-monitor/spec.md

文件位置: openspec/changes/oil-price-monitor/specs/oil-price-monitor/spec.md

这是最核心的规格文档,包含所有行为规范。每个 Requirement 包含:

  • 规范描述(SHOULD/MUST 等关键词)
  • 多个 Scenario(WHEN...THEN 格式)
  • Scenario 是潜在的测试用例

本项目共创建了 6 个 Requirement:

Requirement 1: 城市配置管理

系统 SHALL 支持管理监控城市列表,包括添加城市和删除城市操作。

初始城市列表固定为:北京、上海、广东、浙江。

添加城市时,系统 SHALL 验证城市名是否已在列表中,若存在则返回错误。

删除城市时,系统 SHALL 弹出确认对话框,用户确认后方可删除。

#### Scenario: 成功添加新城市
- **WHEN** 用户输入一个不存在于列表中的城市名并提交
- **THEN** 系统 SHALL 将该城市添加到配置列表并返回成功

#### Scenario: 添加已存在城市
- **WHEN** 用户输入一个已存在于列表中的城市名并提交
- **THEN** 系统 SHALL 返回错误提示"该城市已在监控列表中"

#### Scenario: 删除城市
- **WHEN** 用户点击城市卡片上的删除按钮
- **THEN** 系统 SHALL 弹出确认对话框
- **AND** 用户点击确认后,系统 SHALL 从配置列表中移除该城市

Requirement 2: 油价数据获取

系统 SHALL 每日定时(08:00)自动从 showapi 油价接口获取各监控城市的最新油价数据。

showapi 接口地址为 https://route.showapi.com/138-46,
使用 POST/GET 方式请求,参数 prov 为省份/城市名。

获取时须串行逐个查询,避免并发请求导致 QPS 超限。

新增城市时若本地无当日数据,系统 SHALL 自动触发该城市当日数据获取。

#### Scenario: 定时任务自动获取数据
- **WHEN** 每日 08:00 定时任务触发
- **THEN** 系统 SHALL 串行查询 showapi 获取各城市油价
- **AND** 将数据写入/更新 data/oil_prices.csv

#### Scenario: 新增城市无数据时自动获取
- **WHEN** 用户添加新城市
- **AND** 本地无该城市当日数据
- **THEN** 系统 SHALL 立即调用 showapi 获取该城市当日数据

Requirement 3: 油价数据存储

系统 SHALL 将每日获取的油价数据存入本地 CSV 文件 data/oil_prices.csv。

CSV 文件每行记录包含:date(日期)、city(城市名)、
p89、p90、p92、p95、p98、p0(各油号价格)、update_time(数据更新时间)。

同一天同一城市的数据 SHALL 以最后一条为准(覆盖更新)。

城市配置 SHALL 存入 config/cities.json,
每条记录包含 name(城市名)和 code(showapi 查询用省份名)。

#### Scenario: 油价数据写入
- **WHEN** 系统成功从 showapi 获取某城市油价数据
- **THEN** 系统 SHALL 写入/更新 CSV 中该城市当日记录

#### Scenario: 数据覆盖
- **WHEN** 同一城市在同日有多条数据到达
- **THEN** 系统 SHALL 保留最后一条数据

Requirement 4: 当前油价展示

系统 SHALL 在网页上展示各城市当前油价,
包括全部油号:89#/90#/92#/95#/98#/0#。

无数据的油号 SHALL 显示"-"。

每个城市卡片 SHALL 显示:城市名、各油号价格、较昨日涨跌(趋势)。

#### Scenario: 展示城市油价卡片
- **WHEN** 用户打开首页
- **THEN** 系统 SHALL 显示各监控城市的油价卡片
- **AND** 各卡片显示该城市所有油号价格
- **AND** 无数据油号显示"-"

#### Scenario: 趋势显示
- **WHEN** 系统获取到某城市今日油价
- **AND** 昨日存在该城市数据
- **THEN** 系统 SHALL 计算并显示各油号较昨日涨跌
- **AND** 上涨显示"▲ +X.XX",下跌显示"▼ -X.XX",持平显示"— 0.00"

Requirement 5: 历史油价趋势图表

系统 SHALL 提供 ECharts 折线图展示历史油价趋势。

图表 SHALL 支持多城市叠加显示,用户可勾选显示哪些城市。

图表 X 轴为日期,Y 轴为价格。

用户 SHALL 可选择切换不同油号查看。

#### Scenario: 历史趋势折线图
- **WHEN** 用户打开历史趋势页面
- **THEN** 系统 SHALL 显示 ECharts 折线图
- **AND** 默认展示所有城市在所选油号的价格趋势

#### Scenario: 城市筛选
- **WHEN** 用户勾选/取消勾选城市复选框
- **THEN** 系统 SHALL 更新图表显示/隐藏对应城市曲线

#### Scenario: 油号切换
- **WHEN** 用户切换油号下拉选择
- **THEN** 系统 SHALL 更新图表数据为所选油号

Requirement 6: 手动刷新

系统 SHALL 提供手动刷新按钮,用户点击后可立即获取最新数据。

手动刷新 SHALL 串行查询所有监控城市。

#### Scenario: 手动刷新数据
- **WHEN** 用户点击"手动刷新"按钮
- **THEN** 系统 SHALL 串行查询 showapi 获取最新数据
- **AND** 更新页面显示

4.5 Artifact 3: design.md

文件位置: openspec/changes/oil-price-monitor/design.md

技术设计文档,解释怎么做:

## Context
个人用户需要一个轻量级油价监控工具...

## Goals / Non-Goals

**Goals:**
- 每日自动从 showapi 获取油价数据并存入 CSV
- 提供网页界面展示当前油价和历史趋势
- 支持动态增删监控城市
- 避免 showapi 免费版的 QPS 限制

**Non-Goals:**
- 不支持用户认证/多用户
- 不支持推送通知
- 不做成 Docker 部署
- 不支持复杂的自定义油号配置

## Decisions

### 技术栈
后端: Python 3.x + Flask
定时任务: APScheduler
HTTP请求: requests
前端: HTML + Bootstrap 5 + ECharts 5
数据存储: CSV (cities.json, oil_prices.csv)

### 目录结构
oil-price-monitor/
├── config/
│   └── cities.json          # 城市配置
├── data/
│   └── oil_prices.csv        # 每日油价历史数据
├── scripts/
│   └── fetch_price.py        # 独立数据抓取脚本
├── web/
│   ├── app.py                # Flask 主应用
│   └── templates/
│       └── index.html        # 主页面
├── requirements.txt
└── README.md

### API 设计
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/cities | 获取城市列表 + 最新油价 |
| POST | /api/cities | 添加城市 |
| DELETE | /api/cities/:name | 删除城市 |
| GET | /api/prices | 获取所有历史油价 |
| GET | /api/prices/:city | 获取指定城市历史油价 |
| POST | /api/refresh | 手动触发数据获取 |

### showapi 接口集成
接口: https://route.showapi.com/138-46
参数: prov = 省份/城市名
返回: { showapi_res_body: { list: [{ p89, p90, p92, p95, p98, p0, ct }] } }
appKey 配置在 config/app.json 中

### 定时任务
使用 APScheduler 在 Flask 应用启动时注册定时任务:
scheduler.add_job(func=fetch_all_cities, trigger='cron', hour=8, minute=0)

### 串行查询策略
for city in cities:
    data = fetch_price(city['code'])
    save_to_csv(data)
    time.sleep(1)  # 避免QPS限制

## Risks / Trade-offs

| 风险 | 应对 |
|------|------|
| showapi 免费接口不稳定或限额 | 手动刷新时给出友好错误提示;数据已持久化到 CSV |
| CSV 并发写入冲突 | 定时任务和手动刷新共用同一写入逻辑 |
| 首次运行无历史数据 | 图表显示"暂无数据"而非报错 |

4.6 Artifact 4: tasks.md

文件位置: openspec/changes/oil-price-monitor/tasks.md

实现任务清单,用于追踪实现进度:

## 1. 项目初始化
- [x] 1.1 创建目录结构(config/、data/、scripts/、web/templates/)并验证目录存在
- [x] 1.2 创建 requirements.txt 包含 Flask、APScheduler、requests 依赖
- [x] 1.3 创建 config/cities.json 初始化4个城市配置
- [x] 1.4 创建 config/app.json 示例(appKey 留空),验证 JSON 格式正确

## 2. 数据获取模块
- [x] 2.1 实现 showapi 接口调用函数 fetch_price(prov)
- [x] 2.2 实现 CSV 读写工具函数
- [x] 2.3 实现串行获取所有城市数据函数 fetch_all_cities()
- [x] 2.4 实现新增城市时自动获取当日数据逻辑

## 3. Flask Web API
- [x] 3.1 实现 GET /api/cities 接口
- [x] 3.2 实现 POST /api/cities(添加城市)
- [x] 3.3 实现 DELETE /api/cities/:name(删除城市)
- [x] 3.4 实现 GET /api/prices 接口
- [x] 3.5 实现 GET /api/prices/:city 接口
- [x] 3.6 实现 POST /api/refresh 接口

## 4. 定时任务
- [x] 4.1 集成 APScheduler,注册每日08:00定时任务
- [x] 4.2 验证定时任务执行后 CSV 数据更新

## 5. 前端页面
- [x] 5.1 创建 index.html 基础结构(Bootstrap CDN)
- [x] 5.2 实现城市卡片展示
- [x] 5.3 实现删除城市确认对话框
- [x] 5.4 实现"添加城市"对话框
- [x] 5.5 实现手动刷新按钮
- [x] 5.6 集成 ECharts 折线图
- [x] 5.7 实现城市复选框筛选
- [x] 5.8 实现油号下拉切换

## 6. 集成验证
- [x] 6.1 运行 python web/app.py 验证应用启动无报错
- [x] 6.2 浏览器访问验证页面加载正常
- [x] 6.3 验证首次运行无历史数据时图表显示"暂无数据"
- [x] 6.4 验证添加新城市后页面立即显示该城市卡片
- [x] 6.5 验证删除城市后页面更新,CSV 和 cities.json 同步

4.7 Propose 阶段的完整文件树

openspec/changes/oil-price-monitor/
├── .openspec.yaml           # schema: spec-driven, created: 2026-08-24
├── proposal.md              # Why + What + Capabilities + Impact
├── specs/
│   └── oil-price-monitor/
│       └── spec.md          # 6个 Requirement + Scenario
├── design.md                # 技术栈 + 目录结构 + API设计 + 风险
└── tasks.md                 # 6个阶段29个任务清单

4.8 查看提案状态

openspec status --change "oil-price-monitor" --json

返回 JSON 中可以看到:

  • isPlanningComplete: true — 所有规划产物已完成
  • artifacts[].status — 每个 artifact 的状态(done/ready/blocked)

5. Apply 阶段:实现代码

5.1 启动 Apply

/opsx:apply

5.2 Apply 做了什么

AI 按照 tasks.md 的清单,逐个完成任务,每完成一个任务就把 - [ ] 改成 - [x]。

5.3 实现代码结构

oil-price-monitor/                          # 项目代码目录(不在 openspec/ 下)
├── config/
│   ├── app.json          # {"appKey": "..."}  用户填入
│   └── cities.json       # 初始4个城市配置
├── data/
│   └── oil_prices.csv    # 每日油价数据(自动生成)
├── scripts/
│   └── fetch_price.py    # 数据获取模块
├── web/
│   ├── app.py            # Flask 主应用 + APScheduler
│   └── templates/
│       └── index.html     # 前端页面(Bootstrap + ECharts)
├── requirements.txt
└── README.md

6. Archive 阶段:归档变更

6.1 启动 Archive

/opsx:archive

6.2 Archive 检查了什么

  1. Artifact 完成状态 — 所有 4 个 artifacts(proposal/specs/design/tasks)都是 done
  2. 任务完成状态 — 所有 29 个任务都标记为 [x]
  3. Delta Spec 同步 — 由于是全新能力(oil-price-monitor),没有历史主 spec 需要同步
  4. Archive 目标路径检查 — 确认 archive/2026-08-24-oil-price-monitor/ 不存在

6.3 执行归档

mkdir -p openspec/changes/archive
mv openspec/changes/oil-price-monitor \
   openspec/changes/archive/2026-08-24-oil-price-monitor

6.4 归档后的完整目录结构

openspec-demo/
├── openspec/
│   ├── config.yaml                    # 项目级配置
│   └── changes/
│       └── archive/
│           └── 2026-08-24-oil-price-monitor/
│               ├── .openspec.yaml    # 归档标记(schema + created)
│               ├── proposal.md
│               ├── specs/
│               │   └── oil-price-monitor/
│               │       └── spec.md  # 6个 Requirement + Scenario
│               ├── design.md
│               └── tasks.md
│
└── oil-price-monitor/                 # 项目实现代码(不在 openspec/ 下)
    ├── config/
    ├── data/
    ├── scripts/
    ├── web/
    ├── requirements.txt
    └── README.md

7. 完整目录结构

7.1 全部文件一览

openspec-demo/
│
├── 1.txt                              # 原始需求
│
├── openspec/                          # OpenSpec 工作目录
│   ├── config.yaml                     # 项目配置(定义 schema)
│   └── changes/
│       └── archive/                    # 归档的变更
│           └── 2026-08-24-oil-price-monitor/
│               ├── .openspec.yaml      # schema: spec-driven
│               ├── proposal.md         # 提案:Why + What + Capabilities
│               ├── specs/              # 规格文档
│               │   └── oil-price-monitor/
│               │       └── spec.md    # 6个 Requirement + Scenario
│               ├── design.md          # 技术设计
│               └── tasks.md           # 任务清单(29项全部[x])
│
└── oil-price-monitor/                  # 项目实现代码
    ├── config/
    │   ├── app.json                   # showapi appKey(用户填入)
    │   └── cities.json                # 城市配置
    ├── data/
    │   └── oil_prices.csv             # 油价历史数据
    ├── scripts/
    │   └── fetch_price.py             # 数据获取模块
    ├── web/
    │   ├── app.py                     # Flask + APScheduler
    │   └── templates/
    │       └── index.html             # Bootstrap + ECharts 前端
    ├── requirements.txt
    └── README.md

7.2 OpenSpec 工作流命令速查

阶段命令作用
探索/opsx:explore自由讨论,厘清需求,不生成文件
提案/opsx:propose创建变更,生成 proposal/spec/design/tasks
实现/opsx:apply按 tasks.md 执行实现,标记完成
归档/opsx:archive归档变更到 archive/ 目录
常用查询命令作用
openspec list --json列出所有变更
openspec status --change "<name>" --json查看变更状态(详细JSON)
openspec status --change "<name>"简洁状态视图

8. 关键心得

8.1 OpenSpec 是一个「变更管理系统」

OpenSpec 的核心价值在于:把每个项目变更当作一个独立的、可追溯的单元来管理。

变更生命周期:
Propose(规划) → Apply(实现) → Archive(归档)
     ↓                ↓
  产出 artifacts   标记 tasks 完成

8.2 proposal.md 是整个系统的入口

proposal.md 中的 Capabilities 部分决定了后续会创建哪些 spec 文件。

proposal.md 中的 Capabilities
    │
    ▼
specs/<capability>/spec.md   ← 自动创建

8.3 spec.md 是行为契约

规格文档描述的是外部可观察的行为,不是内部实现细节。

好的 spec:

  • 包含 WHEN...THEN 格式的 Scenario
  • 每个 Requirement 有明确的 SHALL/MUST 约束
  • Scenario 是潜在的测试用例

8.4 design.md 和 tasks.md 的区别

  • design.md — 解决「怎么做」的问题,技术决策和架构
  • tasks.md — 解决「做什么」的问题,任务清单和进度追踪

8.5 实现代码和 OpenSpec 文档是分离的

实现代码 → oil-price-monitor/        (项目本身)
规划文档 → openspec/changes/xxx/     (OpenSpec 管理)

这样做的好处是:项目代码本身保持干净,OpenSpec 的元数据不影响项目结构。

8.6 Archive 不删除实现代码

归档的是 openspec/changes/ 下的规划文档,不是 oil-price-monitor/ 里的实现代码。实现代码独立存在于项目中,随时可以运行。

8.7 下次如何基于归档重新开始

如果想查看某个归档变更的详情:

openspec list --json
# 查看 openspec/changes/archive/2026-08-24-oil-price-monitor/ 下的文档

归档目录包含完整的规划文档,可以随时回溯查看当时的设计决策。