From 6ad4e9b83034e783f6006f4ebf849b13742b6a28 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 15 Jul 2026 06:21:52 +0000 Subject: [PATCH] Add local_runner.py + config for hands-off end-of-run Part Request submission (cloud drops manifest -> local watcher auto-runs Selenium) --- SKILL.md | 25 +++++++ assets/CHANGELOG.xlsx | Bin 5655 -> 5657 bytes scripts/local_runner.py | 104 +++++++++++++++++++++++++++++ scripts/runner_config.example.json | 11 +++ 4 files changed, 140 insertions(+) create mode 100644 scripts/local_runner.py create mode 100644 scripts/runner_config.example.json diff --git a/SKILL.md b/SKILL.md index 5255812..efd5073 100644 --- a/SKILL.md +++ b/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 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`). diff --git a/assets/CHANGELOG.xlsx b/assets/CHANGELOG.xlsx index 39139a0dfe58a638ab764b72b77930bfc5cee441..ad4994cd3f46c1c0c77f544537057301ee9c372b 100644 GIT binary patch delta 2263 zcmZWrc{J30ADvp9+&9)0ndMBu+*=6%%4S zwy_LDCMBa{@?5WYdEfWE-+w;mo^wCkl@<0;<8b); zx4PhY^Ci#;=C76SwNrhN;HKNJX%c%`flUg@L+Id+qJL%H6tD>)`Bfy_*9FClo~gzq zGZi6`Cs(B`uK3+N`__Y!Ay$x{-aFT)5UdtHHvx+pxG7yiD-7g-jJ^W2a)_E-`y38y z40R9+ZHMg7KA9pnsMkf`JQTkOI)Nrk<++WoC;GplM~i@?oJfaOnhGNjvz)&-w2z-5 zq(;OS{qP#&Mk#sf(sc~4Q^Zvbm7e=g6pd^ROLNeS>} zl>GIi4YcbCWXF`+Z{SnHqC2(#>2W#~Gf_0)8#7@foAH~-JkNr5G4~n;rxq@eaVEui z7veue1S;1M@rehHr~TSg`jCM^+;F)Bx8o!wub$jAhOS#Kx`{txMKEKIF1tL-y21O? zI6b@S#5wbvC?m@dw;SE2yZ7c{8|#x zA?OIzcq;#1c5}NSIYxyL%($?oTXu$zG`c4KAgFn8{}IlD&>my_)U$Msnj!D7ox{=i z$j2KZ%X7u;;2x}AH-Qm%7yYDgq8dQ^^NuJ|-i>|2+;UwrGd7Osm}KPzcw0 zFy+wK64>cGD%TEvEwQT{5v{7+S{u`z3~?LcI}^>wAxEqYrSESYohA2NsRe}dy(YXf zOw6od(3Cr5`%%)q;i(sMTcw~p&)A{kFYH9 zNV!vfdz_?x4{v;3V=XOROz;$$Ey;un2ev6D3_pn~gv{K zk5r%G8Ws`MRZWD(m?)BFx8KrKZ;%R*pBU#;S06TnA5a;ANS}KF)|9vOE#CjmdKynu zR*7|zX)4JW6cZ*#<|5ACuC8sv%b>JQq(z)wd5-A7+b(_7v~}===48KBa#4Zocy&+`Z?C9IeeJr>mC(mtRe00PyJ+Ii z?scNN)~>-{R8?{pJQ_T~h*xbBh%B8>3Gq@8@In81bVHySbx3^drJn%WSvCDq`1roY z(N8e4cJJQ8Q>VCB%SOESIrcQOCsxO0ug-)r4DWmAX?M^DnYq6fC7vTL|*O*JjaUn zb7gNt7)tTkxVL#_;du%OZ*c*r~Gxnt@JtJLt}-C-Qi6WPyrgUrs{ zWi3zLsua3roRtf`dexu+@;MwJ_s^*p3ONSY<5;~2WPgBz(74v6NF1x1kr}FDQ+}5i zW|gjcOVg$(wBlite9gj-d(FAxX{A}0RoO3B4%fUSF&{O`m~k~O-yANZdKtP$zUsn0 zUAj+QKd4=sAJkt-hE9NFkAazHEn5AS1wVLag7N$AQmH+k34 z^7yi0%Nn13jT7Bc54||fkji?U0hP(RyU6-XMVOU3&EW-fm92N>8Z%O`VOp5#o2?Dw zaNg440Xn4S>eI%P7FcfkJMCag(^QIbqJLdw^zcmA8b2TERbU>3B2eFQ2kFrW(Icxf z$}Z=I&>$V+ zd);FCA2V0a1nOzVsqG%%dBxj2lAzgi&uQ3GEvDosqY;hKms~AtJ3G0z(h=GKq}2Gr z&tN7-QrWLni29NX);_lmgQN=3#D%1#?kx>dWNSso5P&=12LA7$P~4=y2yB;_nM@#f zdr}_3CU;;u-1gt_4Z`$4jZfJlYqZ00ltqE(DG{=2%Db)CupP85C#fIk!yn}`pN(MyQLJKKQ;WCeFG4vGvC1-7N=A{BN%#%u#a xN(fR(Wk*z9skrRfw#Wt;1XB1P(L5NX28N=vBcVH6?Cq^2$~+P++>O|w{R4L77=Zu) delta 2256 zcmZWr2T&8(7EK63lzMlqZQ2{l8L2Bqoz=c2K{{8dcn>TmP%sDe}&dix}A4_FQLClOnj4%KI00zX%r$RbU z$IGu_G!vN<_fp>>@J3y7(0E6KKtGg6y%BY%;Ep*|N^C|~p_$M7!^5PygPoG##tJG3 z{K{=%NM%;rbEQ~-K{Kw?Rq#H|CD0yGyu;Uk+F%#_en}m^nMSn;(qJqVvnR8)jIMbm znE5+I>zNy-5_q!q)BB!$z?Fd{c;B4!+am5F^6i`qK_OK9i&8L>tK&&nftHCqE4Bk7 z43UY14?piJ{(%Zv1Zaqs;wHycQ}(T8Ue(1!P4*^yyEQ$qlWJKl-%9*#w38B_Bif$U z7`k%F>x_dXPy1U&zf+l+aZ-LW?vsc}tg{MtbNx^9Uio}F%s_oM={mubwf)Fo^YlNK zfQ~(>-34kfflpkiXsM!+G{K^HKfkZ4(4KZ;`prs4Q*iW(khTQAe90p`A;W&B{wmW~ za$;?R#h(#B_)x%Y+nDec4@%dN+robLj@&1fR~F(?qSZV@6Me?@GE8mNUttR6tNK7S z%_;EX8_9aFwylJsMbpJ7k=%?CT7^=(J>>5*x+w_-5FlyM2j5ZJvX+m&1V(OI3N`9&+i)KzO(GKT+fvnS?p`xy~4m}8+d5C zj8EvPc$Ub;QdQpW<~X$Sd9KMa%T%}rqL4B9bJ2@Jv(xc6genu(TLsmHsmg@aaGjq? zf}V^c8ah8QMy-Zs>8q3VH+u&ED52!T!jXgCN$xTdyI!Nh59Sx}d>GHf>pb7LxDDjC z(1r@GTE3Q~zslDV7_41JOl6lcG33`{ zs?l#!3MWxyGUq3-_KO!1`hUVFyl+F6_1P>zNABq zo*`#DNsi&eo9c)G6Xz%Z^0fFJC*ji#v<)W3%H7Qr87cJMkM)cI?WomF_qC& zL~1)DErE#ms{wh0)2agxa@6J{t_mT-FgzxSPm*QNNW0wNRwlJKOfce!8zTF?H;QGLjIh04>-&aI8-pEn4@+k?D0tvMsXPwl8fGNYs{a%S0APh5KavuJ1MmA(4jKvX0wrC* zNtp@+lubdhf^#u~tx4yq3AO_^mej;k%o~BvBs=ZTQY~MCfeJ1EzMrFJOgO*k|CMArBY}%|3IBRX#(XomCt?UQx zAE`2Z^A1$0?6{HNt?)(xkEC=_awSFiox)#inuM}fV6_cJ&U9)Q3v}+0F&w8C=l+HN z!ZgWZXc}U@6Ui%D#Na%KY+=Vv9%Na6yo@zV+D!H3SMU6Ng1xeg-A=KFTk>xHnIANA z4f?L6LCR+L!Mck!N=;JGt-dC#!V^nWZ8p`6XtJ?Y;h<8-@tTm$9LrNy*y50aZQ__Y zUz3X!>vd^6F~Yd5vb&j_`Bfcn7my1P;!SePuUbKV!+vV;xAS%S(Axxc_L0tE0rmub zju3jobDe#Oxx_(jo-UAmSGKocFu6jHr%Ce&qs_uR%uOD)e2q|7@nwiEnaN%&`#9G~ zXOa$2`w`-YH;0BCxyqtjE$ZBuHMG)%fddEhxd{1f-?<{6eOi32kZp2x-d8 zAYOWmTQ{6H)VAWjXkKiinW~-0HvjSdIu@r}aa3_3CYM-ub}KJowz~ZpEq(6am*vro zcAMYlg-TJiCY+Hmh1^-$Gn%5yJ@cGLHmlPV|dkT?9U6H zCTDgsRoFf1$ercuVTD@r-`i@irBoz`_ANz3RW5p+C)^^2A~VDtyoUFdnC>ZUS1WTe zL*Gj{wSs7e0ePkLEY*0c?j~nHxvHxcCZJa8+Q$A9O9^xN5Q496X&1$71Rd0L%YKuaIQnyeI!LhVujE fNjSK`3AlF*(n!T{@e?q748lk&a8V{Q$BFiD>C6Q- diff --git a/scripts/local_runner.py b/scripts/local_runner.py new file mode 100644 index 0000000..7020839 --- /dev/null +++ b/scripts/local_runner.py @@ -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() diff --git a/scripts/runner_config.example.json b/scripts/runner_config.example.json new file mode 100644 index 0000000..6f79490 --- /dev/null +++ b/scripts/runner_config.example.json @@ -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 +}