Skip to content

Commit e88dd35

Browse files
author
SentienceDEV
committed
Merge pull request #46 from SentienceAPI/improve_agent3
optimize agent.ts and stealth mode
2 parents 636dfa6 + 07addfb commit e88dd35

File tree

3 files changed

+321
-7
lines changed

3 files changed

+321
-7
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/agent.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,17 @@ export class SentienceAgent {
152152
throw new Error(`Snapshot failed: ${snap.error}`);
153153
}
154154

155+
// Apply element filtering based on goal
156+
const filteredElements = this.filterElements(snap, goal);
157+
158+
// Create filtered snapshot
159+
const filteredSnap: Snapshot = {
160+
...snap,
161+
elements: filteredElements
162+
};
163+
155164
// 2. GROUND: Format elements for LLM context
156-
const context = this.buildContext(snap, goal);
165+
const context = this.buildContext(filteredSnap, goal);
157166

158167
// 3. THINK: Query LLM for next action
159168
const llmResponse = await this.queryLLM(context, goal);
@@ -169,7 +178,7 @@ export class SentienceAgent {
169178
const actionStr = llmResponse.content.trim();
170179

171180
// 4. EXECUTE: Parse and run action
172-
const result = await this.executeAction(actionStr, snap);
181+
const result = await this.executeAction(actionStr, filteredSnap);
173182

174183
const durationMs = Date.now() - startTime;
175184
result.durationMs = durationMs;
@@ -217,14 +226,78 @@ export class SentienceAgent {
217226
throw new Error('Unexpected: loop should have returned or thrown');
218227
}
219228

229+
/**
230+
* Filter elements from snapshot based on goal context.
231+
* Applies goal-based keyword matching to boost relevant elements and filters out irrelevant ones.
232+
*/
233+
private filterElements(snap: Snapshot, goal: string): Element[] {
234+
let elements = snap.elements;
235+
236+
// If no goal provided, return all elements (up to limit)
237+
if (!goal) {
238+
return elements.slice(0, this.snapshotLimit);
239+
}
240+
241+
const goalLower = goal.toLowerCase();
242+
243+
// Extract keywords from goal
244+
const keywords = this.extractKeywords(goalLower);
245+
246+
// Boost elements matching goal keywords
247+
const scoredElements: Array<[number, Element]> = [];
248+
for (const el of elements) {
249+
let score = el.importance;
250+
251+
// Boost if element text matches goal
252+
if (el.text && keywords.some(kw => el.text!.toLowerCase().includes(kw))) {
253+
score += 0.3;
254+
}
255+
256+
// Boost if role matches goal intent
257+
if (goalLower.includes('click') && el.visual_cues.is_clickable) {
258+
score += 0.2;
259+
}
260+
if (goalLower.includes('type') && (el.role === 'textbox' || el.role === 'searchbox')) {
261+
score += 0.2;
262+
}
263+
if (goalLower.includes('search')) {
264+
// Filter out non-interactive elements for search tasks
265+
if ((el.role === 'link' || el.role === 'img') && !el.visual_cues.is_primary) {
266+
score -= 0.5;
267+
}
268+
}
269+
270+
scoredElements.push([score, el]);
271+
}
272+
273+
// Re-sort by boosted score
274+
scoredElements.sort((a, b) => b[0] - a[0]);
275+
elements = scoredElements.map(([, el]) => el);
276+
277+
return elements.slice(0, this.snapshotLimit);
278+
}
279+
280+
/**
281+
* Extract meaningful keywords from goal text
282+
*/
283+
private extractKeywords(text: string): string[] {
284+
const stopwords = new Set([
285+
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
286+
'of', 'with', 'by', 'from', 'as', 'is', 'was'
287+
]);
288+
const words = text.split(/\s+/);
289+
return words.filter(w => !stopwords.has(w) && w.length > 2);
290+
}
291+
220292
/**
221293
* Convert snapshot elements to token-efficient prompt string
222294
* Format: [ID] <role> "text" {cues} @ (x,y) (Imp:score)
295+
* Note: elements are already filtered by filterElements() in act()
223296
*/
224297
private buildContext(snap: Snapshot, goal: string): string {
225298
const lines: string[] = [];
226299

227-
for (const el of snap.elements.slice(0, this.snapshotLimit)) {
300+
for (const el of snap.elements) {
228301
// Extract visual cues
229302
const cues: string[] = [];
230303
if (el.visual_cues.is_primary) cues.push('PRIMARY');

src/browser.ts

Lines changed: 243 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,251 @@ export class SentienceBrowser {
104104
});
105105

106106
this.page = this.context.pages()[0] || await this.context.newPage();
107+
108+
// Apply context-level stealth patches (runs on every new page)
109+
await this.context.addInitScript(() => {
110+
// Early webdriver hiding - runs before any page script
111+
// Use multiple strategies to completely hide webdriver
112+
113+
// Strategy 1: Try to delete it first
114+
try {
115+
delete (navigator as any).webdriver;
116+
} catch (e) {
117+
// Property might not be deletable
118+
}
119+
120+
// Strategy 2: Redefine to return undefined and hide from enumeration
121+
Object.defineProperty(navigator, 'webdriver', {
122+
get: () => undefined,
123+
configurable: true,
124+
enumerable: false,
125+
writable: false
126+
});
127+
128+
// Strategy 3: Override 'in' operator check
129+
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
130+
Object.prototype.hasOwnProperty = function(prop: string | number | symbol) {
131+
if (this === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
132+
return false;
133+
}
134+
return originalHasOwnProperty.call(this, prop);
135+
};
136+
});
107137

108-
// 5. Apply Stealth (Basic)
138+
// 5. Apply Comprehensive Stealth Patches
139+
// Use both CDP (earlier) and addInitScript (backup) for maximum coverage
140+
141+
// Strategy A: Use CDP to inject at the earliest possible moment
142+
const client = await this.page.context().newCDPSession(this.page);
143+
await client.send('Page.addScriptToEvaluateOnNewDocument', {
144+
source: `
145+
// Aggressive webdriver hiding - must run before ANY page script
146+
Object.defineProperty(navigator, 'webdriver', {
147+
get: () => undefined,
148+
configurable: true,
149+
enumerable: false
150+
});
151+
152+
// Override Object.getOwnPropertyDescriptor
153+
const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
154+
Object.getOwnPropertyDescriptor = function(obj, prop) {
155+
if (obj === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
156+
return undefined;
157+
}
158+
return originalGetOwnPropertyDescriptor(obj, prop);
159+
};
160+
161+
// Override Object.keys
162+
const originalKeys = Object.keys;
163+
Object.keys = function(obj) {
164+
const keys = originalKeys(obj);
165+
if (obj === navigator) {
166+
return keys.filter(k => k !== 'webdriver' && k !== 'Webdriver');
167+
}
168+
return keys;
169+
};
170+
171+
// Override Object.getOwnPropertyNames
172+
const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
173+
Object.getOwnPropertyNames = function(obj) {
174+
const names = originalGetOwnPropertyNames(obj);
175+
if (obj === navigator) {
176+
return names.filter(n => n !== 'webdriver' && n !== 'Webdriver');
177+
}
178+
return names;
179+
};
180+
181+
// Override 'in' operator check
182+
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
183+
Object.prototype.hasOwnProperty = function(prop) {
184+
if (this === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
185+
return false;
186+
}
187+
return originalHasOwnProperty.call(this, prop);
188+
};
189+
`
190+
});
191+
192+
// Strategy B: Also use addInitScript as backup (runs after CDP but before page scripts)
109193
await this.page.addInitScript(() => {
110-
Object.defineProperty(navigator, 'webdriver', { get: () => false });
194+
// 1. Hide navigator.webdriver (comprehensive approach for advanced detection)
195+
// Advanced detection checks for property descriptor, so we need multiple strategies
196+
try {
197+
// Strategy 1: Try to delete the property
198+
delete (navigator as any).webdriver;
199+
} catch (e) {
200+
// Property might not be deletable, continue with redefine
201+
}
202+
203+
// Strategy 2: Redefine to return undefined (better than false)
204+
// Also set enumerable: false to hide from Object.keys() checks
205+
Object.defineProperty(navigator, 'webdriver', {
206+
get: () => undefined,
207+
configurable: true,
208+
enumerable: false
209+
});
210+
211+
// Strategy 3: Override Object.getOwnPropertyDescriptor only for navigator.webdriver
212+
// This prevents advanced detection that checks the property descriptor
213+
const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
214+
Object.getOwnPropertyDescriptor = function(obj: any, prop: string | symbol) {
215+
if (obj === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
216+
return undefined;
217+
}
218+
return originalGetOwnPropertyDescriptor(obj, prop);
219+
} as any;
220+
221+
// Strategy 4: Hide from Object.keys() and Object.getOwnPropertyNames()
222+
const originalKeys = Object.keys;
223+
Object.keys = function(obj: any) {
224+
const keys = originalKeys(obj);
225+
if (obj === navigator) {
226+
return keys.filter(k => k !== 'webdriver' && k !== 'Webdriver');
227+
}
228+
return keys;
229+
} as any;
230+
231+
// Strategy 5: Hide from Object.getOwnPropertyNames()
232+
const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
233+
Object.getOwnPropertyNames = function(obj: any) {
234+
const names = originalGetOwnPropertyNames(obj);
235+
if (obj === navigator) {
236+
return names.filter(n => n !== 'webdriver' && n !== 'Webdriver');
237+
}
238+
return names;
239+
} as any;
240+
241+
// Strategy 6: Override hasOwnProperty to hide from 'in' operator checks
242+
const originalHasOwnProperty = Object.prototype.hasOwnProperty;
243+
Object.prototype.hasOwnProperty = function(prop: string | number | symbol) {
244+
if (this === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
245+
return false;
246+
}
247+
return originalHasOwnProperty.call(this, prop);
248+
};
249+
250+
// 2. Inject window.chrome object (required for Chrome detection)
251+
if (typeof (window as any).chrome === 'undefined') {
252+
(window as any).chrome = {
253+
runtime: {},
254+
loadTimes: function() {},
255+
csi: function() {},
256+
app: {}
257+
};
258+
}
259+
260+
// 3. Patch navigator.plugins (should have length > 0)
261+
// Only patch if plugins array is empty (headless mode issue)
262+
const originalPlugins = navigator.plugins;
263+
if (originalPlugins.length === 0) {
264+
// Create a PluginArray-like object with minimal plugins
265+
const fakePlugins = [
266+
{
267+
name: 'Chrome PDF Plugin',
268+
filename: 'internal-pdf-viewer',
269+
description: 'Portable Document Format',
270+
length: 1,
271+
item: function() { return null; },
272+
namedItem: function() { return null; }
273+
},
274+
{
275+
name: 'Chrome PDF Viewer',
276+
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
277+
description: '',
278+
length: 0,
279+
item: function() { return null; },
280+
namedItem: function() { return null; }
281+
},
282+
{
283+
name: 'Native Client',
284+
filename: 'internal-nacl-plugin',
285+
description: '',
286+
length: 0,
287+
item: function() { return null; },
288+
namedItem: function() { return null; }
289+
}
290+
];
291+
292+
// Create PluginArray-like object (array-like but not a real array)
293+
// This needs to behave like the real PluginArray for detection to pass
294+
const pluginArray: any = {};
295+
fakePlugins.forEach((plugin, index) => {
296+
Object.defineProperty(pluginArray, index.toString(), {
297+
value: plugin,
298+
enumerable: true,
299+
configurable: true
300+
});
301+
});
302+
303+
Object.defineProperty(pluginArray, 'length', {
304+
value: fakePlugins.length,
305+
enumerable: false,
306+
configurable: false
307+
});
308+
309+
pluginArray.item = function(index: number) {
310+
return this[index] || null;
311+
};
312+
pluginArray.namedItem = function(name: string) {
313+
for (let i = 0; i < this.length; i++) {
314+
if (this[i] && this[i].name === name) return this[i];
315+
}
316+
return null;
317+
};
318+
319+
// Make it iterable (for for...of loops)
320+
pluginArray[Symbol.iterator] = function*() {
321+
for (let i = 0; i < this.length; i++) {
322+
yield this[i];
323+
}
324+
};
325+
326+
// Make it array-like for Array.from() and spread
327+
Object.setPrototypeOf(pluginArray, Object.create(null));
328+
329+
Object.defineProperty(navigator, 'plugins', {
330+
get: () => pluginArray,
331+
configurable: true,
332+
enumerable: true
333+
});
334+
}
335+
336+
// 4. Ensure navigator.languages exists and has values
337+
if (!navigator.languages || navigator.languages.length === 0) {
338+
Object.defineProperty(navigator, 'languages', {
339+
get: () => ['en-US', 'en'],
340+
configurable: true
341+
});
342+
}
343+
344+
// 5. Patch permissions API (should exist)
345+
if (!navigator.permissions) {
346+
(navigator as any).permissions = {
347+
query: async (parameters: PermissionDescriptor) => {
348+
return { state: 'granted', onchange: null } as PermissionStatus;
349+
}
350+
};
351+
}
111352
});
112353

113354
// Inject API Key if present

0 commit comments

Comments
 (0)