Add local_runner.py + config for hands-off end-of-run Part Request submission (cloud drops manifest -> local watcher auto-runs Selenium)
parent
0b2a8ccb92
commit
6ad4e9b830
25
SKILL.md
25
SKILL.md
|
|
@ -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
|
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.
|
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
|
## Per-typeid versioning
|
||||||
|
|
||||||
Versioning is **per typeid**, not global. Each typeid carries its own `template_version` and
|
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
|
- `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
|
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.
|
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/template/versions.json` — per-typeid `template_version` + `skill_version`.
|
||||||
- `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add;
|
- `assets/CHANGELOG.xlsx` — global version/parameter changelog (created on first add;
|
||||||
merged into the skill repo's copy in Gitea by `push-skill`).
|
merged into the skill repo's copy in Gitea by `push-skill`).
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue