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

main
admin 2026-07-15 08:02:56 +00:00
parent c74c9d8e22
commit 7ff9751504
No known key found for this signature in database
6 changed files with 153 additions and 29 deletions

View File

@ -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 <ID>` (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 <part params.json> --typeid <ID> \
--file <inbox>/<MPN>.SchLib --file <inbox>/<MPN>.PcbLib --file <inbox>/<MPN>_datasheet.pdf \
--out <inbox>/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`).

Binary file not shown.

View File

@ -104,10 +104,17 @@ Run it:
python scripts/altium365_part_request.py --manifest part_requests.json \
--base https://<workspace>.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

View File

@ -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 <schlib params.json> --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 typeidtype 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()

View File

@ -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)

View File

@ -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
}