汽车保养服务计划表规划器
来自 Wikiprompt,自由的提示词百科全书
汽车保养服务计划表规划器 一个完整的Claude风格技能,包含结构化提示、参考文档、模板、示例,以及一个Python标准库脚本,用于通过跟踪保养间隔和历史记录、整合工作行程并预测成本来规划汽车保养。
提示词内容收藏
🌐
---
name: car-maintenance-service-schedule-planner
description: 根据保养计划和服务历史规划汽车保养 - 根据里程表读数估算行驶距离,计算哪些项目已逾期、即将到期或下次到期(按距离或时间,以先到者为准),将项目合并为更少的维修厂到访次数,并预测每月预留费用。标记逾期的安全和法律项目、缺失记录和里程表笔误。包含一个离线的标准库 Python 规划器。
---
# 汽车保养服务计划规划器
您帮助车主跟上保养进度,同时避免过度支付。人们容易失去头绪,因为每个项目都有自己的时间表:机油每 15,000 公里或 12 个月,制动液每 2 年(无论距离),年检每年一次,正时皮带在 120,000 公里或 8 年。总有些项目即将到期,收据散落各处,昂贵的项目总是突然出现。您将车主的保养计划和服务历史转化为清晰的到期项目列表、一个将项目分组为尽可能少的维修厂到访次数的简短到访计划,以及他们可以预算的成本预测。
您根据车主提供的保养间隔(来自车主手册、保养手册或经销商)进行规划。您不诊断故障或决定零件是否磨损;告诉用户遵循维修厂的发现和制造商的计划,并立即检查警告灯、噪音、制动或转向问题。
## 此技能中的文件
- `scripts/car_service_plan.py` - 读取汽车 JSON(车辆、当前里程表、带有公里和/或月间隔的计划项目、严苛服务间隔、首次测试延迟、成本、安全/法律标志、名称别名)和服务日志 CSV(日期、里程表、以 ";" 分隔的项目、成本、维修厂);根据读数估算年行驶距离,查找每个项目上次完成的时间,计算下次到期日期(按时间和预计距离),状态为 已逾期 / 即将到期 / 正常,在时间范围内捆绑维修厂到访次数和每季度成本;报告发现 CS01-CS09(仅限 Python 3.8+ 标准库)
- `references/intervals-and-severe-service.md` - 如何阅读制造商计划、时间与距离、严苛服务条件、典型项目说明以及绝不能推迟的事项
- `references/checks-and-codes.md` - 每个发现代码、其严重性及处理方法,以及输入字段
- `templates/service-plan.md` - 一页计划及带去维修厂的检查清单
- `examples/car.json` - 一辆 2019 年汽油旅行车,行驶 83,400 公里,有 11 个计划项目(说明性间隔和价格 - 请替换为您手册中的内容)
- `examples/service-log.csv` - 七年的服务历史,包含合并到访、一个别名("Oil change")、一个不在计划中的项目和一个里程表笔误
- `examples/service-plan.md` - 示例上的实际规划器输出
## 工作流程
### 1. 收集计划和服务历史
询问品牌、型号、发动机和首次注册日期、今天的里程表读数,以及车主手册或保养手册中的保养计划(项目、距离间隔、时间间隔、严苛服务间隔(如果他们大多短途行驶、拖车或在灰尘或极热或极冷环境中行驶))。询问服务历史:发票、服务手册中的印章或经销商的打印输出,包含日期、里程表和已完成的项目。从他们维修厂的最后发票中添加大致价格。将它们写入类似 `examples/car.json` 的汽车 JSON 和类似 `examples/service-log.csv` 的日志中。
### 2. 运行规划器
```
python3 scripts/car_service_plan.py examples/car.json examples/service-log.csv
python3 scripts/car_service_plan.py car.json log.csv --odometer 84100 --today 2026-11-20
python3 scripts/car_service_plan.py car.json log.csv --severe --horizon 36
python3 scripts/car_service_plan.py car.json log.csv --json
python3 scripts/car_service_plan.py --self-test
```
退出码 0 = 没有逾期的安全或法律项目,1 = 安全/法律项目已逾期(或日志中有高严重性发现),2 = 输入错误。如果没有 shell,对于每个项目,取最后日期和里程表,加上两个间隔,用年行驶距离将距离转换为日期,并取较早者。
### 3. 阅读结果
- 首先处理高严重性:CS01 高(制动、制动液、轮胎、法律测试逾期)- 小心驾驶并立即预订;CS04 高(日期在未来,读数高于今天的读数)- 修复日志。
- 中严重性:其他逾期项目、即将到期的项目、无记录的项目(CS03 - 从新车开始计算,因此在支付前检查服务手册)和里程表读数倒退。
- 低/信息:计划内的大额项目(CS08)、远早于间隔完成的项目(CS05)、不在计划中的日志条目、正在使用的严苛间隔。
- 如果年行驶距离看起来不对,检查读数;使用 `--odometer` 和今天的读数。
### 4. 计划到访
使用到访表:为所有逾期项目以及捆绑窗口内到期的项目预订第 1 次到访,并要求维修厂一次性报价整个列表。如果节省一次行程,可将廉价项目提前几周,但切勿将安全或法律项目推迟。用到访、维修厂检查清单和每月预留金额填写 `templates/service-plan.md`。
### 5. 交付
用通俗语言返回逾期和即将到期列表、带日期和预计成本的到访计划、12 个月和 24 个月成本预测以及每月预留金额,以及需要检查的问题(无历史记录的项目的记录、其发动机的确切间隔、需要经销商保养的保修条件)。
## 示例结果
对于 2019 款 Octavia,行驶 83,400 公里(约每年 11,000 公里),有四个项目逾期:制动液(126 天,安全)、检查服务和空气滤清器(按时间)和轮胎换位(3,300 公里),道路适航性测试于 2026 年 11 月 20 日到期 - 一次到访即可覆盖全部五项,约 410 欧元。没有正时皮带的记录,因此从新车开始计算,按车龄于 2027 年 11 月到期(约 680 欧元),新轮胎预计于 2027 年 6 月更换(约 560 欧元)。未来 24 个月的保养费用约为 2,445 欧元,因此每月预留约 102 欧元。2023 年 9 月的 4,790 公里读数是笔误,已忽略。
## 规则
- 使用制造商针对确切发动机的间隔;示例值仅为说明性。
- 以先到者为准:项目在其距离和时间限制中较早者到期,即使汽车很少行驶。
- 切勿建议推迟制动、轮胎、转向、警告灯或法律测试以节省金钱或行程。
- 无记录的项目是未知的,而非已完成:说明这一点并建议检查服务手册或询问维修厂。
- 成本为估算值;告诉用户获取报价,并在建议独立维修厂之前提及保修条件。
## 间隔和严苛服务
## 间隔来源
- 针对您的确切型号、发动机和车型年份的车主手册或保养手册。同一型号的汽油、柴油和混合动力发动机,以及"固定"与"灵活/长效"服务制度,可能有不同的计划。
- 许多汽车在仪表盘上显示下次机油服务(服务指示器)。如果与手册不一致,请询问经销商车辆设置的是哪种制度。
- 牢记保修条件:一些制造商要求按照其计划使用正品规格零件进行保养;独立维修厂通常没问题,只要他们遵循计划并保留发票。
## 以先到者为准
每个同时具有距离和时间间隔的项目在其两个限制中较早者到期。低里程汽车达到时间限制(机油老化、制动液吸水、橡胶皮带和轮胎老化);高里程汽车达到距离限制。规划器用您的年行驶距离将距离限制投影到日期。
## 严苛服务
大多数手册在车辆主要用于以下情况时列出更短的间隔:
- 短途行驶(约 10 公里以下,发动机很少完全热),尤其是在寒冷天气;
- 走走停停的城市交通、出租车或送货使用、长时间怠速;
- 拖曳拖车或房车、车顶负载、山区驾驶;
- 多尘或沙质道路、非常热或非常冷的气候。
如果这些情况中的任何一种描述了您的大部分驾驶,请使用 `--severe`(或 `"severe": true`),以便具有 `severe_km` / `severe_months` 的项目使用更短的间隔。
## 项目说明(典型,请检查您的手册)
| 项目 | 典型依据 | 说明 |
|---|---|---|
| 发动机机油和滤清器 | 距离和时间 | 最重要的常规项目;使用手册中的机油规格 |
| 检查服务 | 距离和时间 | 更大的服务;通常包括许多检查 |
| 制动液 | 仅时间(通常 2 年) | 吸水;切勿推迟 |
| 制动检查 / 刹车片 / 刹车盘 | 距离和时间 | 按磨损更换,而非固定间隔 |
| 冷却液 | 时间或"终身" | 每年冬天检查液位和防冻保护 |
| 空气滤清器、空调滤清器 | 距离和时间 | 在多尘或污染地区更短 |
| 火花塞(汽油) | 距离和时间 | 现代发动机间隔长 |
| 正时皮带(如配备) | 距离和时间 | 故障可能毁坏发动机;通常与水泵一起更换;链条通常无间隔 |
| 轮胎 | 磨损和车龄 | 每月检查胎纹深度;许多制造商建议 6-10 年更换,无论胎纹如何 |
| 轮胎换位 | 距离 | 如果您的手册推荐(不适用于前后轮胎尺寸不同的情况) |
| 道路适航性测试 | 时间(法律) | 首次测试在几年后(例如 3 或 4 年),然后根据国家每 1-2 年一次 |
| 12 V 电池 | 状况 | 4-5 年后在冬季前测试 |
## 法律测试
规则因国家而异(英国 MOT、德国 TUV/HU、法国 CT、土耳其 TUVTURK、美国州检查)。输入您国家的首次测试延迟为 `first_months`,重复间隔为 `months`,并将项目标记为 `"legal": true`。许多方案允许您提前一点测试而不丢失日期;在将其捆绑到较早到访之前检查您的情况。
## 绝不能推迟的事项
制动和制动液、轮胎(胎纹、损坏、车龄)、转向和悬架故障、警告灯(机油压力、制动、发动机温度)以及法律测试。规划器将这些标记为 `safety` / `legal`,并在逾期时将其视为高严重性。
## 获取输入
- 每次到访的里程表和日期:发票、服务手册印章、经销商数字服务记录、法律测试证书(它们记录里程表)。
- 价格:您最后的发票或报价;为通常同时更换的零件(雨刷、灯泡、刹车片)增加 10-20%。
## 检查和发现代码
| 代码 | 严重性 | 检查 | 处理方法 |
|---|---|---|---|
| CS01 | 高:安全或法律项目逾期;中:其他项目逾期 | 到期日期或到期距离已过 | 预订;对于安全项目,在完成前小心驾驶 |
| CS02 | 中 | 在 `due_soon.days` 或 `due_soon.km` 内到期 | 与下次到访一起预订 |
| CS03 | 中 | 日志中无该项目记录:从首次注册开始计算 | 在支付前检查服务手册 / 收据 / 经销商记录 |
| CS04 | 高:日期在未来,读数高于当前里程表;中:读数低于较早读数,里程表不可读 | 日志一致性 | 修复笔误;读数在距离和到期计算中被忽略 |
| CS05 | 信息 | 上次完成时间低于两个间隔的 60% | 如果是有意的(严苛使用、保修)则正常;否则钱花早了 |
| CS06 | 信息 | 正在使用严苛服务间隔 | 检查条件是否仍然适用 |
| CS07 | 信息 | 日志项目不在您的计划中(维修、额外项目) | 保留用于历史;如果它是另一个名称下的计划项目,添加别名 |
| CS08 | 低 | 成本 400 或以上的项目落在计划范围内 | 为其预算;尽早获取报价 |
| CS09 | 中 | 里程表读数不足(至少 60 天前的读数):使用默认年行驶距离 | 添加读数(测试证书、燃油应用、仪表盘照片) |
## 结论
- **逾期包括安全/法律**:立即预订。
- **逾期**:尽快预订。
- **即将到期** / **最新**。
## 数字计算方式
- 年行驶距离:从今天前至少一年的最新日志读数到今天的读数(如果没有那么旧的读数,则取最早的读数)。
- 下次到期:上次完成日期 + 月数,以及上次完成里程表 + 公里数,用您的每日距离转换为日期;较早者获胜。无记录的项目从首次注册 0 公里开始计算(`first_months` 替换首次的 `months`,例如首次法律测试在 36 个月后)。
- 范围:每个项目从假定完成日期(其到期日期,或如果逾期则为今天)重复。
- 到访:在某个到访的第一个项目的 `bundle.days` 和 `bundle.km` 内到期的项目加入该到访(提前,绝不推迟)。
- 成本:每次发生的 `cost` 添加到其季度;每月预留 = 范围总计 / 范围月数。
## 汽车 JSON 键
`vehicle{name, first_registered, unit}, odometer{reading, date}, driving{default_per_year, severe, severe_reasons}, currency, horizon_months, due_soon{km, days}, bundle{km, days}, schedule[{item, km, months, severe_km, severe_months, first_months, cost, safety, legal, aliases[]}]`。
## 服务日志 CSV
`date, odometer, item, cost, shop, notes` - 一次到访中的多个项目以 `;` 分隔。名称与计划项目和别名不区分大小写匹配。成本列用于您的记录;预测使用计划价格。
## 模板
# 汽车保养计划:<汽车,注册>
**日期:** <YYYY-MM-DD> **里程表:** <83,400 公里> **年行驶距离:** <约 11,000 公里> **计划:** <车主手册,正常 / 严苛服务>
## 立即执行
| 项目 | 原因 | 预计成本 |
|---|---|---|
| <制动液> | <逾期 4 个月(2 年间隔)- 安全> | <85> |
| <道路适航性测试> | <2026-11-20 到期 - 法律> | <60> |
## 下次到访
| 时间(预订截止) | 大约在 | 项目 | 预计成本 |
|---|---|---|---|
| <2027-02-14> | <86,500 公里> | <机油和滤清器,制动检查> | <180> |
## 预算
- 未来 12 个月:<1,220> <欧元>
- 未来 24 个月:<2,445> <欧元> -> 每月预留 <102>
- 未来大额项目:<正时皮带套件和水泵,2027 年 11 月,约 680>
## 需要检查
- [ ] <无正时皮带记录 - 检查服务手册 / 询问经销商>
- [ ] <此发动机的确切制动液间隔>
- [ ] <保修:是否要求经销商保养?>
## 维修厂检查清单
- 汽车:<品牌,型号,发动机,VIN(如询问)>,里程表 <...>
- 请执行:<来自"立即执行"的列表>
- 请在更换前检查并报价:<刹车片/刹车盘,轮胎,雨刷,电池>
- 请在服务手册上盖章 / 发送数字记录并在发票上注明里程表。
*间隔来自 <来源>;成本为估算值。立即检查任何异常情况(警告灯、噪音、跑偏、振动)。*
## 示例:car.json
{
"vehicle": {"name": "2019 Skoda Octavia Estate 1.5 TSI (petrol)", "first_registered": "2019-11-14", "unit": "km"},
"odometer": {"reading": 83400, "date": "2026-11-04"},
"driving": {"default_per_year": 12000, "severe": false, "severe_reasons": ["mostly short trips in winter", "occasional caravan towing"]},
"currency": "EUR",
"horizon_months": 24,
"due_soon": {"km": 1500, "days": 45},
"bundle": {"km": 2500, "days": 60},
"schedule": [
{"item": "Engine oil and filter", "km": 15000, "months": 12, "severe_km": 7500, "severe_months": 6, "cost": 140, "aliases": ["oil service", "oil change"]},
{"item": "Inspection service", "km": 30000, "months": 24, "severe_km": 20000, "cost": 190, "aliases": ["inspection"]},
{"item": "Brake inspection", "km": 20000, "months": 12, "cost": 40, "safety": true},
{"item": "Brake fluid", "months": 24, "cost": 85, "safety": true},
{"item": "Air filter", "km": 60000, "months": 48, "severe_km": 30000, "cost": 45},
{"item": "Cabin filter", "km": 30000, "months": 24, "cost": 40, "aliases": ["pollen filter"]},
{"item": "Spark plugs", "km": 60000, "months": 72, "cost": 160},
{"item": "Tyre rotation", "km": 10000, "cost": 30},
{"item": "Tyres (set of 4)", "km": 45000, "months": 72, "cost": 560, "safety": true},
{"item": "Timing belt kit and water pump", "km": 120000, "months": 96, "cost": 680},
{"item": "Roadworthiness test", "months": 12, "first_months": 36, "cost": 60, "legal": true, "aliases": ["mot", "periodic inspection"]}
]
}
## 示例:service-log.csv
date,odometer,item,cost,shop,notes
2020-11-20,11500,Oil service; Brake inspection,165,Main dealer,first service
2021-11-05,22800,Oil service; Brake inspection; Cabin filter,210,Main dealer,
2022-05-28,33900,Oil service; Inspection service; Brake fluid; Air filter,395,Main dealer,
2022-11-08,38000,Roadworthiness test,55,Test centre,passed
2023-06-15,45150,Oil service; Brake inspection; Tyre rotation; Tyres (set of 4),735,QuickFit,new tyres 205/55 R16
2023-09-02,4790,Windscreen chip repair,60,GlassFix,
2023-11-13,49000,Roadworthiness test,55,Test centre,passed
2024-07-01,56900,Oil service; Inspection service; Brake fluid; Spark plugs; Brake inspection,520,Main dealer,
2024-11-12,59800,Roadworthiness test,58,Test centre,advisory: rear wiper
2025-08-20,70100,Oil service; Brake inspection; Cabin filter; Tyre rotation,240,Corner Garage,
2025-11-20,73000,Roadworthiness test,60,Test centre,passed
2026-02-14,75300,Oil change; Brake inspection,150,Corner Garage,dash light came on
## 示例:service-plan.md
# 服务计划:2019 Skoda Octavia Estate 1.5 TSI (petrol)
今天 2026-11-04,里程表 83,400 公里。您每年行驶约 11,008 公里(2025-08-20 70,100 -> 2026-11-04 83,400)。正常服务间隔,计划范围 24 个月。来源:examples/service-log.csv。
**结论:4 个项目逾期,包括安全/法律项目 - 立即预订到访**
未来 12 个月:约 1,220.00 欧元;未来 24 个月:2,445.00 欧元(每月预留约 101.88 欧元)。发现:1 高,6 中,2 低,3 信息。
## 项目
| 项目 | 间隔 | 上次完成 | 到期日期 | 到期公里数 | 下次(预计) | 触发 | 状态 | 预计成本 |
|---|---|---|---|---|---|---|---|---|
| Inspection service | 30,000 公里 / 24 个月 | 2024-07-01 在 56,900 | 2026-07-01 | 86,900 | 2026-11-04 | 时间 | **已逾期** | 190.00 |
| Brake fluid [安全] | 24 个月 | 2024-07-01 在 56,900 | 2026-07-01 | - | 2026-11-04 | 时间 | **已逾期** | 85.00 |
| Air filter | 60,000 公里 / 48 个月 | 2022-05-28 在 33,900 | 2026-05-28 | 93,900 | 2026-11-04 | 时间 | **已逾期** | 45.00 |
| Tyre rotation | 10,000 公里 | 2025-08-20 在 70,100 | - | 80,100 | 2026-11-04 | 距离 | **已逾期** | 30.00 |
| Roadworthiness test [法律] | 12 个月 | 2025-11-20 在 73,000 | 2026-11-20 | - | 2026-11-20 | 时间 | **即将到期** | 60.00 |
| Engine oil and filter | 15,000 公里 / 12 个月 | 2026-02-14 在 75,300 | 2027-02-14 | 90,300 | 2027-02-14 | 时间 | 正常 | 140.00 |
| Brake inspection [安全] | 20,000 公里 / 12 个月 | 2026-02-14 在 75,300 | 2027-02-14 | 95,300 | 2027-02-14 | 时间 | 正常 | 40.00 |
| Tyres (set of 4) [安全] | 45,000 公里 / 72 个月 | 2023-06-15 在 45,150 | 2029-06-15 | 90,150 | 2027-06-15 | 距离 | 正常 | 560.00 |
| Cabin filter | 30,000 公里 / 24 个月 | 2025-08-20 在 70,100 | 2027-08-20 | 100,100 | 2027-08-20 | 时间 | 正常 | 40.00 |
| Timing belt kit and water pump | 120,000 公里 / 96 个月 | 无记录(从新车开始计算,2019-11-14) | 2027-11-14 | 120,000 | 2027-11-14 | 时间 | 正常 | 680.00 |
| Spark plugs | 60,000 公里 / 72 个月 | 2024-07-01 在 56,900 | 2030-07-01 | 116,900 | 2029-11-18 | 距离 | 正常 | 160.00 |
## 维修厂到访(60 天 / 2,500 公里内的项目捆绑)
| # | 预订截止 | 大约在 | 项目 | 预计成本 |
|---|---|---|---|---|
| 1 | 2026-11-04(现在) | 83,400 公里 | Air filter(已逾期),Brake fluid(已逾期),Inspection service(已逾期),Tyre rotation(已逾期),Roadworthiness test | 410.00 |
| 2 | 2027-02-14 | 86,476 公里 | Brake inspection,Engine oil and filter | 180.00 |
| 3 | 2027-06-15 | 90,125 公里 | Tyres (set of 4) | 560.00 |
| 4 | 2027-08-20 | 92,116 公里 | Cabin filter,Tyre rotation | 70.00 |
| 5 | 2027-11-14 | 94,710 公里 | Timing belt kit and water pump,Roadworthiness test | 740.00 |
| 6 | 2028-02-14 | 97,484 公里 | Brake inspection,Engine oil and filter | 180.00 |
| 7 | 2028-08-27 | 103,365 公里 | Tyre rotation | 30.00 |
| 8 | 2028-11-04 | 105,446 公里 | Brake fluid,Inspection service | 275.00 |
## 按季度成本预测
| 季度 | 预计成本 |
|---|---|
| 2026-Q4 | 410.00 |
| 2027-Q1 | 180.00 |
| 2027-Q2 | 560.00 |
| 2027-Q3 | 40.00 |
| 2027-Q4 | 770.00 |
| 2028-Q1 | 180.00 |
| 2028-Q3 | 30.00 |
| 2028-Q4 | 275.00 |
| **24 个月总计** | **2,445.00** |
## 发现
- **高 CS01** Brake fluid 逾期 126 天(安全项目)- 立即预订
- **中 CS01** Air filter 逾期 160 天 - 立即预订
- **中 CS01** Inspection service 逾期 126 天 - 立即预订
- **中 CS01** Tyre rotation 逾期 3,300 公里 - 立即预订
- **中 CS02** Roadworthiness test 即将到期:2026-11-20
- **中 CS03** Timing belt kit and water pump:您的日志中无记录 - 从新车开始计算;检查服务手册或收据
- **中 CS04** 第 7 行:2023-09-02 的里程表 4,790 低于 2023-06-15 的 45,150 - 笔误或错误的汽车?读数已忽略
- **低 CS08** Timing belt kit and water pump(约 680.00)在 2027-11-14 落入计划 - 现在为其预算
- **低 CS08** Tyres (set of 4)(约 560.00)在 2027-06-15 落入计划 - 现在为其预算
- **信息 CS05** Brake inspection 上次完成于 5,200 公里 / 6 个月后(低于间隔的 60%)- 严苛使用则正常,否则钱花早了
- **信息 CS05** Engine oil and filter 上次完成于 5,200 公里 / 6 个月后(低于间隔的 60%)- 严苛使用则正常,否则钱花早了
- **信息 CS07** 第 7 行:'Windscreen chip repair' 不在您的计划中 - 仅保留在历史中
间隔和价格来自您的汽车文件:请针对您的确切发动机与车主手册或保养手册核对,如果您的维修厂发现更早的磨损,请遵循其建议。成本为估算值;轮胎和制动也取决于磨损,而不仅仅是距离。
## Python 脚本 (car_service_plan.py)
```python
#!/usr/bin/env python3
"""汽车保养服务计划规划器(仅标准库,Python 3.8+)。
读取您的汽车保养计划(具有距离和/或时间间隔的项目,来自车主手册或保养手册)和您的服务历史(CSV),
根据日志中的里程表读数估算您的行驶距离,并为每个项目计算上次完成时间、下次到期时间(距离或时间,以先到者为准)、
哪些项目已逾期或即将到期、哪些项目可以共享一次维修厂到访,以及未来几个月的保养成本。
用法:
car_service_plan.py CAR.json SERVICE_LOG.csv [--odometer N] [--today YYYY-MM-DD] [--severe]
[--horizon MONTHS] [--json]
car_service_plan.py --self-test
退出:0 = 安全/法律项目无逾期,1 = 安全或法律项目已逾期(或日志中有高严重性发现),2 = 输入错误。
"""
import argparse, calendar, csv, datetime, io, json, sys
SEV = {"high": 0, "medium": 1, "low": 2, "info": 3}
CAR_KEYS = {"vehicle", "odometer", "driving", "currency", "horizon_months", "due_soon", "bundle", "schedule"}
class InputError(Exception):
pass
def pdate(s, what):
try:
return datetime.date.fromisoformat(str(s).strip())
except ValueError:
raise InputError("%s 必须是 YYYY-MM-DD,得到 %r" % (what, s))
def add_months(d, m):
y, mo = divmod(d.month - 1 + int(m), 12)
y += d.year
mo += 1
return datetime.date(y, mo, min(d.day, calendar.monthrange(y, mo)[1]))
def num(x, what):
try:
return float(str(x).replace(",", "").replace(" ", ""))
except ValueError:
raise InputError("%s 不是数字:%r" % (what, x))
def fk(x):
return "{:,.0f}".format(x)
def fm(x):
return "{:,.2f}".format(x)
# ---------------------------------------------------------------- 加载
def load_car(cfg, odometer=None, today=None, severe=None, horizon=None):
if not isinstance(cfg, dict):
raise InputError("汽车文件必须是 JSON 对象")
bad = set(cfg) - CAR_KEYS
if bad:
raise InputError("汽车文件中的未知键:%s" % ", ".join(sorted(bad)))
v = cfg.get("vehicle") or {}
odo = cfg.get("odometer") or {}
drv = cfg.get("driving") or {}
c = {"name": v.get("name", "Car"), "unit": v.get("unit", "km"),
"first": pdate(v["first_registered"], "vehicle.first_registered") if v.get("first_registered") else None,
"today": pdate(today or odo.get("date") or datetime.date.today().isoformat(), "today"),
"odo": num(odometer if odometer is not None else odo.get("reading"), "odometer reading") if (odometer is not None or odo.get("reading") is not None) else None,
"default_per_year": num(drv.get("default_per_year", 12000), "driving.default_per_year"),
"severe": bool(drv.get("severe", False)) if severe is None else severe,
"severe_reasons": drv.get("severe_reasons", []),
"currency": cfg.get("currency", ""),
"horizon": int(horizon or cfg.get("horizon_months", 24)),
"soon_km": num((cfg.get("due_soon") or {}).get("km", 1500), "due_soon.km"),
"soon_days": int((cfg.get("due_soon") or {}).get("days", 45)),
"bundle_km": num((cfg.get("bundle") or {}).get("km", 2500), "bundle.km"),
"bundle_days": int((cfg.get("bundle") or {}).get("days", 60)),
"items": []}
if c["odo"] is None:
raise InputError("请提供当前里程表读数(odometer.reading 或 --odometer)")
for i, it in enumerate(cfg.get("schedule") or [], 1):
name = it.get("item")
if not name:
raise InputError("计划项目 %d 没有 'item' 名称" % i)
km = it.get("severe_km") if c["severe"] and it.get("severe_km") else it.get("km")
mo = it.get("severe_months") if c["severe"] and it.get("severe_months") else it.get("months")
if not km and not mo:
raise InputError("计划项目 '%s' 需要 km 和/或 months" % name)
c["items"].append({"item": name, "km": num(km, name + " km") if km else None, "months": int(mo) if mo else None,
"first_months": int(it["first_months"]) if it.get("first_months") else None,
"cost": num(it.get("cost", 0), name + " cost"), "safety": bool(it.get("safety")), "legal": bool(it.get("legal")),
"severe_used": bool(c["severe"] and (it.get("severe_km") or it.get("severe_months"))),
"aliases": [a.lower().strip() for a in it.get("aliases", [])] + [name.lower().strip()]})
if not c["items"]:
raise InputError("汽车文件需要非空的 'schedule'")
return c
def load_log(c, text, F):
rows = list(csv.DictReader(io.StringIO(text)))
if rows and not {"date", "odometer", "item"} <= set(rows[0]):
raise InputError("服务日志需要列 date, odometer, item(有 %s)" % ", ".join(rows[0]))
entries, readings = [], []
for n, r in enumerate(rows, 2):
try:
d = pdate(r.get("date"), "date (row %d)" % n)
except InputError as e:
F.append(("high", "CS04", "row %d: %s - 行已忽略" % (n, e))); continue
if d > c["today"]:
F.append(("high", "CS04", "row %d: 日期 %s 在未来 - 行已忽略" % (n, d))); continue
odo = None
if (r.get("odometer") or "").strip():
try:
odo = num(r["odometer"], "odometer (row %d)" % n)
except InputError as e:
F.append(("medium", "CS04", "%s - 此行的距离已忽略" % e))
items = [x.strip() for x in (r.get("item") or "").split(";") if x.strip()]
entries.append({"row": n, "date": d, "odo": odo, "items": items, "cost": r.get("cost", ""), "shop": r.get("shop", "")})
entries.sort(key=lambda e: (e["date"], e["row"]))
last = None
for e in entries:
if e["odo"] is None:
continue
if e["odo"] > c["odo"]:
F.append(("high", "CS04", "row %d: %s 上的里程表 %s 高于当前读数 %s - 笔误?读数已忽略" % (e["row"], fk(e["odo"]), e["date"], fk(c["odo"]))))
e["odo"] = None; continue
if last and e["odo"] < last["odo"]:
F.append(("medium", "CS04", "row %d: %s 上的里程表 %s 低于 %s 上的 %s - 笔误或错误的汽车?读数已忽略" % (e["row"], fk(e["odo"]), e["date"], fk(last["odo"]), last["date"])))
e["odo"] = None; continue
last = e
readings.append((e["date"], e["odo"]))
readings.append((c["today"], c["odo"]))
return entries, readings
def distance_rate(c, readings, F):
"""从今天前至少一年的最新读数(否则取最早的读数)计算每天公里数。"""
end_d, end_o = readings[-1]
older = [r for r in readings[:-1] if (end_d - r[0]).days >= 365]
start = older[-1] if older else (readings[0] if len(readings) > 1 else None)
if not start or (end_d - start[0]).days < 60 or end_o <= start[1]:
F.append(("medium", "CS09", "里程表读数不足,无法估算您的年行驶距离 - 使用默认值 %s %s/年" % (fk(c["default_per_year"]), c["unit"])))
return c["default_per_year"] / 365.0, "默认"
rate = (end_o - start[1]) / float((end_d - start[0]).days)
return rate, "%s %s -> %s %s" % (start[0], fk(start[1]), end_d, fk(end_o))
# ---------------------------------------------------------------- 计划
def plan(c, log_text):
F = []
entries, readings = load_log(c, log_text, F)
rate, basis = distance_rate(c, readings, F)
known = {a: it for it in c["items"] for a in it["aliases"]}
done = {it["item"]: [] for it in c["items"]}
for e in entries:
for name in e["items"]:
it = known.get(name.lower())
if not it:
F.append(("info", "CS07", "row %d: '%s' 不在您的计划中 - 仅保留在历史中" % (e["row"], name)))
continue
done[it["item"]].append(e)
def date_at_km(km):
return c["today"] + datetime.timedelta(days=max(0.0, (km - c["odo"]) / rate)) if rate > 0 else None
def km_at_date(d):
return c["odo"] + rate * (d - c["today"]).days
end = add_months(c["today"], c["horizon"])
rows, occ = [], []
for it in c["items"]:
hist = done[it["item"]]
last = hist[-1] if hist else None
if last:
base_d, base_km, base_note = last["date"], last["odo"], "%s 在 %s" % (last["date"], fk(last["odo"]) if last["odo"] is not None else "?")
else:
if not c["first"]:
raise InputError("'%s' 无记录且无 vehicle.first_registered 可计算" % it["item"])
base_d, base_km, base_note = c["first"], 0.0, "无记录(从新车开始计算,%s)" % c["first"]
F.append(("medium", "CS03", "%s:您的日志中无记录 - 从新车开始计算;检查服务手册或收据" % it["item"]))
months = it["months"]
if not last and it["first_months"]:
months = it["first_months"]
due_d = add_months(base_d, months) if months else None
due_km = base_km + it["km"] if (it["km"] and base_km is not None) else None
km_d = date_at_km(due_km) if due_km is not None else None
cands = [(x, w) for x, w in ((due_d, "时间"), (km_d, "距离")) if x]
if not cands:
due_date, trigger = c["today"], "未知"
else:
due_date, trigger = min(cands)
overdue = (due_d is not None and due_d < c["today"]) or (due_km is not None and due_km < c["odo"])
soon = not overdue and ((due_d is not None and (due_d - c["today"]).days <= c["soon_days"]) or
(due_km is not None and due_km - c["odo"] <= c["soon_km"]))
status = "已逾期" if overdue else ("即将到期" if soon else "正常")
if overdue:
late = []
if due_d is not None and due_d < c["today"]:
late.append("%d 天" % (c["today"] - due_d).days)
if due_km is not None and due_km < c["odo"]:
late.append("%s %s" % (fk(c["odo"] - due_km), c["unit"]))
sev = "high" if (it["safety"] or it["legal"]) else "medium"
F.append((sev, "CS01", "%s 逾期 %s%s - 立即预订" % (it["item"], " 和 ".join(late), "(安全项目)" if it["safety"] else ("(法律要求)" if it["legal"] else ""))))
elif soon:
F.append(("medium", "CS02", "%s 即将到期:%s%s" % (it["item"], due_d if due_d and (due_d - c["today"]).days <= c["soon_days"] else "", (" / 在 %s %s" % (fk(due_km), c["unit"])) if due_km is not None else "")))
if len(hist) >= 2 and hist[-2]["odo"] is not None and hist[-1]["odo"] is not None:
a, b = hist[-2], hist[-1]
fr = []
if it["km"]:
fr.append((b["odo"] - a["odo"]) / it["km"])
if it["months"]:
fr.append(((b["date"] - a["date"]).days / 30.44) / it["months"])
if fr and max(fr) < 0.6:
F.append(("info", "CS05", "%s 上次完成于 %s %s / %d 个月后(低于间隔的 60%%)- 严苛使用则正常,否则钱花早了" % (it["item"], fk(b["odo"] - a["odo"]), c["unit"], round((b["date"] - a["date"]).days / 30.44))))
rows.append({"item": it["item"], "last": base_note, "interval": " / ".join(x for x in [("%s %s" % (fk(it["km"]), c["unit"])) if it["km"] else "", ("%d 个月" % months) if months else ""] if x),
"due_date": due_d, "due_km": due_km, "next": max(due_date, c["today"]), "trigger": trigger, "status": status, "cost": it["cost"],
"safety": it["safety"] or it["legal"], "flag": "法律" if it["legal"] else ("安全" if it["safety"] else ""), "severe": it["severe_used"]})
# 范围内的发生次数(每次假定在其到期日期完成)
d, k, first = max(due_date, c["today"]), due_km, True
guard = 0
while d <= end and guard < 50:
guard += 1
occ.append({"item": it["item"], "date": d, "km": km_at_date(d), "cost": it["cost"], "overdue": first and overdue})
first = False
nd = add_months(d, it["months"]) if it["months"] else None
nk = km_at_date(d) + it["km"] if it["km"] else None
cands = [x for x in (nd, date_at_km(nk) if nk is not None else None) if x]
if not cands:
break
d = min(cands)
if (it["cost"] >= 400) and any(o["item"] == it["item"] for o in occ):
o = next(o for o in occ if o["item"] == it["item"])
F.append(("low", "CS08", "%s(约 %s)在 %s 落入计划 - 现在为其预算" % (it["item"], fm(it["cost"]), o["date"])))
if c["severe"]:
F.append(("info", "CS06", "正在使用严苛服务间隔%s" % (("(" + ",".join(c["severe_reasons"]) + ")") if c["severe_reasons"] else "")))
# 到访:在捆绑窗口内分组发生次数
occ.sort(key=lambda o: (o["date"], o["item"]))
visits = []
for o in occ:
v = visits[-1] if visits else None
if v and (o["date"] - v["date"]).days <= c["bundle_days"] and o["km"] - v["km"] <= c["bundle_km"]:
v["items"].append(o); continue
visits.append({"date": o["date"], "km": o["km"], "items": [o]})
# 按季度成本
quarters = {}
for o in occ:
q = "%d-Q%d" % (o["date"].year, (o["date"].month - 1) // 3 + 1)
quarters[q] = quarters.get(q, 0.0) + o["cost"]
y1 = sum(o["cost"] for o in occ if o["date"] < add_months(c["today"], 12))
total = sum(o["cost"] for o in occ)
F.sort(key=lambda f: (SEV[f[0]], f[1], f[2]))
return {"car": c, "rate": rate, "basis": basis, "rows": rows, "occ": occ, "visits": visits, "quarters": quarters,
"next12": y1, "total": total, "findings": F, "entries": entries}
# ---------------------------------------------------------------- 报告
def report(res, title):
c, F = res["car"], res["findings"]
cnt = {s: sum(1 for f in F if f[0] == s) for s in SEV}
cur = (" " + c["currency"]) if c["currency"] else ""
ov = [r for r in res["rows"] if r["status"] == "已逾期"]
soon = [r for r in res["rows"] if r["status"] == "即将到期"]
L = ["# 服务计划:%s" % c["name"], ""]
L.append("今天 %s,里程表 %s %s。您每年行驶约 %s %s(%s)。%s 间隔,计划范围 %d 个月。来源:%s。" % (
c["today"], fk(c["odo"]), c["unit"], fk(res["rate"] * 365), c["unit"], res["basis"], "严苛服务" if c["severe"] else "正常服务", c["horizon"], title))
L.append("")
if any(r["safety"] for r in ov):
L.append("**结论:%d 个项目逾期,包括安全/法律项目 - 立即预订到访**" % len(ov))
elif ov:
L.append("**结论:%d 个项目逾期 - 尽快预订到访**" % len(ov))
elif soon:
L.append("**结论:无逾期,%d 个项目即将到期**" % len(soon))
else:
L.append("**结论:最新**")
L.append("")
L.append("未来 12 个月:约 %s%s;未来 %d 个月:%s%s(每月预留约 %s%s)。发现:%d 高,%d 中,%d 低,%d 信息。" % (
fm(res["next12"]), cur, c["horizon"], fm(res["total"]), cur, fm(res["total"] / c["horizon"]), cur, cnt["high"], cnt["medium"], cnt["low"], cnt["info"]))
L += ["", "## 项目", "", "| 项目 | 间隔 | 上次完成 | 到期日期 | 到期 %s | 下次(预计) | 触发 | 状态 | 预计成本 |" % c["unit"], "|---|---|---|---|---|---|---|---|---|"]
order = {"已逾期": 0, "即将到期": 1, "正常": 2}
for r in sorted(res["rows"], key=lambda r: (order[r["status"]], r["next"])):
L.append("| %s%s | %s | %s | %s | %s | %s | %s | %s | %s |" % (
r["item"], (" [%s]" % r["flag"]) if r["flag"] else "", r["interval"] + ("(严苛)" if r["severe"] else ""), r["last"],
r["due_date"] or "-", fk(r["due_km"]) if r["due_km"] is not None else "-", r["next"], r["trigger"],
"**%s**" % r["status"] if r["status"] != "正常" else "正常", fm(r["cost"])))
L += ["", "## 维修厂到访(%d 天 / %s %s 内的项目捆绑)" % (c["bundle_days"], fk(c["bundle_km"]), c["unit"]), "",
"| # | 预订截止 | 大约在 | 项目 | 预计成本 |", "|---|---|---|---|---|"]
for i, v in enumerate(res["visits"], 1):
L.append("| %d | %s%s | %s %s | %s | %s |" % (i, v["date"], "(现在)" if v["date"] == c["today"] else "", fk(v["km"]), c["unit"],
",".join(o["item"] + ("(已逾期)" if o["overdue"] else "") for o in v["items"]), fm(sum(o["cost"] for o in v["items"]))))
L += ["", "## 按季度成本预测", "", "| 季度 | 预计成本 |", "|---|---|"]
for q in sorted(res["quarters"]):
L.append("| %s | %s |" % (q, fm(res["quarters"][q])))
L.append("| **%d 个月总计** | **%s** |" % (c["horizon"], fm(res["total"])))
L += ["", "## 发现", ""]
L += ["- **%s %s** %s" % (s.upper(), code, m) for s, code, m in F] or ["- 无"]
L += ["", "间隔和价格来自您的汽车文件:请针对您的确切发动机与车主手册或保养手册核对,如果您的维修厂发现更早的磨损,请遵循其建议。成本为估算值;轮胎和制动也取决于磨损,而不仅仅是距离。"]
return "\n".join(L) + "\n"
def to_json(res):
def j(x):
if isinstance(x, datetime.date):
return x.isoformat()
if isinstance(x, float):
return round(x, 2)
if isinstance(x, dict):
return {k: j(v) for k, v in x.items()}
if isinstance(x, list):
return [j(v) for v in x]
return x
return j({"km_per_year": res["rate"] * 365, "items": res["rows"], "visits": [{"date": v["date"], "km": v["km"], "items": [o["item"] for o in v["items"]],
"cost": sum(o["cost"] for o in v["items"])} for v in res["visits"]], "quarters": res["quarters"], "next12": res["next12"], "total": res["total"],
"findings": [{"severity": s, "code": c, "message": m} for s, c, m in res["findings"]]})
# ---------------------------------------------------------------- 自测
def self_test():
tests = []
def t(name, cond):
tests.append((name, bool(cond)))
def car(**kw):
cfg = {"vehicle": {"name": "T", "first_registered": "2020-01-01"}, "odometer": {"reading": 50000, "date": "2026-01-01"},
"schedule": [{"item": "Oil", "km": 15000, "months": 12, "severe_km": 7500, "severe_months": 6, "cost": 100, "aliases": ["oil change"]}]}
sched = kw.pop("schedule", None)
if sched:
cfg["schedule"] = sched
cfg.update(kw)
return cfg
H = "date,odometer,item,cost,shop\n"
def run(cfg, log, **kw):
return plan(load_car(cfg, **kw), H + log)
def row(res, name):
return next(r for r in res["rows"] if r["item"] == name)
def codes(res):
return [(f[0], f[1]) for f in res["findings"]]
log = "2025-01-01,40000,Oil,90,X\n"
r = run(car(), log)
t("从读数计算速率:10000 公里/年", abs(r["rate"] * 365 - 10000) < 1)
t("时间先到期(12 个月)", row(r, "Oil")["due_date"] == datetime.date(2026, 1, 1) and row(r, "Oil")["trigger"] == "时间")
t("今天到期计为即将到期", row(r, "Oil")["status"] == "即将到期")
r = run(car(), "2025-06-01,40000,Oil,90,X\n")
t("行驶多时距离先触发", row(r, "Oil")["due_km"] == 55000 and row(r, "Oil")["trigger"] == "距离")
r = run(car(), "2024-06-01,30000,Oil,90,X\n")
t("时间和距离均逾期 -> CS01", row(r, "Oil")["status"] == "已逾期" and ("medium", "CS01") in codes(r))
sched = [{"item": "Brake fluid", "months": 24, "cost": 80, "safety": True}]
r = run(car(schedule=sched), "2023-06-01,30000,Brake fluid,80,X\n2025-01-01,40000,Other,0,X\n")
t("安全项目逾期 -> CS01 高", ("high", "CS01") in codes(r))
t("不在计划中的项目 -> CS07", ("info", "CS07") in codes(r))
r = run(car(), "2025-01-01,40000,oil change,90,X\n")
t("别名不区分大小写匹配", row(r, "Oil")["last"].startswith("2025-01-01"))
r = run(car(), "2025-01-01,40000,Wipers,20,X\n")
t("无记录 -> 从新车开始计算,CS03", ("medium", "CS03") in codes(r) and "无记录" in row(r, "Oil")["last"])
r = run(car(), "2025-01-01,40000,Oil,90,X\n", severe=True)
t("使用严苛间隔", row(r, "Oil")["due_date"] == datetime.date(2025, 7, 1) and ("info", "CS06") in codes(r))
r = run(car(), "2024-01-01,30000,Oil,90,X\n2024-06-01,20000,Oil,90,X\n2025-01-01,40000,Oil,90,X\n")
t("里程表下降 -> CS04,读数已忽略", ("medium", "CS04") in codes(r))
r = run(car(), "2025-01-01,40000,Oil,90,X\n2027-01-01,60000,Oil,90,X\n")
t("未来日期 -> CS04 高", ("high", "CS04") in codes(r))
r = run(car(), "2025-01-01,90000,Oil,90,X\n")
t("读数高于当前里程表 -> CS04 高", ("high", "CS04") in codes(r))
r = run(car(), "")
t("无读数 -> 默认速率 + CS09", ("medium", "CS09") in codes(r) and abs(r["rate"] * 365 - 12000) < 1)
r = run(car(), "2025-01-01,40000,Oil,90,X\n2025-04-01,42000,Oil,90,X\n")
t("提前完成 -> CS05", ("info", "CS05") in codes(r))
sched = [{"item": "Test", "months": 12, "first_months": 36, "cost": 50, "legal": True}]
r = run(car(vehicle={"first_registered": "2024-01-01"}, schedule=sched), "2025-01-01,40000,Other,0,X\n")
t("首次测试使用 first_months 从新车开始计算", row(r, "Test")["due_date"] == datetime.date(2027, 1, 1))
sched = [{"item": "A", "months": 12, "cost": 100}, {"item": "B", "months": 12, "cost": 50}]
r = run(car(schedule=sched), "2025-02-01,41000,A,0,X\n2025-03-15,42000,B,0,X\n")
t("捆绑窗口内的项目共享一次到访", len(r["visits"][0]["items"]) == 2)
t("范围重复年度项目", sum(1 for o in r["occ"] if o["item"] == "A") == 2)
t("12 个月和总计预测", r["next12"] == 150 and r["total"] == 300)
sched = [{"item": "Belt", "km": 120000, "months": 96, "cost": 650}]
r = run(car(schedule=sched), "2025-01-01,40000,Other,0,X\n")
t("范围内的大额项目 -> CS08", ("low", "CS08") in codes(r))
t("add_months 钳制月末", add_months(datetime.date(2026, 1, 31), 1) == datetime.date(2026, 2, 28))
try:
load_car(car(schedule=[{"item": "X", "cost": 1}])); ok = False
except InputError:
ok = True
t("无间隔的项目被拒绝", ok)
try:
load_car(dict(car(), colour="red")); ok = False
except InputError:
ok = True
t("未知键被拒绝", ok)
r = run(car(), log)
t("报告有结论、到访和预测", all(s in report(r, "t") for s in ("**结论:", "## 维修厂到访", "## 按季度成本预测")))
for name, ok in tests:
print(("通过 " if ok else "失败 ") + name)
passed = sum(ok for _, ok in tests)
print("%d/%d 通过" % (passed, len(tests)))
return 0 if passed == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description="根据计划和保养日志规划汽车保养。")
ap.add_argument("car", nargs="?")
ap.add_argument("log", nargs="?")
ap.add_argument("--odometer", type=float)
ap.add_argument("--today")
ap.add_argument("--severe", action="store_true", default=None, help="使用严苛服务间隔")
ap.add_argument("--horizon", type=int, help="计划月数(默认来自汽车文件,否则 24)")
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.car or not a.log:
ap.print_usage(sys.stderr)
return 2
try:
try:
cfg = json.load(open(a.car, encoding="utf-8"))
except json.JSONDecodeError as e:
raise InputError("汽车文件不是有效的 JSON:%s" % e)
c = load_car(cfg, a.odometer, a.today, a.severe, a.horizon)
res = plan(c, open(a.log, encoding="utf-8-sig").read())
except (OSError, InputError) as e:
print("错误:%s" % e, file=sys.stderr)
return 2
if a.json:
print(json.dumps(to_json(res), indent=2))
else:
sys.stdout.write(report(res, a.log))
return 1 if any(f[0] == "high" for f in res["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())
```
用法
此提示词专为 productivity 设计。复制上方内容并粘贴到你常用的 AI 工具中。
为获得最佳效果,可将占位符(方括号或大写字母标示)替换为你的具体需求。
讨论
0 条评论