dongchedi-cli 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,3 +7,4 @@ build/
7
7
  *.egg-info/
8
8
  __pycache__/
9
9
  .ruff_cache/
10
+ uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dongchedi-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: 懂车帝(dongchedi.com)数据查询 CLI:销量榜 / 品牌 / 车系 / 车型库筛选 / 车型 / 参数配置 / 车主评论 / 搜索
5
5
  Author: weisanju
6
6
  Keywords: automobile,car,cli,dongchedi,vehicle-data
@@ -46,8 +46,12 @@ dcd filter --price 10,20 --fuel 纯电 --body 轿车 # 车型库筛选:10-20
46
46
  dcd filter --body 小型SUV # 全部小型SUV(级别细分,如 紧凑型SUV/中大型车/中型MPV)
47
47
  dcd filter --price 10, --fuel 新能源 --sort 销量高 -n 50 # 10万以上新能源,按销量排序
48
48
  dcd cars 20154 # 星愿的车型年款与价格(含停售/未上市)
49
+ dcd cars 26051 -k 700 --fuel 纯电 # 车型筛选:名称含 700 的纯电车型(大小写不敏感)
50
+ dcd cars 26051 --price 15,17 # 按指导价筛选(万,一侧留空表示不限)
49
51
  dcd specs 20154 259371 # 车型参数配置表
50
52
  dcd specs 20154 259371 259369 -s 电池 # 多车对比,只看"电池"章节
53
+ dcd specs 26051 --all # 全系可查配置车型横向对比
54
+ dcd specs 26051 --all --price 15,17 --fuel 纯电 -s 电池 # 全系筛选后对比
51
55
  dcd reviews 20154 -p 1 -n 20 # 车主评论(含成交价/评分)
52
56
  dcd search 秦plus # 搜车系
53
57
  dcd dealer 259371 # 某车型的经销商报价与优惠(默认杭州)
@@ -88,8 +92,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
88
92
  | brands | `GET /motor/brand/v4/brand/` | 品牌在 `data[].info`(其中 4 个热门品牌在 `info.brand`) |
89
93
  | series | `POST /motor/pc/car/brand/select_series_v2` | 表单参数是 `brand`(不是 `brand_id`,传错会静默返回全站热门车系) |
90
94
  | filter | `POST /motor/pc/car/brand/select_series_v2` | 与 series 同接口,隐藏筛选参数:`price="10,20"`(万元,区间重叠即命中,`-1` 表示不限)、`fuel_form`(1汽油/2柴油/3油电混合/4纯电/5增程/6插混/13轻混)、`series_type`(0轿车/1SUV/2MPV…)、`outter_detail_type`(细分级别:10小型SUV/11紧凑型SUV…);`page` 分页有效,总数在 `data.series_count`;枚举出自车型库页前端 chunk(2026-09 逐项实测) |
91
- | cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068 为占位需跳过 |
92
- | specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI 已按请求过滤 |
95
+ | cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068 为占位需跳过;`-k/--price/--fuel` 为本地筛选,能源取自配置接口 `car_info[].info.fuel_form.value`(纯电动/增程式/插电式混合动力/汽油/油电混合/48V轻混系统,子串匹配),筛选时按 car_id 去重(同一车型会同时出现在「在售」与年款 tab) |
96
+ | specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI 已按请求过滤(返回车型与 car_list 不完全一致,约只含可查配置/在售车型);`--all` 一次取全系在本地筛选 |
93
97
  | reviews | `GET /motor/pc/car/series/get_review_list` | 需 `Referer: /auto/series/score/{series_id}` |
94
98
  | search | `GET /motor/searchapi/search_content` | `cur_tab=2`,车系卡片为 `cell_type=5002`(不一定出现,无则回退内容标题) |
95
99
  | dealer | `GET /motor/pc/car/series/car_dealer_price` | 匿名可用;依赖 `city_name`(全国 → 暂无报价),未覆盖城市会回落邻近城市 |
@@ -106,6 +110,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
106
110
  - 登录态 Cookie 含中文值,urllib 请求头默认 latin-1 编码,发送前已转 UTF-8 裸字节(与浏览器线上行为一致)。
107
111
  - 参数配置表字段:`data.properties[]` 为行定义(type 0/2 为章节标题,1 普通行,3 带子项,6 跳过),值在 `data.car_info[].info[<key>].value`;type 3 的值挂在 `sub_list[].key` 上。注意 `type` 字段时而为 int 时而为字符串,代码里已统一归一化。
108
112
  - 车型库筛选中 `series_type` 与 `outter_detail_type` **互斥**:同传时服务端会忽略后者(按大类返回);CLI 的 `--body` 已自动只传其一(轿车/SUV 传 series_type,小型SUV 等细分传 outter_detail_type)。
113
+ - 年款分组标题的「/ 后缀」不一定是能源:奥迪A6L 是世代代号(/ C9、/ C8),部分车系无后缀(钛7 PHEV)。`cars --fuel` 不解析标题,统一以配置接口的「能源类型」为准;因此只覆盖可查配置的车型,停售车型若无配置数据会被筛掉。
114
+ - `cars` / `specs --all` 的筛选均在本地完成;`--json` 始终输出接口原始数据、不受筛选影响。筛选无匹配时:`cars` 输出空表(退出码 0,提示在 stderr),`specs --all` 报错(退出码 1)。
109
115
  - 接口可能随版本变化(版本参数当前为 version_code=476),失效时优先用 `--json` 排查原始返回。
110
116
 
111
117
  ## 开发与发布
@@ -26,8 +26,12 @@ dcd filter --price 10,20 --fuel 纯电 --body 轿车 # 车型库筛选:10-20
26
26
  dcd filter --body 小型SUV # 全部小型SUV(级别细分,如 紧凑型SUV/中大型车/中型MPV)
27
27
  dcd filter --price 10, --fuel 新能源 --sort 销量高 -n 50 # 10万以上新能源,按销量排序
28
28
  dcd cars 20154 # 星愿的车型年款与价格(含停售/未上市)
29
+ dcd cars 26051 -k 700 --fuel 纯电 # 车型筛选:名称含 700 的纯电车型(大小写不敏感)
30
+ dcd cars 26051 --price 15,17 # 按指导价筛选(万,一侧留空表示不限)
29
31
  dcd specs 20154 259371 # 车型参数配置表
30
32
  dcd specs 20154 259371 259369 -s 电池 # 多车对比,只看"电池"章节
33
+ dcd specs 26051 --all # 全系可查配置车型横向对比
34
+ dcd specs 26051 --all --price 15,17 --fuel 纯电 -s 电池 # 全系筛选后对比
31
35
  dcd reviews 20154 -p 1 -n 20 # 车主评论(含成交价/评分)
32
36
  dcd search 秦plus # 搜车系
33
37
  dcd dealer 259371 # 某车型的经销商报价与优惠(默认杭州)
@@ -68,8 +72,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
68
72
  | brands | `GET /motor/brand/v4/brand/` | 品牌在 `data[].info`(其中 4 个热门品牌在 `info.brand`) |
69
73
  | series | `POST /motor/pc/car/brand/select_series_v2` | 表单参数是 `brand`(不是 `brand_id`,传错会静默返回全站热门车系) |
70
74
  | filter | `POST /motor/pc/car/brand/select_series_v2` | 与 series 同接口,隐藏筛选参数:`price="10,20"`(万元,区间重叠即命中,`-1` 表示不限)、`fuel_form`(1汽油/2柴油/3油电混合/4纯电/5增程/6插混/13轻混)、`series_type`(0轿车/1SUV/2MPV…)、`outter_detail_type`(细分级别:10小型SUV/11紧凑型SUV…);`page` 分页有效,总数在 `data.series_count`;枚举出自车型库页前端 chunk(2026-09 逐项实测) |
71
- | cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068 为占位需跳过 |
72
- | specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI 已按请求过滤 |
75
+ | cars | `GET /motor/pc/car/series/car_list` | `data.tab_list[]`,type 1115=车型、1137=年款分组,1068 为占位需跳过;`-k/--price/--fuel` 为本地筛选,能源取自配置接口 `car_info[].info.fuel_form.value`(纯电动/增程式/插电式混合动力/汽油/油电混合/48V轻混系统,子串匹配),筛选时按 car_id 去重(同一车型会同时出现在「在售」与年款 tab) |
76
+ | specs | `GET /motor/pc/car/get_full_config_info` | 需 `series_id` + `car_ids`;实测忽略 car_ids,总是返回全车系,CLI 已按请求过滤(返回车型与 car_list 不完全一致,约只含可查配置/在售车型);`--all` 一次取全系在本地筛选 |
73
77
  | reviews | `GET /motor/pc/car/series/get_review_list` | 需 `Referer: /auto/series/score/{series_id}` |
74
78
  | search | `GET /motor/searchapi/search_content` | `cur_tab=2`,车系卡片为 `cell_type=5002`(不一定出现,无则回退内容标题) |
75
79
  | dealer | `GET /motor/pc/car/series/car_dealer_price` | 匿名可用;依赖 `city_name`(全国 → 暂无报价),未覆盖城市会回落邻近城市 |
@@ -86,6 +90,8 @@ Cookie 文件解析优先级:`--cookie-file` 参数 → `DCD_COOKIE_FILE` 环
86
90
  - 登录态 Cookie 含中文值,urllib 请求头默认 latin-1 编码,发送前已转 UTF-8 裸字节(与浏览器线上行为一致)。
87
91
  - 参数配置表字段:`data.properties[]` 为行定义(type 0/2 为章节标题,1 普通行,3 带子项,6 跳过),值在 `data.car_info[].info[<key>].value`;type 3 的值挂在 `sub_list[].key` 上。注意 `type` 字段时而为 int 时而为字符串,代码里已统一归一化。
88
92
  - 车型库筛选中 `series_type` 与 `outter_detail_type` **互斥**:同传时服务端会忽略后者(按大类返回);CLI 的 `--body` 已自动只传其一(轿车/SUV 传 series_type,小型SUV 等细分传 outter_detail_type)。
93
+ - 年款分组标题的「/ 后缀」不一定是能源:奥迪A6L 是世代代号(/ C9、/ C8),部分车系无后缀(钛7 PHEV)。`cars --fuel` 不解析标题,统一以配置接口的「能源类型」为准;因此只覆盖可查配置的车型,停售车型若无配置数据会被筛掉。
94
+ - `cars` / `specs --all` 的筛选均在本地完成;`--json` 始终输出接口原始数据、不受筛选影响。筛选无匹配时:`cars` 输出空表(退出码 0,提示在 stderr),`specs --all` 报错(退出码 1)。
89
95
  - 接口可能随版本变化(版本参数当前为 version_code=476),失效时优先用 `--json` 排查原始返回。
90
96
 
91
97
  ## 开发与发布
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dongchedi-cli"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "懂车帝(dongchedi.com)数据查询 CLI:销量榜 / 品牌 / 车系 / 车型库筛选 / 车型 / 参数配置 / 车主评论 / 搜索"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  """懂车帝数据查询 CLI。"""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -3,6 +3,8 @@
3
3
  免登录子命令:rank / brands / series / filter / cars / specs / reviews / search / dealer
4
4
  需登录态子命令:series-info(HTML SSR)/ me(账号信息)
5
5
 
6
+ cars 可按 关键字/价格/能源 筛选;specs 支持 --all 对比全系可查配置车型并叠加同一套筛选
7
+
6
8
  登录态来源(auth-import 写入,或 --cookie-file / DCD_COOKIE_FILE 指定):
7
9
  dcd auth-import <playwright-state.json> # playwright-cli state-save 的产物
8
10
 
@@ -420,13 +422,19 @@ SORTS = {
420
422
  }
421
423
 
422
424
 
423
- def parse_price(text: str) -> str:
424
- """'10,20' -> '10,20';一侧留空表示不限:'10,' -> '10,-1',',20' -> '0,20'。"""
425
+ def parse_price_range(text: str) -> tuple[str | None, str | None]:
426
+ """'10,20' -> ('10','20');一侧留空表示不限:'10,' -> ('10',None),',20' -> (None,'20')。"""
425
427
  normalized = text.replace("万", "").replace(",", ",").replace("~", "~")
426
428
  match = re.fullmatch(r"\s*(\d+(?:\.\d+)?)?\s*[,\-~]\s*(\d+(?:\.\d+)?)?\s*", normalized)
427
429
  if not match or (not match.group(1) and not match.group(2)):
428
430
  raise DcdError('价格格式应为 "最低,最高"(单位万),如 "10,20";一侧留空表示不限,如 "10,"')
429
- return f"{match.group(1) or '0'},{match.group(2) or '-1'}"
431
+ return match.group(1), match.group(2)
432
+
433
+
434
+ def parse_price(text: str) -> str:
435
+ """'10,20' -> '10,20';一侧留空表示不限:'10,' -> '10,-1',',20' -> '0,20'。"""
436
+ low, high = parse_price_range(text)
437
+ return f"{low or '0'},{high or '-1'}"
430
438
 
431
439
 
432
440
  def resolve_fuel(text: str) -> str:
@@ -502,6 +510,99 @@ def cmd_filter(args) -> None:
502
510
  print_series_table(series)
503
511
 
504
512
 
513
+ # ---------- 车系内筛选(cars / specs --all 共用) ----------
514
+
515
+ # 配置接口的能源类型实际取值:纯电动 / 增程式 / 插电式混合动力 / 汽油 / 油电混合 / 48V轻混系统
516
+ # 按子串匹配,别名与 filter 子命令保持一致
517
+ FUEL_TEXTS = {
518
+ "新能源": ("纯电", "增程", "插电"),
519
+ "纯电": ("纯电",),
520
+ "纯电动": ("纯电",),
521
+ "增程": ("增程",),
522
+ "增程式": ("增程",),
523
+ "插混": ("插电",),
524
+ "插电混动": ("插电",),
525
+ "插电式混合动力": ("插电",),
526
+ "汽油": ("汽油",),
527
+ "柴油": ("柴油",),
528
+ "油电混动": ("油电",),
529
+ "油电混合": ("油电",),
530
+ "轻混": ("轻混", "48V"),
531
+ "48V轻混": ("轻混", "48V"),
532
+ }
533
+
534
+
535
+ def resolve_fuel_text(text: str) -> tuple[str, ...]:
536
+ key = text.strip()
537
+ if key in FUEL_TEXTS:
538
+ return FUEL_TEXTS[key]
539
+ raise DcdError(
540
+ f"未知能源 {text!r};可用: 新能源/纯电/增程/插混/汽油/柴油/油电混动/轻混"
541
+ )
542
+
543
+
544
+ def parse_wan(value) -> float | None:
545
+ """车系接口指导价是浮点,配置接口是 '13.99万' 字符串,统一为数字(万)。"""
546
+ if isinstance(value, (int, float)):
547
+ return float(value)
548
+ match = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*万?\s*", str(value or ""))
549
+ return float(match.group(1)) if match else None
550
+
551
+
552
+ def car_energy(car: dict) -> str:
553
+ return (((car.get("info") or {}).get("fuel_form") or {}).get("value") or "").strip()
554
+
555
+
556
+ def matches_keyword(name: str, keyword: str) -> bool:
557
+ return keyword.lower() in name.lower()
558
+
559
+
560
+ def matches_price(price_wan: float | None, bounds: tuple[float | None, float | None]) -> bool:
561
+ if price_wan is None:
562
+ return False
563
+ low, high = bounds
564
+ if low is not None and price_wan < low:
565
+ return False
566
+ return high is None or price_wan <= high
567
+
568
+
569
+ def matches_fuel(energy: str, texts: tuple[str, ...]) -> bool:
570
+ return bool(energy) and any(t in energy for t in texts)
571
+
572
+
573
+ def parse_price_bounds(text: str) -> tuple[float | None, float | None]:
574
+ low, high = parse_price_range(text)
575
+ return (float(low) if low else None, float(high) if high else None)
576
+
577
+
578
+ def fetch_car_fuels(series_id) -> dict[str, str]:
579
+ """车系内全部可查配置车型的能源类型:car_id -> fuel_form。"""
580
+ payload = api_get("/motor/pc/car/get_full_config_info", {"series_id": series_id})
581
+ fuels = {}
582
+ for car in (payload.get("data") or {}).get("car_info") or []:
583
+ cid = car.get("car_id")
584
+ if cid is not None:
585
+ fuels[str(cid)] = car_energy(car)
586
+ return fuels
587
+
588
+
589
+ def filter_desc(keyword, price, fuel) -> str:
590
+ parts = []
591
+ if keyword:
592
+ parts.append(f"关键字 {keyword}")
593
+ if price:
594
+ low, high = parse_price_range(price)
595
+ if low and high:
596
+ parts.append(f"价格 {low}-{high}万")
597
+ elif low:
598
+ parts.append(f"价格 {low}万以上")
599
+ else:
600
+ parts.append(f"价格 {high}万以下")
601
+ if fuel:
602
+ parts.append(fuel)
603
+ return " · ".join(parts)
604
+
605
+
505
606
  def cmd_cars(args) -> None:
506
607
  payload = api_get(
507
608
  "/motor/pc/car/series/car_list",
@@ -510,27 +611,66 @@ def cmd_cars(args) -> None:
510
611
  if args.json:
511
612
  dump_json(payload)
512
613
  return
513
- tabs = (payload.get("data") or {}).get("tab_list") or []
514
- rows = []
515
- for tab in tabs:
614
+ filtering = bool(args.keyword or args.price or args.fuel)
615
+ bounds = parse_price_bounds(args.price) if args.price else None
616
+ texts = resolve_fuel_text(args.fuel) if args.fuel else None
617
+ fuels = fetch_car_fuels(args.series_id) if texts else {}
618
+ seen: set[str] = set()
619
+ rows: list[list[str]] = []
620
+ header: list[str] | None = None
621
+ section: list[list[str]] = []
622
+
623
+ def flush() -> None:
624
+ # 筛选模式下整段无匹配的章节连标题一起隐藏
625
+ if not filtering or section:
626
+ if header:
627
+ rows.append(header)
628
+ rows.extend(section)
629
+
630
+ for tab in (payload.get("data") or {}).get("tab_list") or []:
516
631
  status = tab.get("tab_text") or ""
517
632
  for entry in tab.get("data") or []:
518
633
  info = entry.get("info") or {}
519
- if entry.get("type") == "1068":
634
+ etype = entry.get("type")
635
+ if etype == "1068":
520
636
  continue
521
- if entry.get("type") == "1137":
522
- rows.append([f"[{status}] {info.get('name') or ''}", "", "", "", "", ""])
637
+ if etype == "1137":
638
+ flush()
639
+ header = [f"[{status}] {info.get('name') or ''}", "", "", "", "", ""]
640
+ section = []
523
641
  continue
524
- rows.append(
642
+ name = info.get("car_name") or info.get("name") or ""
643
+ car_id = str(info.get("car_id") or info.get("id") or "")
644
+ if filtering:
645
+ if args.keyword and not matches_keyword(name, args.keyword):
646
+ continue
647
+ if bounds and not matches_price(parse_wan(info.get("official_price")), bounds):
648
+ continue
649
+ if texts and not matches_fuel(fuels.get(car_id, ""), texts):
650
+ continue
651
+ # 同一车型会同时出现在「在售」与年款 tab,筛选时按 car_id 去重
652
+ if car_id and car_id in seen:
653
+ continue
654
+ if car_id:
655
+ seen.add(car_id)
656
+ section.append(
525
657
  [
526
- info.get("car_name") or info.get("name") or "",
658
+ name,
527
659
  str(info.get("year") or ""),
528
660
  info.get("official_price_str") or "",
529
661
  info.get("dealer_price") or "",
530
662
  info.get("owner_price") or "",
531
- str(info.get("car_id") or info.get("id") or ""),
663
+ car_id,
532
664
  ]
533
665
  )
666
+ flush()
667
+ if filtering:
668
+ matched = sum(1 for r in rows if r[5])
669
+ desc = filter_desc(args.keyword, args.price, args.fuel)
670
+ if matched:
671
+ print(f"筛选后 {matched} 款({desc})", file=sys.stderr)
672
+ else:
673
+ print(f"没有匹配的车型({desc})", file=sys.stderr)
534
674
  print(render_table(["车型", "年款", "官方指导价", "经销商价", "车主价", "车型ID"], rows))
535
675
 
536
676
 
@@ -560,6 +700,16 @@ def _spec_value(car: dict, prop: dict) -> str:
560
700
 
561
701
 
562
702
  def cmd_specs(args) -> None:
703
+ if args.all and args.car_id:
704
+ raise DcdError("--all 与车型ID 不能同时使用")
705
+ if not args.all and (args.keyword or args.price or args.fuel):
706
+ raise DcdError(
707
+ "--keyword/--price/--fuel 需配合 --all 使用,如: dcd specs 26051 --all --fuel 纯电"
708
+ )
709
+ if not args.all and not args.car_id:
710
+ raise DcdError(
711
+ "需要车型ID(可多个,逗号分隔),或用 --all 对比全系可查配置车型;车型ID 用 dcd cars 查"
712
+ )
563
713
  car_ids = [cid for raw in args.car_id for cid in raw.split(",") if cid]
564
714
  payload = api_get(
565
715
  "/motor/pc/car/get_full_config_info",
@@ -570,14 +720,29 @@ def cmd_specs(args) -> None:
570
720
  return
571
721
  data = payload["data"]
572
722
  cars = data.get("car_info") or []
573
- if not cars:
574
- raise DcdError(f"未找到车型配置: {', '.join(car_ids)}")
575
- # 实测该接口忽略 car_ids,总是返回整个车系,这里按请求过滤
576
- by_id = {str(c.get("car_id")): c for c in cars}
577
- missing = [cid for cid in car_ids if cid not in by_id]
578
- if missing:
579
- raise DcdError(f"车系 {args.series_id} 下不存在车型: {', '.join(missing)}")
580
- cars = [by_id[cid] for cid in car_ids]
723
+ if args.all:
724
+ bounds = parse_price_bounds(args.price) if args.price else None
725
+ texts = resolve_fuel_text(args.fuel) if args.fuel else None
726
+ cars = [
727
+ car
728
+ for car in cars
729
+ if (not args.keyword or matches_keyword(car.get("car_name") or "", args.keyword))
730
+ and (not bounds or matches_price(parse_wan(car.get("official_price")), bounds))
731
+ and (not texts or matches_fuel(car_energy(car), texts))
732
+ ]
733
+ desc = filter_desc(args.keyword, args.price, args.fuel)
734
+ if not cars:
735
+ raise DcdError(f"全系没有匹配的车型({desc})")
736
+ print(f"全系 {len(cars)} 款" + (f"({desc})" if desc else ""), file=sys.stderr)
737
+ else:
738
+ if not cars:
739
+ raise DcdError(f"未找到车型配置: {', '.join(car_ids)}")
740
+ # 实测该接口忽略 car_ids,总是返回整个车系,这里按请求过滤
741
+ by_id = {str(c.get("car_id")): c for c in cars}
742
+ missing = [cid for cid in car_ids if cid not in by_id]
743
+ if missing:
744
+ raise DcdError(f"车系 {args.series_id} 下不存在车型: {', '.join(missing)}")
745
+ cars = [by_id[cid] for cid in car_ids]
581
746
  headers = ["参数"] + [c.get("car_name") or str(c.get("car_id")) for c in cars]
582
747
  rows: list[list[str]] = []
583
748
  section = ""
@@ -912,14 +1077,27 @@ def build_parser() -> argparse.ArgumentParser:
912
1077
  p.add_argument("-j", "--json", action="store_true", help="输出原始 JSON")
913
1078
  p.set_defaults(func=cmd_filter)
914
1079
 
915
- p = sub.add_parser("cars", help="车系下的车型(年款/价格)")
1080
+ p = sub.add_parser("cars", help="车系下的车型(年款/价格),可按条件筛选")
916
1081
  p.add_argument("series_id", help="车系ID")
1082
+ p.add_argument("-k", "--keyword", help="按车型名筛选(不区分大小写),如 700、max")
1083
+ p.add_argument("--price", help='按指导价筛选(万),如 "15,17";一侧留空表示不限')
1084
+ p.add_argument(
1085
+ "-f", "--fuel", help="按能源筛选:纯电/增程/插混/汽油/柴油/油电混动/新能源/轻混"
1086
+ )
917
1087
  p.add_argument("-j", "--json", action="store_true", help="输出原始 JSON")
918
1088
  p.set_defaults(func=cmd_cars)
919
1089
 
920
- p = sub.add_parser("specs", help="车型参数配置表(可多车对比)")
1090
+ p = sub.add_parser("specs", help="车型参数配置表(指定车型对比,或 --all 对比全系)")
921
1091
  p.add_argument("series_id", help="车系ID")
922
- p.add_argument("car_id", nargs="+", help="车型ID,可多个(用 dcd cars 查)")
1092
+ p.add_argument("car_id", nargs="*", help="车型ID,可多个(用 dcd cars 查);或用 --all 对比全系")
1093
+ p.add_argument(
1094
+ "--all", action="store_true", help="对比全系可查配置车型,可叠加 --keyword/--price/--fuel"
1095
+ )
1096
+ p.add_argument("-k", "--keyword", help="按车型名筛选(需 --all),如 Max、四驱")
1097
+ p.add_argument("--price", help='按指导价筛选(需 --all,万),如 "15,17"')
1098
+ p.add_argument(
1099
+ "-f", "--fuel", help="按能源筛选(需 --all):纯电/增程/插混/汽油/油电混动/新能源/轻混"
1100
+ )
923
1101
  p.add_argument("-s", "--section", help="只看包含该文字的章节,如 电池")
924
1102
  p.add_argument("-j", "--json", action="store_true", help="输出原始 JSON")
925
1103
  p.set_defaults(func=cmd_specs)