mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
fix(tool): browser_use Windows compat + stop LLM treating it as web search
This commit is contained in:
parent
0aee2a1ca5
commit
1864801c90
@ -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 知识库上下文注入
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user