实战记录:通过 OpenSpec 完成一个油价监控小工具的全流程,重点记录每一步中文档的生成与演变过程。
项目目标: 开发一个油价监控小工具,从 showapi 获取北京、上海、广东、浙江的油价数据,存入本地 CSV,提供网页界面展示当前油价和历史趋势。
1.OpenSpec 核心命令
OpenSpec 是一个基于 AI 的变更管理系统,通过 CLI 命令驱动完整的工作流。以下是四个核心命令:
| 命令 | 作用 | 说明 |
|---|---|---|
/opsx:explore | 探索想法、调查问题 | 纯思考模式,不写代码,用于厘清需求 |
/opsx:propose | 创建完整变更提案 | 生成 proposal/spec/design/tasks 文档 |
/opsx:apply | 执行实现 | AI 按任务清单逐个完成代码,标记进度 |
/opsx:archive | 归档变更 | 将完成后的规划文档移入 archive 目录 |
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 检查了什么
- Artifact 完成状态 — 所有 4 个 artifacts(proposal/specs/design/tasks)都是
done - 任务完成状态 — 所有 29 个任务都标记为
[x] - Delta Spec 同步 — 由于是全新能力(
oil-price-monitor),没有历史主 spec 需要同步 - 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/ 下的文档
归档目录包含完整的规划文档,可以随时回溯查看当时的设计决策。