讨论

汽车保养服务计划表规划器

来自 Wikiprompt,自由的提示词百科全书

Fatih Kadir Akın

2026年11月4日

汽车保养服务计划表规划器 一个完整的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 工具中。

为获得最佳效果,可将占位符(方括号或大写字母标示)替换为你的具体需求。

参考资料

分类:productivity| prompts.chat| car-maintenance| service-schedule

讨论