From 7ff9751504c767b88dafac386687c9bb33417f07 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 15 Jul 2026 08:02:56 +0000 Subject: [PATCH] Finalize Altium 365 Part Request end task: build_part_request_manifest.py (typeid->component-type), runner review-mode (fill+leave for Save), full end-to-end workflow in SKILL.md --- SKILL.md | 57 ++++++++++----- assets/CHANGELOG.xlsx | Bin 5659 -> 5662 bytes references/part_request_web.md | 15 ++-- scripts/build_part_request_manifest.py | 94 +++++++++++++++++++++++++ scripts/local_runner.py | 8 ++- scripts/runner_config.example.json | 8 +-- 6 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 scripts/build_part_request_manifest.py diff --git a/SKILL.md b/SKILL.md index efd5073..e31dc7b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -376,26 +376,42 @@ Request Id. Full field mapping, prerequisites, and the exact browser steps are i Because the files upload from local disk, commit each component's `.SchLib`/`.PcbLib`/datasheet to the operator's machine (device bridge) first, and put those local paths in the manifest. -### Fully automatic end-of-run (no operator action) +### The end-to-end workflow (this is the standard run) -To make the Part Request the automatic tail of every run, the end task doesn't drive the browser -from the cloud (it can't reach the operator's desktop Chrome). Instead: +When the central library is a managed Altium 365 Workspace, a full run goes: -1. After the Gitea push, the skill **writes `part_requests.json`** for the component(s) processed - this run, and **commits it plus each component's `.SchLib`/`.PcbLib`/datasheet to an *inbox - folder on the operator's machine*** via the device bridge (e.g. - `.../altium-library-master/_part_request_inbox/`). Use the local file paths (under that inbox) - in the manifest's `files`. -2. A one-time **local runner** on the operator's Windows machine (`scripts/local_runner.py`, set - up as a Task Scheduler job at logon or a startup shortcut, configured via - `runner_config.json`) watches that inbox, ensures Chrome is up with the debug port on the - signed-in profile, runs `altium365_part_request.py` on the manifest, and archives it. +1. **Datasheet + make** → extract parameters, classify to a typeid, fill the per-part Excel + workbook, human-verify (the existing flow). +2. **Operator provides the `.SchLib` + `.PcbLib`** → write the full parameter set into the + `.SchLib` with `schlib_write.py --typeid ` (template columns + SOP params, plus the + ComponentDescription), exactly as before. +3. **Push to Gitea** (part folder + symbol/footprint, per the normal push steps). +4. **Prepare the Altium 365 Part Request and hand it to the local runner** — the automatic tail: + a. Build the manifest: + ```bash + python scripts/build_part_request_manifest.py --params --typeid \ + --file /.SchLib --file /.PcbLib --file /_datasheet.pdf \ + --out /part_requests.json + ``` + (`build_part_request_manifest.py` maps the typeid to the Workspace Component Type — e.g. + `CER → Capacitors` — pulls manufacturer/MPN/Description/parameters from the params, and lists + the local attachment paths.) + b. **Commit the `.SchLib`, `.PcbLib`, datasheet, and `part_requests.json` into the operator's + Altium Runner inbox** on their PC via the device bridge (the `inbox` folder in + `runner_config.json`, e.g. `C:/Altium Runner/inbox`). The manifest's `files` paths must be + those on-disk inbox paths. +5. **The local runner finishes it, hands-off.** `scripts/local_runner.py` (installed once on the + operator's machine as a Task Scheduler job at logon) watches the inbox, ensures Chrome is up + with the debug port on the signed-in profile, and runs `altium365_part_request.py`, which + **fills the Part Request completely — fields, parameters, and all three attachments — and + leaves it on screen without saving** (review mode). The operator just **reviews and clicks + Save**. Nothing is submitted without their click. -So each run ends with the skill dropping the manifest+files into the inbox; the local runner -submits the Part Request on its own. The only standing requirements are that the operator's -machine is on and its Chrome profile has been signed into the Workspace at least once (the runner -relaunches Chrome with the debug port and the persisted session is reused). This keeps the -per-component Claude-token cost at zero for the submission step. +So the operator's only actions per component are giving the datasheet+make, then the +`.SchLib`+`.PcbLib`, then reviewing the filled Part Request and clicking Save. Everything between +is automatic, and the submission step costs zero Claude tokens. Standing requirements: the +operator's machine is on, the runner is installed, and its Chrome profile has been signed into the +Workspace once (the runner relaunches Chrome with the debug port and reuses the session). ## Per-typeid versioning @@ -563,10 +579,13 @@ plain flat push, but it does not merge the changelog or blank the token, so pref - `scripts/altium365_part_request.py` — standalone **Selenium** submitter: attaches to the operator's signed-in Chrome and loops the `part_requests.json` manifest, filling and saving each Part Request. Token-free per component; the token-economical end task for a whole library. +- `scripts/build_part_request_manifest.py` — build/append `part_requests.json` from a component's + schlib `params.json` + local file paths; maps typeid → Altium Component Type. Run at the end of + a run, before committing the manifest+files to the operator's inbox. - `scripts/local_runner.py` (+ `runner_config.example.json`) — the operator installs this **once** on their machine; it watches the inbox for manifests the skill drops there, ensures Chrome's - debug session, runs the Selenium submitter, and archives each manifest — making submission the - automatic, hands-off tail of every run. + debug session, and runs the Selenium submitter in **review mode** (fills everything, leaves the + form for the operator to review + Save) — the automatic, hands-off tail of every run. - `assets/template/versions.json` — per-typeid `template_version` + `skill_version`. - `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add; merged into the skill repo's copy in Gitea by `push-skill`). diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index c08a63791a329fd1f1a7bf4b9a17975938dab460..7aaf24400c64e11b1034d63fd504086cb90a2b01 100644 GIT binary patch delta 2122 zcmY*aXHb)g7EK5xARv-ZLXjR_Kututbc0J3Q7HjK6QqOmQ2Z!SdIA#|kd1Lrxlgu^+y zaq`7RD&>R&S3DO(bh~%HH{0x6>325WTRm>lsVt8`3-Qu*hcNkNLFq*~Eg?F(KchQF zq_BjA;AiCCwB=%_;lcbS2n-L>c}hg~vFXE7{_^re*PHqeO#%du+X#Sfw74x#_L>)G zWdZUBy)P$_!yMJhn6d?T8x}qWT<7_p1_vdDnXJX!U*9Se4}Z&8V|~Ko)Svmu4|z)f z7^Q`o8(k+!PRotkksXRO3~^~(emb;$*7R-kidy~NQX@}e&uH_mRQs2L0C|z1tI;Yi zp*9LT4XKN;pZak9SD=~=upTG(fkdu4{oQpWJ*cX%d+gmMqL&sq&pvF-$3^OCiZ5P4 zlR!#=x&Q&bWN-SX%tVffR>9RwSD9zC_eXSP&{FOsk!*}M%tzlUeCfdwGHO%kP zCfD*Jxdq(edh$&oEi?;Res`)0>BKxENg5oNoOhec%&q!NFHp?q*&``vN#Zw-nm8Mo z{rxI=mq|+G4g|Izfb#4@=v@u788<9#q`L)c-(Jt?HM9vd-!vrDyEPQMcJla%=;4Gf z@c%os`FX8#Y3W*o9S3)U%EeacuhfK7-z@dxw++;*>zPoN#@(@|@N%3Xd3q#i?nQZ& z@k*BbIZEt!{xrH&^qvHscZPYoDkHU4*stITVy1v0V+yEAD;Smg1QXtu7h13&gzwyj z8*~9Jnsl{A&=iyvx_*Z*-pD;MFx_)9)G*(I8>fw_=uBmZRl(2RnYJF-v_fR6=tbNwK~mx&!kw4Cy>&5}M@VRssh{~emIfH8FA)6!zm=e51V%}eUyI_Hxp zdbVnj{JBMwswMbd6Q&pIL!RA4lNI{|o+P1qVgytEyw-{8IoZi)(NQ1lPn4{icT_CA z|8w(-;ZZ3ppTP{^0O?lEJLl^I8ZncHnRL?L0fT5w5&B-T*S{olxq$`#hPdv@PV1*ca}Rr+RjjBg%rhf-oAw@9hTk zKE}{OWxoW~`8#8rhESn8IGwVH7x8BxOS{I-%NA`pS(K03(*XGQC&6@FzRyGEfq+29 zq9img9I*f7Peuvc6nYIwllN3Mm$B}Gdkv^v!2(~aAe_pGf6^ARb$6rHWFDMNHN%xC5EnUov41AH+` zqyE?ucy71l1>Sb({fZ`+XuwzXqjmOl(Aw}Tu|DpeAgFyAGQj?O=n)ZWe`2`0&$I^{ zAX!w9=Aoe)loytl^#koTm0n=pc;UHtvegV=bHyRUJW+Lcx#e-OL z$X~mTPaP%c>X?kGz1@=w^21NXD71=Q5ESQZz#(RLlr*+%(l8F*wu!AZF+1o);q_v) zrP0*|7te`NZuWx6nwQ7?ZC}le_>3h0R@7$82j~@Mv(>8QK)mEXJ637zC}vm9#?WA6 z9;tS=L%io*S()0_V~D@{3}Ho4X6#`Gr?w)UO)-WUXZof(ncXzxIL?WnX9UBpIVmjc zW<6@AaE1QZ{qY6SD}Oy(rStBLc@R>QRA|z(4__r*Llssd&)26Wd^oxH->ea?soB{g}1+Q1|Sam(pBUok>~<<5wf^+ECK%50ZzL=F5b>3U?-8Z+JK#Y zoF4wRD<<~If|+EzdrG)$-XA!hzPc}*Ng82QYQkIsql(VW5Z1(vV#-`om1#lhg z0pov`1A+MV!{}GVg2i}2pa|bn!6A5G_u!Kee!f;_U^Y19SEMr?@1!(JwCwX?`(b|Q z*WP}Q1z#Ycg~SeSS_nyiHAvnCJ|L4QLVrjd9CpvbXCuHMQ18CqVgI)sOJWKk JIp6&p{s;Pr&9ndj delta 2134 zcmZ8jdpOhW8{Zr!r{0;UkkjVO3d>=&!U$`TdLxv>oMu7}&GzbB*b)&I$wp40C`3|5 z$fu?*JJP!+GL<}A@>;J`L03@U0X8yV(h-4|{Vo>Bb4 zo#bZd&C) lX-*tC~FLA9i#%?VEeF>b#KZw6KLdZ*OnvD^CSh;oQz|W0~tt)CAT7 zR4HR$5)9YeW@LlVE27Dil}T!`dnMlibbtCz>B0QxS?Yu!F&tiQ4G0g^tp_0#y~E}j zF8Ql<_W{)r;)6QoHVwrqO~-!tbf;JnzIXOL}d_7Q=b*eM=sX;O}?mXABkOS zCUh_C8)s_rqdMAk720Qid|2TK)MqSwFMPAOHefoMSO_csD$B3QuNdQcb{YM<5dx1} z9xRQTHL$su#8yLMPN^avGcXX-9UPGHxCSp0Wl&Q)^QcPJ6$jT0@pX2_Tncz={Y`QFW*i6zs*7=!7&UbLn}ug(~1f{fFl zYuTv?FPFY7=NI0`GW~AH8G!VW7u%Z89w_Fnw8+Opb zd{6AV<-qN4hglaA$fr-|UMQRCKC6*qho6+J7Pp6%!1T)877d0|mm1f4)B%%YFE3xz zr2X+BJOBwNqMEd86R~b#Rq^L>@d$F1hZ&oqMQPlyHd-YXO+w}tJO8xh7ChuN*qR8; zKOQj18-|uM6_Qj>fUDK_Lj}1Xj+A2iJ=!d@La$qvV_)ZeG&A+wErD_D;#-%iC9Z}B z=RfJlNDPr4qg*AZ>zKRV0xS@dK0}zjvp`0Wjm5~}{Z)IT60#2-3&APtTJnNz7Md6z z|1j6UWaD4;J9gbGiLP-U@e#!#H zLx?!t8S3}Xd0e~nH<8m_A7in1=VeSopSD%rAY#N0-#l517_sdA%T|MyX6j7Q<>Uv$ z;W6UHIpXE*>f4B|yMTtE70RjdZjQ}pS=-sAmS~s636k4#f4-;v(MG#gISiaN3pZ{m z_FoNrrH3TMNq*EZpdr?w`Xdb5O(0rK_llWmaDs@hmoYmd0MnLDX1bTUU0ZNIH|733 zng_j8MvA8Rm~|`(&C}vTCSo?NkEP!Fn7`qW!}(J z7taY49#HGdl;Q_}@UZX6(;7LouVR7V{ru2?UJ+~5I!(~-T6;$(N*g`gc{?J|TXq-U z-JEaueF_?9rlN z%OLlQ?C}Z`k*EMfwa(H37WoWZlls@2v8#X9UUnFz79X!B`CNu7Cp}ge=XNFhzIF;r z!N7y|OdZZ$2iCqO1*F$>xqZ+-zr#G4h3!@kNCz6byv&+$czcC_;@;_&WsA9IEtBAj z_)oLc4*6iZTl+if>s3b8|HKlX91!Tlm1=6A5ZFx+T)oxNyzX zy&{y}ks{}W6IyYcM$DiKlpa1~3yRQ#SW8_l{ml+3nh&;a?O}`GZoD4Hqw1OgH%8Zm z{@+1vCN(HEZw`!{5C|l>iD%C2!jCpy4Sm)2j F{{R6a&y)ZF diff --git a/references/part_request_web.md b/references/part_request_web.md index c9ef0a2..06ae855 100644 --- a/references/part_request_web.md +++ b/references/part_request_web.md @@ -104,10 +104,17 @@ Run it: python scripts/altium365_part_request.py --manifest part_requests.json \ --base https://.365.altium.com [--review-first | --no-submit] ``` -`--no-submit` fills every form without saving (dry run); `--review-first` pauses after the first -form so the operator can eyeball the mapping before the batch submits. The skill writes the -`part_requests.json` manifest and commits each component's files to the operator's disk (device -bridge) so the local `files` paths resolve. +Default operation is **review mode**: the runner (`local_runner.py`) invokes the submitter with +`--no-submit`, so it fills the form completely — fields, parameters, and all attachments — and +**leaves it on screen for the operator to review and click Save**. This is the review-first gate +and works even unattended (a console "press Enter" can't). Set `"auto_submit": true` in +`runner_config.json` only to submit without review. (`--review-first` is a console-only variant +that pauses for Enter.) + +Build the manifest with `scripts/build_part_request_manifest.py` (it maps the typeid to the +Workspace Component Type and pulls manufacturer/MPN/Description/parameters from the schlib +`params.json`). The skill writes `part_requests.json` and commits it + each component's files to +the operator's inbox (device bridge) so the local `files` paths resolve. ## Safety and auditing diff --git a/scripts/build_part_request_manifest.py b/scripts/build_part_request_manifest.py new file mode 100644 index 0000000..b7bc57c --- /dev/null +++ b/scripts/build_part_request_manifest.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Build (or append to) a `part_requests.json` manifest for the Altium 365 Part Request end task, +from a component's schlib `params.json` plus its local file paths. The skill runs this at the end +of a run, then commits the manifest + the component's `.SchLib`/`.PcbLib`/datasheet into the +operator's Altium Runner inbox; the local runner picks it up and fills the Part Request. + +Usage: + python build_part_request_manifest.py --params --typeid CER \ + --file "C:/Altium Runner/inbox/JMK105BJ105KV-F.SchLib" \ + --file "C:/Altium Runner/inbox/JMK105BJ105KV-F.PcbLib" \ + --file "C:/Altium Runner/inbox/JMK105BJ105KV-F.pdf" \ + --out "C:/Altium Runner/inbox/part_requests.json" + +`--component-type` overrides the typeid→type mapping; `--assignee` sets the librarian. +""" +import argparse, json, os + +# Vecmocon typeid -> Altium 365 Part Request "Component Type" (options captured live from the +# form's dropdown). Edit as your Workspace's component-type list evolves; --component-type wins. +TYPEID_TO_COMPONENT_TYPE = { + "CER": "Capacitors", "ELE": "Capacitors", "TAN": "Capacitors", "PLY": "Capacitors", + "FLM": "Capacitors", "SFY": "Capacitors", "SUP": "Capacitors", + "FIX": "Resistors", "TFR": "Resistors", "MFR": "Resistors", "CFR": "Resistors", + "MOR": "Resistors", "WWR": "Resistors", "SHT": "Resistors", "ARR": "Resistors", + "POT": "Resistors", "FSR": "Resistors", "NTC": "Resistors", "PTC": "Resistors", + "PWR": "Inductors", "FBD": "Inductors", "CMC": "Inductors", "RFI": "Inductors", + "XFM": "Inductors", "CTX": "Inductors", "CPL": "Inductors", + "REC": "Diodes", "FRD": "Diodes", "SCH": "Diodes", "SIC": "Diodes", "ZEN": "Diodes", + "TVS": "Diodes", "ESD": "Diodes", "SWI": "Diodes", "BRG": "Diodes", "LED": "LED", + "BJT": "Transistors", "MOS": "Transistors", "SCM": "Transistors", "GAN": "Transistors", + "IGBT": "Transistors", "JFET": "Transistors", "DIG": "Transistors", + "MCU": "Processors", "LDO": "Power Supply", "DCD": "Power Supply", "PMU": "Power Supply", + "BMS": "Integrated Circuits", "DRV": "Drivers", "AMP": "Amplifiers", "CMP": "Amplifiers", + "VRF": "Data Converters", "ADC": "Data Converters", "DAC": "Data Converters", + "ISO": "Optoelectronics", "XCV": "Interface", "AFE": "Integrated Circuits", "MEM": "Memory", + "LOG": "Logic", "SEN": "Sensors", "IFC": "Interface", "CLK": "Crystals & Oscillators", + "SVR": "Power Supply", "MTR": "Integrated Circuits", + "FUS": "Fuses", "RSF": "Fuses", "VAR": "Miscellaneous", "GDT": "Miscellaneous", "CBK": "Switches", + "DCM": "Power Supply", "IDC": "Power Supply", "INV": "Power Supply", "OBC": "Power Supply", + "CHG": "Power Supply", "PSU": "Power Supply", "RCM": "Power Supply", + "RLS": "Relays", "RLP": "Relays", "SSR": "Relays", "RLR": "Relays", "CTC": "Relays", + "SWT": "Switches", "PBT": "Switches", "DSW": "Switches", "RSW": "Switches", "RSY": "Switches", + "CWB": "Connectors", "CBB": "Connectors", "HDR": "Connectors", "FFC": "Connectors", + "USB": "Connectors", "PWC": "Connectors", "TBK": "Connectors", + "ANC": "Radio&RF", "ANP": "Radio&RF", "ANE": "Radio&RF", "SAW": "Radio&RF", "RFM": "Radio&RF", + "XTL": "Crystals & Oscillators", "OSC": "Crystals & Oscillators", "MMO": "Crystals & Oscillators", + "RSN": "Crystals & Oscillators", + "CLI": "Batteries", "CLF": "Batteries", "CCO": "Batteries", "CNI": "Batteries", + "BPK": "Batteries", "CHL": "Batteries", + "BUZ": "Audio", "PBZ": "Audio", "SPK": "Audio", "IND": "Optoelectronics", + "DSG": "Optoelectronics", "OLE": "Optoelectronics", "LCD": "Optoelectronics", "TFT": "Optoelectronics", + "STE": "Sensors", "SCU": "Sensors", "SVO": "Sensors", "SHA": "Sensors", "SIM": "Sensors", + "SPR": "Sensors", + "FAN": "Mechanicals", "HSK": "Mechanicals", "TPD": "Mechanicals", +} + + +def build_entry(params, typeid, component_type, files, assignee): + p = params.get("parameters", {}) + ctype = component_type or TYPEID_TO_COMPONENT_TYPE.get((typeid or "").upper(), "Miscellaneous") + mpn = p.get("Manufacturer Part") or params.get("component") or "" + return { + "manufacturer": p.get("Manufacturer", ""), + "mpn": mpn, + "description": p.get("Description", ""), + "component_type": ctype, + "assignee": assignee or params.get("assignee", ""), + "parameters": {k: v for k, v in p.items() if v not in ("", None)}, + "files": files, + } + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--params", required=True, help="the schlib params.json for the component") + ap.add_argument("--typeid", help="component typeid (auto-maps to an Altium Component Type)") + ap.add_argument("--component-type", help="Altium Component Type (overrides the typeid map)") + ap.add_argument("--assignee", default="", help="librarian to assign the request to (optional)") + ap.add_argument("--file", action="append", default=[], + help="local path to attach (repeatable): .SchLib, .PcbLib, datasheet") + ap.add_argument("--out", required=True, help="part_requests.json (created or appended to)") + a = ap.parse_args() + + params = json.load(open(a.params, encoding="utf-8")) + entry = build_entry(params, a.typeid, a.component_type, a.file, a.assignee) + data = json.load(open(a.out, encoding="utf-8")) if os.path.exists(a.out) else {"requests": []} + data.setdefault("requests", []).append(entry) + json.dump(data, open(a.out, "w", encoding="utf-8"), indent=2, ensure_ascii=False) + print(f"wrote {a.out} (+1 request: {entry['mpn']} -> {entry['component_type']}, " + f"{len(a.file)} file(s))") + + +if __name__ == "__main__": + main() diff --git a/scripts/local_runner.py b/scripts/local_runner.py index 7020839..fdb8553 100644 --- a/scripts/local_runner.py +++ b/scripts/local_runner.py @@ -74,8 +74,12 @@ def process(cfg, manifest): "--out", manifest + ".result.json"] if cfg.get("chromedriver"): cmd += ["--chromedriver", cfg["chromedriver"]] - if cfg.get("review_first"): - cmd += ["--review-first"] + # Default is REVIEW mode: fill the form completely (fields + parameters + attachments) and + # leave it on screen WITHOUT saving, so the operator reviews and clicks Save themselves. + # This is the review-first gate and it works even when the runner runs unattended (unlike a + # console "press Enter"). Set "auto_submit": true in the config only to submit without review. + if not cfg.get("auto_submit", False): + cmd += ["--no-submit"] subprocess.run(cmd, check=False) diff --git a/scripts/runner_config.example.json b/scripts/runner_config.example.json index 6f79490..5bf9a01 100644 --- a/scripts/runner_config.example.json +++ b/scripts/runner_config.example.json @@ -1,11 +1,11 @@ { "base": "https://vecmocon-technologies-pvt-ltd.365.altium.com", - "inbox": "D:/User Data/Desktop/altium-library-master/_part_request_inbox", - "processed": "D:/User Data/Desktop/altium-library-master/_part_request_inbox/processed", + "inbox": "C:/Altium Runner/inbox", + "processed": "C:/Altium Runner/inbox/processed", "chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe", - "user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data", + "user_data_dir": "C:/altium-runner/chrome-profile", "debug_port": 9222, "chromedriver": null, - "review_first": false, + "auto_submit": false, "poll_seconds": 30 }