fix(tool): browser_use Windows compat + stop LLM treating it as web search

This commit is contained in:
matevip 2026-04-30 08:54:15 +08:00
parent 0aee2a1ca5
commit 1864801c90
3 changed files with 102 additions and 15 deletions

View File

@ -926,22 +926,27 @@ public class AgentGraphBuilder {
If you try to read a PDF/Office file with read_file, you will get binary garbage or an error.
""".formatted(entity.getId());
String searchGuidance = "";
if (builtinSearchEnabled) {
searchGuidance = """
// Web-search vs browser_use priority guidance emitted unconditionally so the rule
// also reaches OpenAI-compatible / Anthropic / Gemini / DeepSeek / Ollama agents that
// do not have builtin search. Issue #40: without this rule the model treats
// browser_use as a search tool and gets stuck in a Playwright launch loop on Windows.
String searchGuidance = """
## Web Search Capability
You have **dual search capability**:
1. **Built-in search** (preferred): Your responses automatically incorporate live web search results from the model provider. For most queries, answer directly your response already includes real-time search data.
2. **search tool** (supplementary): Available as a fallback. Supports advanced parameters: `freshness` (day/week/month/year), `language` (zh-CN/en), `count` (1-10).
### Priority Rules
- **Default**: Answer directly using built-in search. Do NOT say you cannot search your replies already include live results.
- **Use search tool** ONLY when: you need precise time filtering (e.g., user asks for "yesterday's news" call search with freshness=day), specific language results, or your built-in results feel insufficient.
- **NEVER** call both browser_use and search tool for the same query.
### Tool Priority
- For plain web search or fetching public page content, call the `search` tool. It supports advanced parameters: `freshness` (day/week/month/year), `language` (zh-CN/en), `count` (1-10).
- Call `browser_use` ONLY when you need to interact with a page (click, fill forms, screenshot, run JS, follow a logged-in flow). Do NOT use `browser_use` as a search alternative.
- **NEVER** call both `browser_use` and `search` for the same query.
- When searching for news, use the standard format: `📰 [Category] Title Source | Time + Summary`, up to 5 results per category.
""";
if (builtinSearchEnabled) {
searchGuidance += """
### Built-in Search (preferred when available)
Your responses automatically incorporate live web search results from the model provider. For most queries, answer directly your reply already includes real-time search data. Do NOT say you cannot search.
Use the `search` tool ONLY when you need precise time filtering (e.g., "yesterday's news" freshness=day), a specific language, or when built-in results feel insufficient.
""";
}
// Wiki 知识库上下文注入

View File

@ -206,9 +206,25 @@ public class BrowserLauncher {
return null;
}
// Issue #40 On Windows (and macOS) Chrome refuses to spawn a second instance against
// the default user-data-dir if the user already has Chrome open: the new chrome.exe just
// forwards its argv to the running instance and exits, so stderr never prints
// "DevTools listening on ...". An isolated profile dir avoids that conflict and is also
// what the openfang launcher does.
Path userDataDir;
try {
userDataDir = Files.createTempDirectory("mateclaw-cdp-profile-");
} catch (Exception e) {
trace.add(Attempt.fail(Strategy.EXTERNAL_CDP, browserBin.toString(),
System.currentTimeMillis() - t0,
"failed to create isolated user-data-dir: " + e.getMessage()));
return null;
}
List<String> command = new ArrayList<>();
command.add(browserBin.toString());
command.add("--remote-debugging-port=0");
command.add("--user-data-dir=" + userDataDir.toAbsolutePath());
command.add("--no-first-run");
command.add("--no-default-browser-check");
command.add("--disable-extensions");
@ -241,6 +257,7 @@ public class BrowserLauncher {
try {
proc = pb.start();
} catch (Exception e) {
deleteQuietly(userDataDir);
trace.add(Attempt.fail(Strategy.EXTERNAL_CDP, browserBin + " --remote-debugging-port",
System.currentTimeMillis() - t0, "spawn failed: " + e.getMessage()));
return null;
@ -251,6 +268,7 @@ public class BrowserLauncher {
wsUrl = readDevToolsUrl(proc, props.getCdpTimeoutSeconds());
} catch (Exception e) {
proc.destroyForcibly();
deleteQuietly(userDataDir);
trace.add(Attempt.fail(Strategy.EXTERNAL_CDP, browserBin.toString(),
System.currentTimeMillis() - t0, e.getMessage()));
return null;
@ -264,17 +282,40 @@ public class BrowserLauncher {
? browser.newContext()
: browser.contexts().get(0);
Page page = context.pages().isEmpty() ? context.newPage() : context.pages().get(0);
// The temp profile dir is owned by the spawned Chrome for the lifetime of the
// browser; clean up when the JVM exits since we don't track per-session shutdown
// here. (BrowserUseTool stops the browser explicitly on stop, so the JVM hook is
// a backstop for crashes.)
registerProfileDirCleanup(userDataDir);
long elapsed = System.currentTimeMillis() - t0;
trace.add(Attempt.ok(Strategy.EXTERNAL_CDP, browserBin + " + connectOverCDP(" + cdpBase + ")", elapsed));
return Result.success(browser, context, page, true, cdpBase, Strategy.EXTERNAL_CDP, trace);
} catch (Exception e) {
proc.destroyForcibly();
deleteQuietly(userDataDir);
trace.add(Attempt.fail(Strategy.EXTERNAL_CDP, "connectOverCDP(" + cdpBase + ")",
System.currentTimeMillis() - t0, e.getMessage()));
return null;
}
}
private static void deleteQuietly(Path dir) {
if (dir == null) return;
try {
if (!Files.exists(dir)) return;
try (var stream = Files.walk(dir)) {
stream.sorted(java.util.Comparator.reverseOrder()).forEach(p -> {
try { Files.deleteIfExists(p); } catch (Exception ignored) {}
});
}
} catch (Exception ignored) {}
}
private static void registerProfileDirCleanup(Path dir) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> deleteQuietly(dir),
"mateclaw-cdp-profile-cleanup"));
}
// ==================== Helpers ====================
private BrowserType.LaunchOptions baseLaunchOptions(boolean headed) {
@ -312,7 +353,7 @@ public class BrowserLauncher {
return args;
}
/** Platform-specific candidate paths — same list openfang uses. */
/** Platform-specific candidate paths — same list openfang uses, extended for issue #40. */
public static List<Path> systemBrowserCandidates() {
List<Path> paths = new ArrayList<>();
if (IS_WINDOWS) {
@ -328,6 +369,27 @@ public class BrowserLauncher {
if (local != null && !local.isBlank()) {
paths.add(Path.of(local, "Google", "Chrome", "Application", "chrome.exe"));
paths.add(Path.of(local, "Microsoft", "Edge", "Application", "msedge.exe"));
// Chinese Chromium-based browsers they ignore Playwright's "chrome" channel
// detection but launch fine via executablePath. They tend to install per-user.
paths.add(Path.of(local, "360ChromeX", "Chrome", "Application", "360ChromeX.exe"));
paths.add(Path.of(local, "360Chrome", "Chrome", "Application", "360chrome.exe"));
paths.add(Path.of(local, "360se6", "Application", "360se.exe"));
paths.add(Path.of(local, "Tencent", "QQBrowser", "QQBrowser.exe"));
paths.add(Path.of(local, "sogouexplorer", "SogouExplorer.exe"));
}
// Issue #40: many users install Chrome/Edge to a non-system drive (D:, E:, ...).
// Scan only mounted drives to avoid spurious 5s "no media" timeouts on empty letters.
for (java.io.File drive : java.io.File.listRoots()) {
String letter = drive.getPath();
if (letter == null || letter.isBlank()) continue;
if (!drive.exists()) continue;
String upper = letter.toUpperCase(Locale.ROOT);
if (upper.startsWith("C:")) continue; // already covered by ProgramFiles env vars
for (String pfDir : new String[]{"Program Files", "Program Files (x86)"}) {
paths.add(Path.of(letter, pfDir, "Google", "Chrome", "Application", "chrome.exe"));
paths.add(Path.of(letter, pfDir, "Microsoft", "Edge", "Application", "msedge.exe"));
paths.add(Path.of(letter, pfDir, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"));
}
}
} else if (IS_MAC) {
paths.add(Path.of("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"));

View File

@ -83,8 +83,12 @@ public class BrowserUseTool {
Default is headless. Use headed=true with action=start for a visible window.
Typical flow: start open(url) snapshot click/type stop.
If start fails, run action=diagnose for a full report of what's missing and how to fix it.
When web_search is unavailable (no Serper/Tavily API key), use this tool to fetch content directly:
e.g. action=open url=https://news.google.com/search?q=... then action=snapshot to read the page.
SCOPE use this tool ONLY for tasks that require driving a real browser:
clicking, typing into forms, taking screenshots, executing JS in page context, or
interacting with sites that need a logged-in session. For plain web search or
retrieving public page content, prefer the `search` tool do not call `browser_use`
as a search alternative.
Supported actions:
- start: Launch a new browser (tries system Chrome, system Edge, then Playwright bundled). Optional headed=true.
@ -154,6 +158,12 @@ public class BrowserUseTool {
/**
* 获取或创建共享 Playwright 实例双重检查锁定
* 首次调用约 1-2s启动 Node.js后续调用 ~0ms
*
* <p>Issue #40: Playwright.create() spawns a Node.js driver subprocess by extracting
* a bundled binary to a temp directory. On Windows this can fail when the user profile
* path contains non-ASCII characters or when antivirus quarantines the extracted exe.
* We wrap the failure with a message that points the LLM/user at action=diagnose so
* they don't get a bare stack trace.
*/
private Playwright getOrCreatePlaywright() {
Playwright pw = sharedPlaywright;
@ -167,7 +177,17 @@ public class BrowserUseTool {
}
log.info("[BrowserUse] Creating shared Playwright instance...");
long start = System.currentTimeMillis();
pw = Playwright.create();
try {
pw = Playwright.create();
} catch (Throwable t) {
String os = System.getProperty("os.name", "?");
log.error("[BrowserUse] Playwright.create() failed on {}: {}", os, t.getMessage(), t);
throw new PlaywrightException(
"Failed to start Playwright driver on " + os + ": " + t.getMessage()
+ ". Common causes on Windows: (a) user profile path contains non-ASCII chars,"
+ " (b) antivirus blocked the extracted driver exe, (c) %TEMP% is on a read-only volume."
+ " Run action=diagnose for a full report.", t);
}
sharedPlaywright = pw;
log.info("[BrowserUse] Playwright instance created in {}ms", System.currentTimeMillis() - start);
return pw;