#!/usr/bin/env python3 """Ingest archived public competitor sources into the BJD mother vault.""" from __future__ import annotations import json from datetime import datetime, timezone from pathlib import Path VAULT = Path("/home/nac/repo/obsidian-wiki-linux/kb/bjd-vault") RESULTS = Path("/tmp/bjd_competitor_download_results.json") NOW = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") def load_results() -> list[dict]: return json.loads(RESULTS.read_text(encoding="utf-8")) def source_type(item: dict) -> str: if item.get("type"): return item["type"] name = item["name"].lower() if name.endswith(".pdf"): return "pdf" if name.endswith(".zip"): return "cad" return "web" def doc_type(item: dict) -> str: name = item["name"].lower() if "drawing" in name or "dxf" in name: return "drawing" if "step" in name: return "cad_model" if "catalog" in name or "guide" in name: return "catalog" if "manual" in name or "instruction" in name: return "instruction_manual" if "datasheet" in name or "spec-sheet" in name or "whitepaper" in name: return "spec_sheet" if "page" in name: return "product_page" return "product_parameter" def product_line(item: dict) -> list[str]: series = item.get("series", "") brand = item.get("brand", "") if brand in {"Rosenberger"} or "hfm" in item["name"].lower(): return ["Mini FAKRA", "High Speed"] if brand == "TE": return ["I/O", "WTB", "Hybrid connector"] if "mini50" in item["name"].lower(): return ["WTB", "Automotive wire-to-board"] if series in {"FH52", "FH52K", "FH75", "FH64MA", "6817"}: return ["FPC", "FFC"] if brand == "IRISO": return ["BTB", "FPC", "WTB"] return ["Automotive connectors"] def related_page(item: dict) -> str: brand = item.get("brand", "") series = item.get("series", "") name = item["name"].lower() if brand == "Hirose" and series in {"FH52", "FH52K"}: return "projects/bjd/references/hirose-fh52-fh52k-public-sources-20260605.md" if brand == "Hirose" and series == "FH75": return "projects/bjd/references/hirose-fh75-fh75m-automotive-fpc-connector.md" if brand in {"Hirose", "Kyocera"} and series in {"FH64MA", "6817"}: return "projects/bjd/references/hirose-fh64ma-kyocera-6817-fpc-connectors.md" if brand == "TE" or "73pos" in name: return "projects/bjd/references/te-2188698-73pin-hybrid-connector-public-sources-20260605.md" if brand == "Molex": return "projects/bjd/references/molex-mini50-connector-system.md" if brand == "IRISO": return "projects/bjd/references/iriso-public-competitor-sources-20260605.md" if brand == "Rosenberger": return "projects/bjd/references/bjd-mini-fakra-product-and-test-evidence.md" return "projects/bjd/references/competitor-public-source-archive-20260605.md" def summary_for(item: dict) -> str: brand = item.get("brand", "Competitor") series = item.get("series", "") dtype = doc_type(item).replace("_", " ") if series: return f"{brand} {series} official public {dtype} archived for BJD competitor lookup and replacement evaluation." return f"{brand} official public {dtype} archived for BJD competitor lookup and replacement evaluation." def registry_records(items: list[dict]) -> list[dict]: records = [] for item in items: if not item.get("ok"): continue brand = item.get("brand", "unknown") stype = source_type(item) rec = { "record_type": "source_archive", "source_id": f"sha256:{item['sha256']}", "archived_at": NOW, "archive_path": item["archive_path"], "original_path": item["url"], "original_name": item["name"], "source_type": stype, "doc_type": doc_type(item), "business_categories": [f"competitors/{brand.lower()}", "products/automotive-connectors"], "product_lines": product_line(item), "customers": [], "competitors": [brand], "standards": [], "market_tags": ["market/automotive"], "title": item["name"].rsplit(".", 1)[0], "summary": summary_for(item), "visibility": "internal", "data_classification": "public-web", "allowed_bots": ["bjd-general-bot"], "classification_confidence": 0.84, "classification_evidence": ["official vendor public source", f"downloaded from {item['url']}"], "catalog_paths": [f"_sources/catalog/bjd/competitors/{brand.lower()}.md"], "project": "bjd", "size_bytes": item.get("size_bytes"), "modified_at": None, "related_pages": [related_page(item)], "bjd_part_numbers": [], "competitor_part_numbers": [item.get("series")] if item.get("series") else [], "series": [item.get("series")] if item.get("series") else [], "pin_count": 73 if "73pos" in item["name"].lower() else None, "needs_review": 0, } records.append(rec) return records def append_registry(records: list[dict]) -> int: path = VAULT / "_meta/source-registry.jsonl" existing = path.read_text(encoding="utf-8") if path.exists() else "" existing_ids = set() for line in existing.splitlines(): try: existing_ids.add(json.loads(line).get("source_id")) except json.JSONDecodeError: pass new = [r for r in records if r["source_id"] not in existing_ids] if new: with path.open("a", encoding="utf-8") as f: for rec in new: f.write(json.dumps(rec, ensure_ascii=False, sort_keys=False) + "\n") return len(new) def md_link(item: dict) -> str: path = item.get("archive_path") if path: return f"[{item['name']}]({path})" return f"[{item['name']}]({item['url']})" def registry_competitor_items() -> list[dict]: path = VAULT / "_meta/source-registry.jsonl" if not path.exists(): return [] items = [] for line in path.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue try: rec = json.loads(line) except json.JSONDecodeError: continue categories = rec.get("business_categories") or [] competitors = rec.get("competitors") or [] if not any(str(c).startswith("competitors/") for c in categories) and not competitors: continue vendor = competitors[0] if competitors else None if not vendor: for c in categories: if str(c).startswith("competitors/"): vendor = str(c).split("/", 1)[1].upper() break if not vendor: continue item = { "ok": True, "name": rec.get("original_name") or rec.get("title") or rec.get("source_id"), "type": rec.get("source_type") or "web", "brand": vendor, "series": (rec.get("series") or rec.get("competitor_part_numbers") or [""])[0], "archive_path": rec.get("archive_path"), "size_bytes": rec.get("size_bytes") or "", "url": rec.get("original_path") or "", "_registry_doc_type": rec.get("doc_type") or "product_parameter", } items.append(item) return items def vendor_catalog(vendor: str, items: list[dict]) -> str: good = [i for i in items if i.get("ok") and i.get("brand") == vendor] failed = [i for i in items if not i.get("ok") and i.get("brand") == vendor] slug = vendor.lower() title = f"{vendor} 竞品源文件目录" rows = [] for item in sorted(good, key=lambda x: (x.get("series") or "", x["name"])): rows.append( f"| {md_link(item)} | {item.get('_registry_doc_type') or doc_type(item)} | {item.get('series','')} | {item.get('size_bytes','')} | [source]({item['url']}) |" ) failed_lines = "" if failed: failed_lines = "\n\n## 待补链接\n\n" + "\n".join( f"- {i['name']} - 下载失败,需从官方页面重新确认直链:{i['url']}" for i in failed ) return f"""--- title: {title} category: meta tags: [source-catalog, bjd, competitors, {slug}, visibility/internal] sources: - _meta/source-registry.jsonl created: 2026-06-05 updated: {NOW} --- # {title} 本页记录已归档到母库的 {vendor} 官方公开竞品资料。资料可用于竞品识别、替代评估、图纸/规格书检索;不要把“有公开资料”自动解释为 BJD 已完成 drop-in 替代。 | 归档文件 | 类型 | 系列 | 大小 bytes | 原始官方链接 | |---|---|---:|---:|---| {chr(10).join(rows) if rows else '| 暂无 | | | | |'} {failed_lines} ## 相关页面 - [[projects/bjd/references/competitor-public-source-archive-20260605]] - [[projects/bjd/references/competitor-cross-reference-map]] """ def index_catalog(items: list[dict]) -> str: vendors = sorted({i.get("brand", "unknown") for i in items if i.get("brand")}) lines = [] for vendor in vendors: good = sum(1 for i in items if i.get("brand") == vendor and i.get("ok")) failed = sum(1 for i in items if i.get("brand") == vendor and not i.get("ok")) note = f"{good} 个已归档" if failed: note += f",{failed} 个待补" lines.append(f"- [[_sources/catalog/bjd/competitors/{vendor.lower()}]] - {vendor},{note}。") return f"""--- title: BJD 竞品资料源文件目录 category: meta tags: [source-catalog, bjd, competitors, visibility/internal] sources: - _meta/source-registry.jsonl created: 2026-06-03 updated: {NOW} --- # BJD 竞品资料源文件目录 该目录记录 BJD wiki 已归档的竞品公开资料,包括官方产品页、目录、规格书、2D 图纸、CAD/STEP/DFX 压缩包和操作说明。归档资料用于后续检索和替代评估。 {chr(10).join(lines)} ## 总索引 - [[projects/bjd/references/competitor-public-source-archive-20260605]] - 2026-06-05 竞品公开资料归档总览。 """ def master_page(items: list[dict]) -> str: by_vendor: dict[str, list[dict]] = {} for item in items: if item.get("ok"): by_vendor.setdefault(item.get("brand", "unknown"), []).append(item) sections = [] for vendor, rows in sorted(by_vendor.items()): lines = [ f"| {i.get('series','')} | {md_link(i)} | {doc_type(i)} | [official]({i['url']}) |" for i in sorted(rows, key=lambda x: (x.get("series") or "", x["name"])) ] sections.append(f"## {vendor}\n\n| 系列 | 归档文件 | 类型 | 原始链接 |\n|---|---|---|---|\n" + "\n".join(lines)) failed = [i for i in items if not i.get("ok")] failed_text = "\n".join(f"- {i.get('brand')} {i.get('series')}: {i['name']} - {i['url']}" for i in failed) return f"""--- title: 2026-06-05 竞品公开资料归档总览 category: references tags: [bjd, competitors, public-sources, drawings, catalogs, visibility/internal] visibility: internal business_domains: [products, sales, engineering] data_classification: public-web allowed_bots: [bjd-general-bot] sources: - _meta/source-registry.jsonl summary: 2026-06-05 联网补充并归档 Hirose、TE、IRISO、Kyocera、Rosenberger 等竞品官方公开资料,覆盖产品页、目录、规格书、2D 图纸和 CAD/STEP/DFX 文件。 relationships: - target: "[[projects/bjd/references/competitor-cross-reference-map]]" type: supports - target: "[[projects/bjd/references/bjd-fpc-cross-reference-20260422]]" type: supports - target: "[[projects/bjd/references/bjd-io-cross-reference-20260422]]" type: supports - target: "[[projects/bjd/references/bjd-wtb-hs-cross-reference-20260422]]" type: supports provenance: extracted: 0.88 inferred: 0.09 ambiguous: 0.03 base_confidence: 0.87 lifecycle: draft lifecycle_changed: 2026-06-05 tier: core created: 2026-06-05T00:00:00Z updated: {NOW} --- # 2026-06-05 竞品公开资料归档总览 本页是竞品官方公开资料的检索入口。已归档文件保存在 `_sources/archive/bjd/2026-06/competitor/`,并写入 `_meta/source-registry.jsonl`。这些资料用于识别竞品结构、pin 数、规格书、图纸、land pattern、CAD/STEP/DFX 文件和替代评估边界;不能单独作为 BJD 已经完成完全替代或 drop-in 的证据。 {chr(10).join(sections)} ## 待补链接 以下官方直链下载失败,已记录为待补,不计入已归档文件: {failed_text if failed_text else "- 无。"} ## 使用口径 - 客户只给竞品料号时,先查本页或对应厂牌 catalog,确认官方图纸/规格书是否已归档。 - 有 2D 图纸、STEP/DXF 时,仍需工程复核 FPC/FFC 厚度、tab、land pattern、焊盘、插入方向、锁扣、温度等级和可靠性要求。 - 公开竞品资料只用于技术对标;对外报价、具体客户价格、交货数量仍按 [[projects/bjd/references/bjd-general-bot-answer-policy]] 的敏感信息边界处理。 ## 相关页面 - [[projects/bjd/references/competitor-cross-reference-map]] - [[projects/bjd/references/hirose-fh52-fh52k-public-sources-20260605]] - [[projects/bjd/references/te-2188698-73pin-hybrid-connector-public-sources-20260605]] - [[projects/bjd/references/iriso-public-competitor-sources-20260605]] - [[projects/bjd/references/hirose-fh75-fh75m-automotive-fpc-connector]] - [[projects/bjd/references/hirose-fh64ma-kyocera-6817-fpc-connectors]] - [[projects/bjd/references/molex-mini50-connector-system]] """ def fh52_page(items: list[dict]) -> str: rows = [i for i in items if i.get("ok") and i.get("brand") == "Hirose" and i.get("series") in {"FH52", "FH52K"}] lines = "\n".join(f"| {i.get('series')} | {md_link(i)} | {doc_type(i)} | [official]({i['url']}) |" for i in rows) return f"""--- title: Hirose FH52/FH52K 官方公开资料归档 category: references tags: [hirose, fh52, fh52k, fpc, ffc, cross-reference, visibility/internal] visibility: internal business_domains: [products, sales, engineering] data_classification: public-web allowed_bots: [bjd-general-bot] sources: - _meta/source-registry.jsonl summary: Hirose FH52/FH52K 官方产品页、目录、2D 图纸、规格书和 STEP 资料归档,用于 FH52-30S-0.5SH 等料号的替代评估。 relationships: - target: "[[projects/bjd/references/bjd-fpc-cross-reference-20260422]]" type: supports - target: "[[projects/bjd/references/hirose-fh75-fh75m-automotive-fpc-connector]]" type: related_to provenance: extracted: 0.86 inferred: 0.10 ambiguous: 0.04 base_confidence: 0.86 lifecycle: draft lifecycle_changed: 2026-06-05 tier: core created: 2026-06-05T00:00:00Z updated: {NOW} --- # Hirose FH52/FH52K 官方公开资料归档 客户问 `FH52-30S-0.5SH`、`FH52K` 或 FH52/FH75 相邻替代时,优先查本页归档的官方产品页、目录、2D 图纸、规格书和 STEP 文件。BJD 已有 FPC `0.5-18` / `101049-X0XXX0` 对 HRS `FH52/FH28` 的对照线索;具体 30Pin 候选如 `101049-203050` 仍需按客户图纸复核 FPC 厚度、tab、land pattern、下接点/上接点、锁扣方向、连接器高度和包装。 | 系列 | 归档文件 | 类型 | 原始链接 | |---|---|---|---| {lines} ## 回答边界 - 可以说:BJD 有 FH52/FH28 对照链路,可从 FPC 0.5-18 系列评估。 - 不要直接承诺:完全 drop-in 或无需改板,除非工程已比对 Hirose 2D 图纸和客户 PCB/FPC。 - 网上能找到的 Hirose 图纸、规格书和 STEP,已尽量归档;后续客户只给 Hirose 料号时,先检索本页,不必第一时间麻烦客户。 ## 相关页面 - [[projects/bjd/references/bjd-fpc-cross-reference-20260422]] - [[projects/bjd/references/hirose-fh75-fh75m-automotive-fpc-connector]] - [[projects/bjd/references/competitor-public-source-archive-20260605]] """ def te_page(items: list[dict]) -> str: rows = [i for i in items if i.get("ok") and i.get("brand") == "TE"] lines = "\n".join(f"| {i.get('series')} | {md_link(i)} | {doc_type(i)} | [official]({i['url']}) |" for i in rows) return f"""--- title: TE 2188698 / 73Pin 混合端子官方公开资料归档 category: references tags: [te, 2188698, 73pin, hybrid-connector, io, cross-reference, visibility/internal] visibility: internal business_domains: [products, sales, engineering] data_classification: public-web allowed_bots: [bjd-general-bot] sources: - _meta/source-registry.jsonl summary: TE 2188698 相关 customer drawing、73pos instruction sheet 和 2D DXF 文件归档,用于 BJD 73Pin 混合端子产品对标。 relationships: - target: "[[projects/bjd/references/bjd-io-cross-reference-20260422]]" type: supports - target: "[[projects/bjd/references/competitor-cross-reference-map]]" type: related_to provenance: extracted: 0.84 inferred: 0.10 ambiguous: 0.06 base_confidence: 0.84 lifecycle: draft lifecycle_changed: 2026-06-05 tier: core created: 2026-06-05T00:00:00Z updated: {NOW} --- # TE 2188698 / 73Pin 混合端子官方公开资料归档 本页保存 TE `2188698` 相关公开图纸和 73pos instruction sheet 资料,用于评估 BJD `P2.0-2-73PIN` / `140084-417320` 这类 73Pin 混合端子产品。既有 wiki 记录显示该产品用于对标 TE `2420419-1` 方向,并与 TE `2188698` / `2188701` 相关配套项有关;具体替代需工程按 customer drawing、DXF、端子/壳体配合和客户应用确认。 | 系列 | 归档文件 | 类型 | 原始链接 | |---|---|---|---| {lines} ## 关键使用口径 - 可以把本页作为 TE 图纸和 2D CAD 检索入口。 - 不应仅凭 73Pin 和系列名承诺完全替代;混合端子需要复核机械界面、端子排布、锁止结构、线端/板端配套、材料和测试要求。 - TE 产品页 HTML 本次下载失败,已在厂牌 catalog 标为待补;PDF/DXF 资料已归档。 ## 相关页面 - [[projects/bjd/references/bjd-io-cross-reference-20260422]] - [[projects/bjd/references/competitor-cross-reference-map]] - [[projects/bjd/references/competitor-public-source-archive-20260605]] """ def iriso_page(items: list[dict]) -> str: rows = [i for i in items if i.get("ok") and i.get("brand") == "IRISO"] lines = "\n".join(f"| {i.get('series')} | {md_link(i)} | {doc_type(i)} | [official]({i['url']}) |" for i in rows) return f"""--- title: IRISO 公开竞品资料归档 category: references tags: [iriso, btb, fpc, wtb, cross-reference, visibility/internal] visibility: internal business_domains: [products, sales, engineering] data_classification: public-web allowed_bots: [bjd-general-bot] sources: - _meta/source-registry.jsonl summary: IRISO 13065、11507/11607、9827/9828/9860 和产品导览公开资料归档,用于 BJD WTB/BTB/FPC 竞品替代检索。 relationships: - target: "[[projects/bjd/references/bjd-btb-cross-reference-20260422]]" type: supports - target: "[[projects/bjd/references/bjd-wtb-hs-cross-reference-20260422]]" type: supports provenance: extracted: 0.84 inferred: 0.12 ambiguous: 0.04 base_confidence: 0.84 lifecycle: draft lifecycle_changed: 2026-06-05 tier: core created: 2026-06-05T00:00:00Z updated: {NOW} --- # IRISO 公开竞品资料归档 本页汇总 IRISO 官方公开资料。既有 BJD cross-reference 中 IRISO 涉及 FPC、浮动 BTB 和 WTB 多条线,尤其 `9827/9828` 浮动 BTB、`13065` WTB 以及 `11507/11607` 新闻资料。使用时应回到具体 BJD 系列和 pin 数做工程比对。 | 系列 | 归档文件 | 类型 | 原始链接 | |---|---|---|---| {lines} ## 检索建议 - 问 `IRISO 9827/9828/9860`:优先查浮动 BTB 归档 PDF,再回到 BJD BTB 对照页。 - 问 `IRISO 13065`:先查 13065 series page 和白皮书,再回到 BJD WTB/HS 对照页。 - 问 `IRISO 11507/11607`:本次归档的是官方新闻页,若后续找到单独 drawing/spec PDF,应继续补档。 ## 相关页面 - [[projects/bjd/references/bjd-btb-cross-reference-20260422]] - [[projects/bjd/references/bjd-wtb-hs-cross-reference-20260422]] - [[projects/bjd/references/competitor-public-source-archive-20260605]] """ def write_pages(items: list[dict]) -> list[str]: written = [] catalog_dir = VAULT / "_sources/catalog/bjd/competitors" catalog_items = registry_competitor_items() seen = {(i.get("brand"), i.get("name"), i.get("archive_path"), i.get("url")) for i in catalog_items} for item in items: key = (item.get("brand"), item.get("name"), item.get("archive_path"), item.get("url")) if key not in seen: catalog_items.append(item) seen.add(key) for vendor in sorted({i.get("brand") for i in catalog_items if i.get("brand")}): path = catalog_dir / f"{vendor.lower()}.md" path.write_text(vendor_catalog(vendor, catalog_items), encoding="utf-8") written.append(str(path)) (catalog_dir / "index.md").write_text(index_catalog(catalog_items), encoding="utf-8") written.append(str(catalog_dir / "index.md")) pages = { "projects/bjd/references/competitor-public-source-archive-20260605.md": master_page(items), "projects/bjd/references/hirose-fh52-fh52k-public-sources-20260605.md": fh52_page(items), "projects/bjd/references/te-2188698-73pin-hybrid-connector-public-sources-20260605.md": te_page(items), "projects/bjd/references/iriso-public-competitor-sources-20260605.md": iriso_page(items), } for rel, body in pages.items(): path = VAULT / rel path.write_text(body, encoding="utf-8") written.append(str(path)) return written def main() -> None: items = load_results() records = registry_records(items) added = append_registry(records) written = write_pages(items) print(json.dumps({ "download_ok": sum(1 for i in items if i.get("ok")), "download_fail": sum(1 for i in items if not i.get("ok")), "registry_added": added, "pages_written": written, }, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()