Add local_runner.py + config for hands-off end-of-run Part Request submission (cloud drops manifest -> local watcher auto-runs Selenium)

main
admin 2026-07-15 06:21:52 +00:00
parent 0b2a8ccb92
commit 6ad4e9b830
No known key found for this signature in database
4 changed files with 140 additions and 0 deletions

View File

@ -376,6 +376,27 @@ 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)
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:
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.
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.
## Per-typeid versioning
Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and
@ -542,6 +563,10 @@ 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/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.
- `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.

104
scripts/local_runner.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""LOCAL RUNNER — runs on the operator's own machine (Windows), set up ONCE, to make Altium 365
Part Request submission the automatic tail of a skill run.
Why this exists: the library-manager skill runs in the cloud, but the Part Request web form must
be driven from the operator's signed-in Chrome on their desktop — the cloud can't reach it. So at
the end of a run the skill drops the finished files + a `part_requests.json` manifest into an
**inbox folder on this machine** (via the device bridge). This runner watches that inbox and, for
each manifest: makes sure Chrome is running with a debugging port on the operator's profile
(launching it if needed the persisted login is reused), runs `altium365_part_request.py`, then
moves the manifest to a `processed/` folder.
Set it up once (Task Scheduler at logon, or a startup shortcut) and every future skill run
auto-submits its Part Requests with no further action as long as this machine is on and the
Chrome profile has been signed into the Workspace at least once.
Config: a `runner_config.json` next to this file, e.g.
{
"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",
"chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe",
"user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data",
"debug_port": 9222,
"chromedriver": null,
"review_first": false,
"poll_seconds": 30
}
Run:
python local_runner.py # watch the inbox forever (use for Task Scheduler / startup)
python local_runner.py --once # process whatever is in the inbox now, then exit
"""
import glob, json, os, shutil, socket, subprocess, sys, time
HERE = os.path.dirname(os.path.abspath(__file__))
def load_cfg():
with open(os.path.join(HERE, "runner_config.json"), encoding="utf-8") as f:
return json.load(f)
def _port_open(port):
s = socket.socket(); s.settimeout(1)
try:
s.connect(("127.0.0.1", int(port))); return True
except Exception:
return False
finally:
s.close()
def ensure_chrome(cfg):
"""Make sure a Chrome with the debug port is up (reusing the operator's logged-in profile)."""
if _port_open(cfg["debug_port"]):
return
udd = os.path.expandvars(cfg["user_data_dir"])
subprocess.Popen([cfg["chrome"], f"--remote-debugging-port={cfg['debug_port']}",
f"--user-data-dir={udd}"])
for _ in range(40):
if _port_open(cfg["debug_port"]):
time.sleep(2) # give the profile a moment to restore the session
return
time.sleep(1)
raise RuntimeError("Chrome did not expose a debug port — check 'chrome' path and 'debug_port'.")
def process(cfg, manifest):
ensure_chrome(cfg)
cmd = [sys.executable, os.path.join(HERE, "altium365_part_request.py"),
"--manifest", manifest, "--base", cfg["base"],
"--debugger-address", f"127.0.0.1:{cfg['debug_port']}",
"--out", manifest + ".result.json"]
if cfg.get("chromedriver"):
cmd += ["--chromedriver", cfg["chromedriver"]]
if cfg.get("review_first"):
cmd += ["--review-first"]
subprocess.run(cmd, check=False)
def main():
cfg = load_cfg()
os.makedirs(cfg["inbox"], exist_ok=True)
os.makedirs(cfg["processed"], exist_ok=True)
once = "--once" in sys.argv
print(f"watching {cfg['inbox']} (base {cfg['base']})")
while True:
for m in sorted(glob.glob(os.path.join(cfg["inbox"], "*.json"))):
if m.endswith(".result.json"):
continue
print("processing", m)
try:
process(cfg, m)
except Exception as e:
print(" error:", e)
shutil.move(m, os.path.join(cfg["processed"], os.path.basename(m)))
if once:
break
time.sleep(int(cfg.get("poll_seconds", 30)))
if __name__ == "__main__":
main()

View File

@ -0,0 +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",
"chrome": "C:/Program Files/Google/Chrome/Application/chrome.exe",
"user_data_dir": "%LOCALAPPDATA%/Google/Chrome/User Data",
"debug_port": 9222,
"chromedriver": null,
"review_first": false,
"poll_seconds": 30
}