app
AI Flow: JLCPCB
Public Made by Adomby adom
The whole JLCPCB order, from gerbers to assembled boards in hand: proven part swaps, design rules, rotation checks, quote and order.
main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
#![allow(dead_code)] // template helpers: keep the ones your stage uses
//! Adom Bridge from a flow app: every desktop action (a browser through pup, a native app, a window
//! recording, files to and from the desktop) is one `adom-bridge` call. The app runs on Linux; the
//! desktop it drives is whatever `--target` names. Copied from aiflow-bridge; keep what you use.
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Clone)]
pub struct Bridge { pub target: String, pub ai_thread: String }
pub struct ClipStop { pub remote: Option<String>, pub local: Option<PathBuf>, pub pulled: bool, pub reply: Value }
impl Bridge {
/// One verb with JSON args. Gated verbs need a per-call `reason` inside `args`.
pub fn call(&self, verb: &str, args: &Value) -> Value {
let out = Command::new("adom-bridge").args(["--ai-thread", &self.ai_thread, "--target", &self.target, verb, &args.to_string()]).output();
match out {
Ok(o) => {
let text = String::from_utf8_lossy(&o.stdout).to_string();
let v: Value = serde_json::from_str(&text).unwrap_or_else(|_| json!({"status": "parse_error", "raw": text.chars().take(400).collect::<String>()}));
// some verbs wrap their answer as a JSON string in `output`
match v.get("output").and_then(|o| o.as_str()).and_then(|s| serde_json::from_str::<Value>(s).ok()) { Some(inner) => inner, None => v }
}
Err(e) => json!({"status": "error", "error": format!("adom-bridge not runnable: {e}")}),
}
}
/// A field from a reply, at the top level or under `data` (verbs answer both ways).
pub fn field<'a>(v: &'a Value, k: &str) -> Option<&'a Value> {
v.get(k).or_else(|| v.get("data").and_then(|d| d.get(k)))
}
pub fn ok(v: &Value) -> bool {
v.get("success").and_then(|s| s.as_bool()) == Some(true) || (v.get("status").and_then(|s| s.as_str()) == Some("ok") && v.get("success").is_none())
}
/// Record ONE window (Windows Graphics Capture: works in the background, nobody at the desk is
/// disturbed). Capped at an hour whatever the AI forgets. Returns the recording id.
pub fn record_window_start(&self, hwnd: i64, reason: &str) -> Result<String, Value> {
let r = self.call("desktop_record_window_start", &json!({"hwnd": hwnd, "fps": 30, "reason": reason, "maxDurationMs": 3_600_000}));
Self::field(&r, "recordingId").or_else(|| Self::field(&r, "id")).and_then(|v| v.as_str()).map(str::to_string).ok_or(r)
}
/// Stop a recording and pull the file into `save_to`, checked: the local copy must exist and be
/// non-empty. The desktop cleans its recordings folder, so pull now or lose the clip.
pub fn record_stop(&self, id: &str, save_to: &Path) -> ClipStop {
let r = self.call("desktop_record_stop", &json!({"recordingId": id}));
let remote = Self::field(&r, "filePath").or_else(|| Self::field(&r, "path"))
.and_then(|v| v.as_str()).map(|s| s.replace('\\', "/"));
match remote {
Some(p) => { let (pulled, local) = self.pull(&p, save_to, 3); ClipStop { remote: Some(p), local: Some(local), pulled, reply: r } }
None => ClipStop { remote: None, local: None, pulled: false, reply: r },
}
}
/// pull_file one desktop file into `save_to` until it lands non-empty (at most `attempts`).
pub fn pull(&self, remote: &str, save_to: &Path, attempts: usize) -> (bool, PathBuf) {
let local = save_to.join(remote.rsplit(['/', '\\']).next().unwrap_or(remote));
for k in 0..attempts.max(1) {
self.call("pull_file", &json!({"filePaths": [remote], "saveTo": save_to.display().to_string(), "reason": "flow app: bring a desktop file into the project"}));
if std::fs::metadata(&local).map(|m| m.len() > 0).unwrap_or(false) { return (true, local); }
if k + 1 < attempts { std::thread::sleep(std::time::Duration::from_secs(3)); }
}
(false, local)
}
/// A background screenshot of one window, copied to `dest`.
pub fn screenshot_window(&self, hwnd: i64, dest: &Path) -> Option<PathBuf> {
let r = self.call("desktop_screenshot_window", &json!({"hwnd": hwnd, "reason": "flow app: evidence screenshot"}));
let d = r.get("data").cloned().unwrap_or(r.clone());
let shots = d.get("screenshots").and_then(|s| s.as_array()).cloned().unwrap_or_else(|| vec![d.clone()]);
let local = shots.iter().find_map(|s| s.get("localPath").or_else(|| s.get("savedTo")).and_then(|v| v.as_str()).map(str::to_string))?;
std::fs::copy(&local, dest).ok()?;
Some(dest.to_path_buf())
}
/// JavaScript in a pup (Puppeteer) browser window. No OS cursor moves, so recordings stay clean.
pub fn pup_eval(&self, window: &str, expression: &str, reason: &str) -> Value {
self.call("pup_eval", &json!({"window": window, "expression": expression, "reason": reason}))
}
}