Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d99a223d23 | ||
|
|
4e7e80a596 | ||
|
|
9bf79a4242 |
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.4.0"
|
version = "1.4.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Fast browser automation CLI for AI agents"
|
description = "Fast browser automation CLI for AI agents"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -8785,13 +8785,7 @@ async fn handle_keydown(cmd: &Value, state: &DaemonState) -> Result<Value, Strin
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or("Missing 'key' parameter")?;
|
.ok_or("Missing 'key' parameter")?;
|
||||||
|
|
||||||
mgr.client
|
interaction::dispatch_single_key(&mgr.client, &session_id, key, "keyDown").await?;
|
||||||
.send_command(
|
|
||||||
"Input.dispatchKeyEvent",
|
|
||||||
Some(json!({ "type": "keyDown", "key": key })),
|
|
||||||
Some(&session_id),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(json!({ "keydown": key }))
|
Ok(json!({ "keydown": key }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8803,13 +8797,7 @@ async fn handle_keyup(cmd: &Value, state: &DaemonState) -> Result<Value, String>
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.ok_or("Missing 'key' parameter")?;
|
.ok_or("Missing 'key' parameter")?;
|
||||||
|
|
||||||
mgr.client
|
interaction::dispatch_single_key(&mgr.client, &session_id, key, "keyUp").await?;
|
||||||
.send_command(
|
|
||||||
"Input.dispatchKeyEvent",
|
|
||||||
Some(json!({ "type": "keyUp", "key": key })),
|
|
||||||
Some(&session_id),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(json!({ "keyup": key }))
|
Ok(json!({ "keyup": key }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -557,6 +557,48 @@ pub async fn press_key_with_modifiers(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dispatch a SINGLE key event (`keyDown` or `keyUp`) carrying the full key
|
||||||
|
/// descriptor — `key`, `code`, `windowsVirtualKeyCode`/`nativeVirtualKeyCode`,
|
||||||
|
/// and (on key-down) printable `text`. Powers the `keydown`/`keyup` commands.
|
||||||
|
///
|
||||||
|
/// The previous implementation sent only `{key}`, so games and shortcut handlers
|
||||||
|
/// that read `event.code` (e.g. `"KeyD"`, `"ArrowRight"`) or `event.keyCode` saw
|
||||||
|
/// nothing — a held key set no movement flag and did nothing (dogfood: holding a
|
||||||
|
/// direction in a canvas platformer barely nudged the player). Sending the same
|
||||||
|
/// descriptor `press` uses makes hold-to-move work regardless of which field the
|
||||||
|
/// page keys off.
|
||||||
|
pub async fn dispatch_single_key(
|
||||||
|
client: &CdpClient,
|
||||||
|
session_id: &str,
|
||||||
|
key: &str,
|
||||||
|
event_type: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let (key_name, code, key_code) = named_key_info(key);
|
||||||
|
// Printable text is only meaningful on key-down; key-up never inserts.
|
||||||
|
let text = if event_type == "keyDown" {
|
||||||
|
key_text(&key_name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
client
|
||||||
|
.send_command_typed::<_, Value>(
|
||||||
|
"Input.dispatchKeyEvent",
|
||||||
|
&DispatchKeyEventParams {
|
||||||
|
event_type: event_type.to_string(),
|
||||||
|
key: Some(key_name),
|
||||||
|
code: Some(code),
|
||||||
|
text: text.clone(),
|
||||||
|
unmodified_text: text,
|
||||||
|
windows_virtual_key_code: Some(key_code),
|
||||||
|
native_virtual_key_code: Some(key_code),
|
||||||
|
modifiers: None,
|
||||||
|
},
|
||||||
|
Some(session_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn scroll(
|
pub async fn scroll(
|
||||||
client: &CdpClient,
|
client: &CdpClient,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -155,23 +155,56 @@ function tabForTarget(targetId) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort recovery for a stale `cb-tab-<tabId>` session: the handle is gone
|
// The STABLE Chrome tabId encoded in a `cb-tab-<tabId>` session id (#17), or
|
||||||
// from our maps, but if the underlying Chrome tab still exists and is eligible,
|
// null for any other session shape (child/iframe sessions). The tabId is the
|
||||||
// re-attach to it and return its id so the in-flight command can be retried.
|
// real source of truth: it survives the renderer-process swaps (cross-origin
|
||||||
// Returns null when the tab is genuinely gone (closed / restricted), in which
|
// OAuth/SSO navs) that tear down the page's CDP target — which is why binding to
|
||||||
// case the caller surfaces the stale-session error. (issue #20.1)
|
// it (like claude-in-chrome) rides through the hop that killed the old
|
||||||
|
// target/sessionId binding (issue #23).
|
||||||
|
function tabIdFromSession(sessionId) {
|
||||||
|
const m = /^cb-tab-(\d+)$/.exec(sessionId || '')
|
||||||
|
return m ? Number(m[1]) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the debugger is attached to a `cb-tab-<tabId>` session's tab, re-attaching
|
||||||
|
// across the transient window of a process swap (with a couple of short retries).
|
||||||
|
// Returns the tabId on success, or null when the tab is genuinely gone
|
||||||
|
// (closed / restricted). (issues #20.1, #23)
|
||||||
async function recoverSessionTab(sessionId) {
|
async function recoverSessionTab(sessionId) {
|
||||||
const m = /^cb-tab-(\d+)$/.exec(sessionId)
|
const tabId = tabIdFromSession(sessionId)
|
||||||
if (!m) return null
|
if (tabId == null) return null
|
||||||
const tabId = Number(m[1])
|
for (let i = 0; i < 3; i++) {
|
||||||
const tab = await chrome.tabs.get(tabId).catch(() => null)
|
const tab = await chrome.tabs.get(tabId).catch(() => null)
|
||||||
if (!eligible(tab)) return null
|
if (!eligible(tab)) return null
|
||||||
try {
|
try {
|
||||||
await attachTab(tabId)
|
await attachTab(tabId)
|
||||||
} catch {
|
if (tabs.has(tabId)) return tabId
|
||||||
return null
|
} catch {
|
||||||
|
// mid-swap: the tab exists but isn't attachable yet — back off and retry.
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 120 + i * 150))
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a CDP command to a tab, riding a debugger detach that can happen between
|
||||||
|
// our attach check and the command itself (a renderer-process swap mid-flight).
|
||||||
|
// On a detached-style failure, drop the stale handle, re-attach the stable tab,
|
||||||
|
// and retry once — so a cross-process nav never surfaces as a hard error (#23).
|
||||||
|
async function sendCdpToTab(tabId, method, params) {
|
||||||
|
const dbg = { tabId }
|
||||||
|
try {
|
||||||
|
return await chrome.debugger.sendCommand(dbg, method, params)
|
||||||
|
} catch (e) {
|
||||||
|
const msg = String((e && e.message) || e)
|
||||||
|
if (!/detached|not attached|target.*(closed|gone)|no target|cannot access|frame.*detached/i.test(msg)) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
detachTab(tabId, false)
|
||||||
|
const ok = await recoverSessionTab(`cb-tab-${tabId}`)
|
||||||
|
if (!ok) throw e
|
||||||
|
return await chrome.debugger.sendCommand(dbg, method, params)
|
||||||
}
|
}
|
||||||
return tabs.has(tabId) ? tabId : null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function anyConnectedTab() {
|
function anyConnectedTab() {
|
||||||
@@ -232,24 +265,26 @@ async function handleForwardCdpCommand(msg) {
|
|||||||
// Fail loudly instead so the agent sees an actionable error, not bad data.
|
// Fail loudly instead so the agent sees an actionable error, not bad data.
|
||||||
let tabId
|
let tabId
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
tabId = tabForSession(sessionId)
|
// The stable Chrome tabId encoded in `cb-tab-<tabId>` is the source of truth
|
||||||
if (!tabId) {
|
// (it survives renderer-process swaps; the CDP target/sessionId does not).
|
||||||
// The session's debugger handle is gone, but `cb-tab-<tabId>` encodes the
|
// Resolve via it primarily — don't depend on a session→tab map entry that the
|
||||||
// STABLE Chrome tabId (#17). A cross-process navigation (e.g. an SSO
|
// detach handler may have cleared — and ensure the debugger is attached,
|
||||||
// redirect to another origin), a service-worker restart, or DevTools
|
// re-attaching across a cross-process nav before failing (issues #20.1, #23).
|
||||||
// briefly stealing the debugger all tear the handle down while the tab
|
// `tabForSession` still covers child/iframe sessions that aren't `cb-tab-*`.
|
||||||
// itself lives on. Before failing, try to transparently re-attach to that
|
tabId = tabIdFromSession(sessionId) ?? tabForSession(sessionId)
|
||||||
// same tab and retry — so `open`/`navigate`/`eval` self-heal instead of
|
if (tabId == null) {
|
||||||
// dead-ending the agent (issue #20.1). attachTab re-mints the identical
|
throw new Error(`unknown sessionId ${sessionId} for ${method}`)
|
||||||
// `cb-tab-<tabId>` session, so the daemon's binding stays valid.
|
}
|
||||||
tabId = await recoverSessionTab(sessionId)
|
if (!tabs.has(tabId)) {
|
||||||
if (!tabId) {
|
const recovered = await recoverSessionTab(sessionId)
|
||||||
|
if (!recovered) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`stale sessionId ${sessionId} for ${method}: its tab is gone (closed, ` +
|
`stale sessionId ${sessionId} for ${method}: its tab is gone (closed, ` +
|
||||||
`navigated across processes, or lost after an extension restart). ` +
|
`navigated across processes, or lost after an extension restart). ` +
|
||||||
`Re-attach by re-opening your target URL before retrying.`,
|
`Re-attach by re-opening your target URL before retrying.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
tabId = recovered
|
||||||
}
|
}
|
||||||
} else if (typeof params?.targetId === 'string') {
|
} else if (typeof params?.targetId === 'string') {
|
||||||
tabId = tabForTarget(params.targetId)
|
tabId = tabForTarget(params.targetId)
|
||||||
@@ -259,18 +294,17 @@ async function handleForwardCdpCommand(msg) {
|
|||||||
// applies to any attached tab.
|
// applies to any attached tab.
|
||||||
tabId = anyConnectedTab()
|
tabId = anyConnectedTab()
|
||||||
}
|
}
|
||||||
if (!tabId) throw new Error(`no attached tab for ${method}`)
|
if (tabId == null) throw new Error(`no attached tab for ${method}`)
|
||||||
const dbg = { tabId }
|
|
||||||
|
|
||||||
// Re-enabling Runtime can leave a stale state; bounce it (matches upstream).
|
// Re-enabling Runtime can leave a stale state; bounce it (matches upstream).
|
||||||
if (method === 'Runtime.enable') {
|
if (method === 'Runtime.enable') {
|
||||||
try {
|
try {
|
||||||
await chrome.debugger.sendCommand(dbg, 'Runtime.disable')
|
await sendCdpToTab(tabId, 'Runtime.disable', undefined)
|
||||||
await new Promise((r) => setTimeout(r, 30))
|
await new Promise((r) => setTimeout(r, 30))
|
||||||
} catch {}
|
} catch {}
|
||||||
return await chrome.debugger.sendCommand(dbg, 'Runtime.enable', params)
|
return await sendCdpToTab(tabId, 'Runtime.enable', params)
|
||||||
}
|
}
|
||||||
return await chrome.debugger.sendCommand(dbg, method, params)
|
return await sendCdpToTab(tabId, method, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- attach / detach ------------------------------------------------------
|
// ---- attach / detach ------------------------------------------------------
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "0.4.7",
|
"version": "0.4.8",
|
||||||
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.1.3",
|
"packageManager": "pnpm@11.1.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user