feat: estensione kde-automation per verifica UI KDE Plasma 6
Tool: kde_screenshot (spectacle), kde_click/kde_type/kde_mousemove (ydotool), kde_window_activate (KWin scripting), kde_ui_inspect (AT-SPI). Daemon ydotoold auto-avviato (sudoers NOPASSWD), promptGuidelines + skill kde-verify.
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* kde-automation — Estensione pi per automazione desktop KDE Plasma 6 (Wayland)
|
||||||
|
*
|
||||||
|
* Fornisce tool nativi per:
|
||||||
|
* - Screenshot (spectacle CLI)
|
||||||
|
* - Input simulato (ydotool: click, type, mousemove)
|
||||||
|
* - Gestione finestre (KWin scripting via D-Bus)
|
||||||
|
* - Ispezione UI (AT-SPI via python-dbus)
|
||||||
|
*
|
||||||
|
* Include la prassi di verifica UI come promptGuidelines e una skill /skill:kde-verify.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
import { execFile, spawn } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
|
||||||
|
const exec = promisify(execFile);
|
||||||
|
const SOCKET = "/run/user/1000/.ydotool_socket";
|
||||||
|
const UID = 1000;
|
||||||
|
const TMP = "/tmp";
|
||||||
|
|
||||||
|
// ---------- helpers ----------
|
||||||
|
|
||||||
|
async function run(cmd: string, args: string[], env?: Record<string, string>): Promise<string> {
|
||||||
|
const { stdout, stderr } = await exec(cmd, args, { env: { ...process.env, ...env } });
|
||||||
|
return stdout + (stderr || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Avvia ydotoold (idempotente) se il socket non esiste. Richiede sudoers NOPASSWD per ydotoold. */
|
||||||
|
async function ensureDaemon(): Promise<string> {
|
||||||
|
if (existsSync(SOCKET)) return "ydotoold attivo (socket presente)";
|
||||||
|
try {
|
||||||
|
await run("pgrep", ["-x", "ydotoold"]);
|
||||||
|
return "ydotoold in esecuzione ma socket mancante: riavviare con sudo ydotoold -p " + SOCKET + " -P 0666 -o " + UID + ":" + UID;
|
||||||
|
} catch {
|
||||||
|
// non in esecuzione: avvia
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const child = spawn("sudo", ["-n", "ydotoold", "-p", SOCKET, "-P", "0666", "-o", `${UID}:${UID}`], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
await new Promise((r) => setTimeout(r, 1200));
|
||||||
|
if (existsSync(SOCKET)) return "ydotoold avviato (socket creato)";
|
||||||
|
return "ydotoold avviato ma socket non ancora presente";
|
||||||
|
} catch (e) {
|
||||||
|
return "ERRORE avvio ydotoold: " + String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Attiva una finestra per resourceClass via KWin scripting. */
|
||||||
|
async function activateWindow(resourceClass: string): Promise<string> {
|
||||||
|
const script = `const target = ${JSON.stringify(resourceClass)};
|
||||||
|
const clients = workspace.windowList();
|
||||||
|
let found = false;
|
||||||
|
for (const c of clients) {
|
||||||
|
if (c.resourceClass === target) {
|
||||||
|
workspace.activeWindow = c;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(found ? "ok" : "notfound");`;
|
||||||
|
const scriptPath = join(TMP, "kwin-activate.js");
|
||||||
|
writeFileSync(scriptPath, script);
|
||||||
|
await run("qdbus6", ["org.kde.KWin", "/Scripting", "org.kde.kwin.Scripting.loadScript", scriptPath]);
|
||||||
|
await run("qdbus6", ["org.kde.KWin", "/Scripting", "org.kde.kwin.Scripting.start"]);
|
||||||
|
return "script KWin eseguito per " + resourceClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ispeziona l'albero AT-SPI (applicazioni + primi children). */
|
||||||
|
async function inspectUI(depth: number): Promise<string> {
|
||||||
|
const py = `
|
||||||
|
import dbus, sys
|
||||||
|
bus = dbus.bus.BusConnection("unix:path=/run/user/1000/at-spi/bus_1")
|
||||||
|
root = bus.get_object("org.a11y.atspi.Registry", "/org/a11y/atspi/accessible/root")
|
||||||
|
acc = dbus.Interface(root, "org.a11y.atspi.Accessible")
|
||||||
|
def nm(o):
|
||||||
|
try: return str(o.Get("org.a11y.atspi.Accessible", "Name", dbus_interface="org.freedesktop.DBus.Properties")) or "?"
|
||||||
|
except Exception: return "?"
|
||||||
|
out = []
|
||||||
|
for ch in acc.GetChildren():
|
||||||
|
try:
|
||||||
|
o = bus.get_object(ch[0], ch[1]); a = dbus.Interface(o, "org.a11y.atspi.Accessible")
|
||||||
|
out.append("APP: %s | %s" % (nm(a), a.GetRoleName()))
|
||||||
|
if ${depth} > 0:
|
||||||
|
for k in a.GetChildren()[:${depth}]:
|
||||||
|
try:
|
||||||
|
ko = bus.get_object(k[0], k[1]); ka = dbus.Interface(ko, "org.a11y.atspi.Accessible")
|
||||||
|
out.append(" - %s | %s" % (nm(ka), ka.GetRoleName()))
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
print("\\n".join(out))
|
||||||
|
`;
|
||||||
|
const scriptPath = join(TMP, "atspi-inspect.py");
|
||||||
|
writeFileSync(scriptPath, py);
|
||||||
|
return run("python3", [scriptPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- estensione ----------
|
||||||
|
|
||||||
|
export default function kdeAutomation(pi: ExtensionAPI) {
|
||||||
|
// --- promptGuidelines: prassi di verifica UI KDE ---
|
||||||
|
pi.on("before_agent_start", async (event) => {
|
||||||
|
const guidelines = event.systemPromptOptions.promptGuidelines ?? [];
|
||||||
|
const kdeGuidelines = [
|
||||||
|
"KDE AUTOMATION (desktop Plasma 6 Wayland): per verifiche UI usa i tool kde_* (kde_screenshot, kde_click, kde_type, kde_mousemove, kde_window_activate, kde_ui_inspect).",
|
||||||
|
" - Prassi di verifica: 1) kde_screenshot per catturare lo stato; 2) analizza l'immagine (agy_analyze se serve visione); 3) azione (kde_click/kde_type/kde_window_activate); 4) kde_screenshot di verifica e confronta.",
|
||||||
|
" - ydotool usa la mappa tastiera US: con layout IT i caratteri speciali (trattini, @, ecc.) vanno specificati con keycode o attenzione.",
|
||||||
|
" - Su Wayland non esiste iniezione input nativa: ydotool (via /dev/uinput) è il metodo; il daemon si avvia da solo (sudoers NOPASSWD configurato).",
|
||||||
|
" - Per finestre: kde_window_activate con resourceClass (es. org.kde.konsole, org.kde.dolphin).",
|
||||||
|
" - Dettagli completi: skill /skill:kde-verify e memoria qmem (project host-enne2-gk35).",
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
systemPrompt: event.systemPrompt,
|
||||||
|
systemPromptOptions: {
|
||||||
|
...event.systemPromptOptions,
|
||||||
|
promptGuidelines: [...guidelines, ...kdeGuidelines],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- skill kde-verify ---
|
||||||
|
pi.on("resources_discover", async () => {
|
||||||
|
return {
|
||||||
|
skillPaths: [join(homedir(), ".pi/agent/extensions/kde-automation/skills")],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- comando /kde-verify ---
|
||||||
|
pi.registerCommand("kde-verify", {
|
||||||
|
description: "Mostra la prassi di verifica UI KDE (screenshot + input + finestre)",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
const text = [
|
||||||
|
"PRASSI VERIFICA UI KDE (Plasma 6 Wayland):",
|
||||||
|
"1. kde_screenshot (mode=fullscreen|active|window|region, output=/tmp/x.png)",
|
||||||
|
"2. Analizza l'immagine (read o agy_analyze per visione)",
|
||||||
|
"3. Azione: kde_window_activate (resourceClass) / kde_click (x,y,button) / kde_type (text)",
|
||||||
|
"4. kde_screenshot di verifica e confronta con lo stato atteso",
|
||||||
|
"Nota: ydotool usa mappa US (layout IT: caratteri speciali diversi).",
|
||||||
|
"Daemon: si avvia da solo via sudoers NOPASSWD (ydotoold).",
|
||||||
|
].join("\n");
|
||||||
|
ctx.ui.notify(text, "info");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- tool: kde_screenshot ---
|
||||||
|
pi.registerTool({
|
||||||
|
name: "kde_screenshot",
|
||||||
|
label: "KDE Screenshot",
|
||||||
|
description:
|
||||||
|
"Cattura uno screenshot del desktop KDE con Spectacle (nativo). mode: fullscreen (intero desktop), active (finestra attiva), window (finestra sotto cursore), region (selezione). output: percorso file PNG. delay: millisecondi di attesa. copy: copia in clipboard invece di salvare.",
|
||||||
|
parameters: Type.Object({
|
||||||
|
mode: Type.Optional(Type.String({ description: "fullscreen | active | window | region (default fullscreen)" })),
|
||||||
|
output: Type.Optional(Type.String({ description: "Percorso file PNG (default /tmp/kde-shot-<ts>.png)" })),
|
||||||
|
delay: Type.Optional(Type.Number({ description: "Delay in millisecondi prima della cattura" })),
|
||||||
|
copy: Type.Optional(Type.Boolean({ description: "Copia in clipboard invece di salvare su file" })),
|
||||||
|
}),
|
||||||
|
async execute(_id, params) {
|
||||||
|
const mode = params.mode ?? "fullscreen";
|
||||||
|
const out = params.output ?? `/tmp/kde-shot-${Date.now()}.png`;
|
||||||
|
const args = ["-b"];
|
||||||
|
if (mode === "active") args.push("-a");
|
||||||
|
else if (mode === "window") args.push("-u");
|
||||||
|
else if (mode === "region") args.push("-r");
|
||||||
|
else args.push("-f");
|
||||||
|
if (params.delay) args.push("-d", String(params.delay));
|
||||||
|
if (params.copy) args.push("-c");
|
||||||
|
else args.push("-o", out);
|
||||||
|
await run("spectacle", args);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: params.copy ? "Screenshot copiato in clipboard" : `Screenshot salvato: ${out}` }],
|
||||||
|
details: { output: params.copy ? undefined : out },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- tool: kde_click ---
|
||||||
|
pi.registerTool({
|
||||||
|
name: "kde_click",
|
||||||
|
label: "KDE Click",
|
||||||
|
description:
|
||||||
|
"Simula un clic del mouse via ydotool. button: 1=sinistro, 2=centro, 3=destro. Se x e y sono forniti, prima muove il mouse a quelle coordinate. Richiede il daemon ydotoold (avvio automatico).",
|
||||||
|
parameters: Type.Object({
|
||||||
|
button: Type.Optional(Type.Number({ description: "1=sinistro (default), 2=centro, 3=destro" })),
|
||||||
|
x: Type.Optional(Type.Number({ description: "Coordinata X (opzionale, muove il mouse prima del clic)" })),
|
||||||
|
y: Type.Optional(Type.Number({ description: "Coordinata Y (opzionale)" })),
|
||||||
|
}),
|
||||||
|
async execute(_id, params) {
|
||||||
|
const daemon = await ensureDaemon();
|
||||||
|
const env = { YDOTOOL_SOCKET: SOCKET };
|
||||||
|
let log = daemon + "\n";
|
||||||
|
if (params.x !== undefined && params.y !== undefined) {
|
||||||
|
await run("ydotool", ["mousemove", String(params.x), String(params.y)], env);
|
||||||
|
log += `mouse -> ${params.x},${params.y}\n`;
|
||||||
|
}
|
||||||
|
await run("ydotool", ["click", String(params.button ?? 1)], env);
|
||||||
|
log += `click ${params.button ?? 1}`;
|
||||||
|
return { content: [{ type: "text", text: log }], details: {} };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- tool: kde_type ---
|
||||||
|
pi.registerTool({
|
||||||
|
name: "kde_type",
|
||||||
|
label: "KDE Type",
|
||||||
|
description:
|
||||||
|
"Digita testo via ydotool nella finestra attiva. ATTENZIONE: ydotool usa la mappa tastiera US — con layout IT i caratteri speciali (trattini, @, ecc.) possono differire. Per testo semplice ASCII va bene.",
|
||||||
|
parameters: Type.Object({
|
||||||
|
text: Type.String({ description: "Testo da digitare" }),
|
||||||
|
}),
|
||||||
|
async execute(_id, params) {
|
||||||
|
const daemon = await ensureDaemon();
|
||||||
|
await run("ydotool", ["type", params.text], { YDOTOOL_SOCKET: SOCKET });
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `${daemon}\nTesto digitato: "${params.text}"` }],
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- tool: kde_mousemove ---
|
||||||
|
pi.registerTool({
|
||||||
|
name: "kde_mousemove",
|
||||||
|
label: "KDE Mouse Move",
|
||||||
|
description: "Sposta il cursore del mouse alle coordinate (x, y) via ydotool.",
|
||||||
|
parameters: Type.Object({
|
||||||
|
x: Type.Number({ description: "Coordinata X" }),
|
||||||
|
y: Type.Number({ description: "Coordinata Y" }),
|
||||||
|
}),
|
||||||
|
async execute(_id, params) {
|
||||||
|
const daemon = await ensureDaemon();
|
||||||
|
await run("ydotool", ["mousemove", String(params.x), String(params.y)], { YDOTOOL_SOCKET: SOCKET });
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `${daemon}\nMouse -> ${params.x},${params.y}` }],
|
||||||
|
details: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- tool: kde_window_activate ---
|
||||||
|
pi.registerTool({
|
||||||
|
name: "kde_window_activate",
|
||||||
|
label: "KDE Window Activate",
|
||||||
|
description:
|
||||||
|
"Attiva (porta in primo piano) la finestra con la resourceClass specificata via KWin scripting. Esempi: org.kde.konsole, org.kde.dolphin, org.kde.plasma-systemmonitor, firefox.",
|
||||||
|
parameters: Type.Object({
|
||||||
|
resourceClass: Type.String({ description: "resourceClass della finestra (es. org.kde.konsole)" }),
|
||||||
|
}),
|
||||||
|
async execute(_id, params) {
|
||||||
|
const result = await activateWindow(params.resourceClass);
|
||||||
|
return { content: [{ type: "text", text: result }], details: {} };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- tool: kde_ui_inspect ---
|
||||||
|
pi.registerTool({
|
||||||
|
name: "kde_ui_inspect",
|
||||||
|
label: "KDE UI Inspect",
|
||||||
|
description:
|
||||||
|
"Ispeziona l'albero di accessibilità AT-SPI: elenca le applicazioni e i loro primi controlli (nomi e ruoli). Utile per capire cosa è presente a schermo e identificare elementi per l'automazione.",
|
||||||
|
parameters: Type.Object({
|
||||||
|
depth: Type.Optional(Type.Number({ description: "Quanti children mostrare per applicazione (default 3)" })),
|
||||||
|
}),
|
||||||
|
async execute(_id, params) {
|
||||||
|
const out = await inspectUI(params.depth ?? 3);
|
||||||
|
return { content: [{ type: "text", text: out }], details: {} };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: kde-verify
|
||||||
|
description: "Prassi di verifica UI su desktop KDE Plasma 6 (Wayland): screenshot con spectacle, input con ydotool, gestione finestre con KWin scripting, ispezione con AT-SPI. Usa questa skill quando devi verificare o automatizzare applicazioni a schermo."
|
||||||
|
metadata:
|
||||||
|
short-description: Verifica e automazione UI su KDE Plasma 6
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verifica UI KDE Plasma 6 (Wayland)
|
||||||
|
|
||||||
|
Prassi operativa per verificare e automatizzare applicazioni a schermo su KDE Plasma 6 (Wayland), usando solo strumenti nativi + ydotool.
|
||||||
|
|
||||||
|
## Ciclo di verifica
|
||||||
|
|
||||||
|
1. **Cattura**: `kde_screenshot` (mode=fullscreen|active|window|region, output=/tmp/x.png)
|
||||||
|
2. **Analizza**: leggi l'immagine con `read` (se il modello supporta immagini) o `agy_analyze` (visione Gemini) per descrivere lo stato
|
||||||
|
3. **Azione**: `kde_window_activate` (resourceClass) → `kde_click` (x,y,button) → `kde_type` (text)
|
||||||
|
4. **Verifica**: nuovo `kde_screenshot` e confronta con lo stato atteso
|
||||||
|
|
||||||
|
## Strumenti
|
||||||
|
|
||||||
|
| Tool | Funzione |
|
||||||
|
|---|---|
|
||||||
|
| `kde_screenshot` | Spectacle: fullscreen/active/window/region, delay, clipboard |
|
||||||
|
| `kde_click` | ydotool click (1=sin, 2=centro, 3=destro), con mousemove opzionale |
|
||||||
|
| `kde_type` | ydotool type (testo nella finestra attiva) |
|
||||||
|
| `kde_mousemove` | ydotool mousemove (x, y) |
|
||||||
|
| `kde_window_activate` | KWin scripting: attiva finestra per resourceClass |
|
||||||
|
| `kde_ui_inspect` | AT-SPI: elenca applicazioni e controlli |
|
||||||
|
|
||||||
|
## Comandi diretti (se i tool non bastano)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Screenshot
|
||||||
|
spectacle -b -f -o /tmp/x.png # fullscreen
|
||||||
|
spectacle -b -a -o /tmp/x.png # finestra attiva
|
||||||
|
spectacle -b -u -o /tmp/x.png # finestra sotto cursore
|
||||||
|
|
||||||
|
# Input (daemon: sudo ydotoold -p /run/user/1000/.ydotool_socket -P 0666 -o 1000:1000 &)
|
||||||
|
export YDOTOOL_SOCKET=/run/user/1000/.ydotool_socket
|
||||||
|
ydotool mousemove 720 450
|
||||||
|
ydotool click 1
|
||||||
|
ydotool type "testo"
|
||||||
|
|
||||||
|
# Finestre (KWin D-Bus)
|
||||||
|
qdbus6 org.kde.KWin /KWin org.kde.KWin.queryWindowInfo # info finestra attiva
|
||||||
|
qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.loadScript /tmp/script.js
|
||||||
|
qdbus6 org.kde.KWin /Scripting org.kde.kwin.Scripting.start
|
||||||
|
|
||||||
|
# Clipboard (workaround per inserire testo)
|
||||||
|
qdbus6 org.kde.klipper /klipper org.kde.klipper.klipper.setClipboardContents "testo"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avvertenze
|
||||||
|
|
||||||
|
- **ydotool usa la mappa tastiera US**: con layout IT i caratteri speciali (trattini → apostrofi, @, ecc.) vengono interpretati diversamente. Per testo semplice ASCII va bene.
|
||||||
|
- **Wayland non ha iniezione input nativa**: ydotool (via /dev/uinput) è il metodo; il daemon si avvia da solo (sudoers NOPASSWD per ydotoold configurato).
|
||||||
|
- **resourceClass comuni**: org.kde.konsole, org.kde.dolphin, org.kde.plasma-systemmonitor, firefox, org.kde.discover.
|
||||||
|
- **AT-SPI**: il bus è su unix:path=/run/user/1000/at-spi/bus_1; per uso avanzato serve pyatspi (non installato).
|
||||||
|
- **Dettagli e lezioni**: memoria qmem project `host-enne2-gk35` (record automazione KDE).
|
||||||
Reference in New Issue
Block a user