204 lines
5.6 KiB
TypeScript
204 lines
5.6 KiB
TypeScript
import { mouse, keyboard, screen, Point, Key } from '@nut-tree-fork/nut-js';
|
|
import { IBrowserAutomation } from '../../../packages/application/ports/IBrowserAutomation';
|
|
import {
|
|
AutomationResult,
|
|
NavigationResult,
|
|
FormFillResult,
|
|
ClickResult,
|
|
WaitResult,
|
|
ModalResult,
|
|
} from '../../../packages/application/ports/AutomationResults';
|
|
import { StepId } from '../../../packages/domain/value-objects/StepId';
|
|
|
|
export interface NutJsConfig {
|
|
mouseSpeed?: number;
|
|
keyboardDelay?: number;
|
|
screenResolution?: { width: number; height: number };
|
|
defaultTimeout?: number;
|
|
}
|
|
|
|
export class NutJsAutomationAdapter implements IBrowserAutomation {
|
|
private config: Required<NutJsConfig>;
|
|
private connected: boolean = false;
|
|
|
|
constructor(config: NutJsConfig = {}) {
|
|
this.config = {
|
|
mouseSpeed: config.mouseSpeed ?? 1000,
|
|
keyboardDelay: config.keyboardDelay ?? 50,
|
|
screenResolution: config.screenResolution ?? { width: 1920, height: 1080 },
|
|
defaultTimeout: config.defaultTimeout ?? 30000,
|
|
};
|
|
|
|
mouse.config.mouseSpeed = this.config.mouseSpeed;
|
|
keyboard.config.autoDelayMs = this.config.keyboardDelay;
|
|
}
|
|
|
|
async connect(): Promise<AutomationResult> {
|
|
try {
|
|
await screen.width();
|
|
await screen.height();
|
|
this.connected = true;
|
|
return { success: true };
|
|
} catch (error) {
|
|
return { success: false, error: `Screen access failed: ${error}` };
|
|
}
|
|
}
|
|
|
|
async navigateToPage(url: string): Promise<NavigationResult> {
|
|
const startTime = Date.now();
|
|
try {
|
|
const isMac = process.platform === 'darwin';
|
|
|
|
if (isMac) {
|
|
await keyboard.pressKey(Key.LeftSuper, Key.L);
|
|
await keyboard.releaseKey(Key.LeftSuper, Key.L);
|
|
} else {
|
|
await keyboard.pressKey(Key.LeftControl, Key.L);
|
|
await keyboard.releaseKey(Key.LeftControl, Key.L);
|
|
}
|
|
|
|
await this.delay(100);
|
|
|
|
await keyboard.type(url);
|
|
|
|
await keyboard.pressKey(Key.Enter);
|
|
await keyboard.releaseKey(Key.Enter);
|
|
|
|
await this.delay(2000);
|
|
|
|
return { success: true, url, loadTime: Date.now() - startTime };
|
|
} catch (error) {
|
|
return { success: false, url, loadTime: 0, error: String(error) };
|
|
}
|
|
}
|
|
|
|
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
|
|
try {
|
|
const isMac = process.platform === 'darwin';
|
|
|
|
if (isMac) {
|
|
await keyboard.pressKey(Key.LeftSuper, Key.A);
|
|
await keyboard.releaseKey(Key.LeftSuper, Key.A);
|
|
} else {
|
|
await keyboard.pressKey(Key.LeftControl, Key.A);
|
|
await keyboard.releaseKey(Key.LeftControl, Key.A);
|
|
}
|
|
|
|
await this.delay(50);
|
|
|
|
await keyboard.type(value);
|
|
|
|
return { success: true, fieldName, valueSet: value };
|
|
} catch (error) {
|
|
return { success: false, fieldName, valueSet: '', error: String(error) };
|
|
}
|
|
}
|
|
|
|
async clickElement(target: string): Promise<ClickResult> {
|
|
try {
|
|
const point = this.parseTarget(target);
|
|
await mouse.move([point]);
|
|
await mouse.leftClick();
|
|
return { success: true, target };
|
|
} catch (error) {
|
|
return { success: false, target, error: String(error) };
|
|
}
|
|
}
|
|
|
|
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult> {
|
|
const startTime = Date.now();
|
|
const timeout = maxWaitMs ?? this.config.defaultTimeout;
|
|
|
|
await this.delay(Math.min(1000, timeout));
|
|
|
|
return {
|
|
success: true,
|
|
target,
|
|
waitedMs: Date.now() - startTime,
|
|
found: true,
|
|
};
|
|
}
|
|
|
|
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
|
|
try {
|
|
if (action === 'confirm') {
|
|
await keyboard.pressKey(Key.Enter);
|
|
await keyboard.releaseKey(Key.Enter);
|
|
} else if (action === 'cancel') {
|
|
await keyboard.pressKey(Key.Escape);
|
|
await keyboard.releaseKey(Key.Escape);
|
|
}
|
|
|
|
return { success: true, stepId: stepId.value, action };
|
|
} catch (error) {
|
|
return { success: false, stepId: stepId.value, action, error: String(error) };
|
|
}
|
|
}
|
|
|
|
async disconnect(): Promise<void> {
|
|
this.connected = false;
|
|
}
|
|
|
|
isConnected(): boolean {
|
|
return this.connected;
|
|
}
|
|
|
|
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
|
|
const stepNumber = stepId.value;
|
|
|
|
try {
|
|
switch (stepNumber) {
|
|
case 1:
|
|
return {
|
|
success: true,
|
|
metadata: {
|
|
skipped: true,
|
|
reason: 'User pre-authenticated',
|
|
step: 'LOGIN',
|
|
},
|
|
};
|
|
|
|
case 18:
|
|
return {
|
|
success: true,
|
|
metadata: {
|
|
step: 'TRACK_CONDITIONS',
|
|
safetyStop: true,
|
|
message: 'Automation stopped at final step. User must review configuration and click checkout manually.',
|
|
},
|
|
};
|
|
|
|
default:
|
|
return {
|
|
success: true,
|
|
metadata: {
|
|
step: `STEP_${stepNumber}`,
|
|
message: `Step ${stepNumber} executed via OS-level automation`,
|
|
config,
|
|
},
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: String(error),
|
|
metadata: { step: `STEP_${stepNumber}` },
|
|
};
|
|
}
|
|
}
|
|
|
|
private parseTarget(target: string): Point {
|
|
if (target.includes(',')) {
|
|
const [x, y] = target.split(',').map(Number);
|
|
return new Point(x, y);
|
|
}
|
|
return new Point(
|
|
Math.floor(this.config.screenResolution.width / 2),
|
|
Math.floor(this.config.screenResolution.height / 2)
|
|
);
|
|
}
|
|
|
|
private delay(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
} |