hyper-agent-browser 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -204,11 +204,12 @@ hyper-agent-browser 专为 AI Agent 设计,可与 Claude Code 无缝集成。
204
204
 
205
205
  ```bash
206
206
  # 方法 1:从本地仓库复制
207
- mkdir -p ~/.claude/skills
208
- cp skills/hyper-browser.md ~/.claude/skills/
207
+ mkdir -p ~/.claude/skills/hyper-agent-browser
208
+ cp skills/hyper-browser.md ~/.claude/skills/hyper-agent-browser/skill.md
209
209
 
210
210
  # 方法 2:直接下载
211
- curl -o ~/.claude/skills/hyper-browser.md \
211
+ mkdir -p ~/.claude/skills/hyper-agent-browser
212
+ curl -o ~/.claude/skills/hyper-agent-browser/skill.md \
212
213
  https://raw.githubusercontent.com/hubo1989/hyper-agent-browser/main/skills/hyper-browser.md
213
214
  ```
214
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-agent-browser",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Pure browser automation CLI for AI Agents - 纯浏览器自动化 CLI,专为 AI Agent 设计",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
package/src/cli.ts CHANGED
@@ -747,7 +747,7 @@ program
747
747
  .command("version")
748
748
  .description("Show version information")
749
749
  .action(() => {
750
- console.log("hyper-agent-browser v0.2.0 (with daemon architecture)");
750
+ console.log("hyper-agent-browser v0.3.1 (with daemon architecture)");
751
751
  console.log(`Bun v${Bun.version}`);
752
752
  console.log("Patchright v1.55.1");
753
753
  });
@@ -2,55 +2,95 @@ import { BrowserManager } from "../browser/manager";
2
2
  import type { BrowserManagerOptions } from "../browser/manager";
3
3
  import type { Session } from "../session/store";
4
4
 
5
+ interface CachedBrowser {
6
+ manager: BrowserManager;
7
+ options: BrowserManagerOptions;
8
+ }
9
+
5
10
  /**
6
11
  * BrowserPool 管理多个 Session 的浏览器实例
7
12
  */
8
13
  export class BrowserPool {
9
- private browsers: Map<string, BrowserManager> = new Map();
14
+ private browsers: Map<string, CachedBrowser> = new Map();
10
15
 
11
16
  async get(session: Session, options: BrowserManagerOptions = {}): Promise<BrowserManager> {
12
17
  const key = session.name;
13
18
 
14
19
  if (this.browsers.has(key)) {
15
- const browser = this.browsers.get(key)!;
16
-
17
- // 检查浏览器是否还连接
18
- if (browser.isConnected()) {
20
+ const cached = this.browsers.get(key)!;
21
+ const browser = cached.manager;
22
+
23
+ // 检查选项是否一致(特别是 headed 模式)
24
+ const optionsMismatch = this.hasOptionsMismatch(cached.options, options);
25
+
26
+ if (optionsMismatch) {
27
+ console.log(
28
+ `Options mismatch for session ${key} (headed: ${cached.options.headed} -> ${options.headed}), recreating browser`,
29
+ );
30
+ try {
31
+ await browser.close();
32
+ } catch (error) {
33
+ console.error("Error closing browser for options change:", error);
34
+ }
35
+ this.browsers.delete(key);
36
+ // 继续创建新的浏览器实例
37
+ } else if (browser.isConnected()) {
38
+ // 选项一致且浏览器已连接,直接返回
19
39
  return browser;
20
- }
21
-
22
- // 浏览器断开连接,尝试重连
23
- try {
24
- await browser.connect();
25
- if (browser.isConnected()) {
26
- return browser;
40
+ } else {
41
+ // 浏览器断开连接,尝试重连
42
+ try {
43
+ await browser.connect();
44
+ if (browser.isConnected()) {
45
+ return browser;
46
+ }
47
+ } catch (error) {
48
+ console.error(`Failed to reconnect browser for session ${key}:`, error);
27
49
  }
28
- } catch (error) {
29
- console.error(`Failed to reconnect browser for session ${key}:`, error);
30
- }
31
50
 
32
- // 重连失败,移除旧实例并创建新的
33
- console.log(`Removing stale browser instance for session ${key}`);
34
- this.browsers.delete(key);
51
+ // 重连失败,移除旧实例并创建新的
52
+ console.log(`Removing stale browser instance for session ${key}`);
53
+ this.browsers.delete(key);
54
+ }
35
55
  }
36
56
 
37
57
  // 创建新的浏览器实例
38
- console.log(`Creating new browser instance for session ${key}`);
58
+ console.log(
59
+ `Creating new browser instance for session ${key} (headed: ${options.headed ?? false})`,
60
+ );
39
61
  const browser = new BrowserManager(session, options);
40
62
  await browser.connect();
41
- this.browsers.set(key, browser);
63
+ this.browsers.set(key, { manager: browser, options: { ...options } });
42
64
 
43
65
  return browser;
44
66
  }
45
67
 
68
+ /**
69
+ * 检查选项是否有重要变化需要重建浏览器
70
+ */
71
+ private hasOptionsMismatch(
72
+ cached: BrowserManagerOptions,
73
+ requested: BrowserManagerOptions,
74
+ ): boolean {
75
+ // headed 模式变化需要重建浏览器
76
+ if ((cached.headed ?? false) !== (requested.headed ?? false)) {
77
+ return true;
78
+ }
79
+ // channel 变化也需要重建
80
+ if (cached.channel && requested.channel && cached.channel !== requested.channel) {
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+
46
86
  async close(sessionName: string): Promise<boolean> {
47
- const browser = this.browsers.get(sessionName);
48
- if (!browser) {
87
+ const cached = this.browsers.get(sessionName);
88
+ if (!cached) {
49
89
  return false;
50
90
  }
51
91
 
52
92
  try {
53
- await browser.close();
93
+ await cached.manager.close();
54
94
  } catch (error) {
55
95
  console.error(`Error closing browser for session ${sessionName}:`, error);
56
96
  }
@@ -59,9 +99,9 @@ export class BrowserPool {
59
99
  }
60
100
 
61
101
  async closeAll(): Promise<void> {
62
- const closePromises = Array.from(this.browsers.entries()).map(async ([name, browser]) => {
102
+ const closePromises = Array.from(this.browsers.entries()).map(async ([name, cached]) => {
63
103
  try {
64
- await browser.close();
104
+ await cached.manager.close();
65
105
  } catch (error) {
66
106
  console.error(`Error closing browser for session ${name}:`, error);
67
107
  }
@@ -84,8 +124,8 @@ export class BrowserPool {
84
124
  async cleanup(): Promise<void> {
85
125
  const toRemove: string[] = [];
86
126
 
87
- for (const [name, browser] of this.browsers.entries()) {
88
- if (!browser.isConnected()) {
127
+ for (const [name, cached] of this.browsers.entries()) {
128
+ if (!cached.manager.isConnected()) {
89
129
  toRemove.push(name);
90
130
  }
91
131
  }
@@ -101,30 +101,43 @@ export class DomSnapshotExtractor {
101
101
  // Try ID first
102
102
  if (el.id) return `#${el.id}`;
103
103
 
104
- // Generate nth-child selector
105
- let selector = el.tagName.toLowerCase();
104
+ // Build path from element to root (or an element with ID)
105
+ const path: string[] = [];
106
106
  let current: Element | null = el;
107
107
 
108
- while (current?.parentElement) {
109
- const parent: Element = current.parentElement;
110
- const siblings = Array.from(parent.children);
111
- const index = siblings.indexOf(current);
112
-
113
- if (index >= 0) {
114
- selector = `${parent.tagName.toLowerCase()} > ${selector}:nth-child(${index + 1})`;
108
+ while (current && current !== document.body && path.length < 6) {
109
+ const parentEl: Element | null = current.parentElement;
110
+ if (!parentEl) break;
111
+
112
+ // Find index among siblings of same tag type
113
+ const currentTag = current.tagName;
114
+ const siblings: Element[] = [];
115
+ for (let i = 0; i < parentEl.children.length; i++) {
116
+ const child = parentEl.children[i];
117
+ if (child.tagName === currentTag) {
118
+ siblings.push(child);
119
+ }
115
120
  }
121
+ const index = siblings.indexOf(current) + 1;
122
+
123
+ // Use nth-of-type for uniqueness among same-tag siblings
124
+ const segment =
125
+ siblings.length > 1
126
+ ? `${current.tagName.toLowerCase()}:nth-of-type(${index})`
127
+ : current.tagName.toLowerCase();
128
+
129
+ path.unshift(segment);
116
130
 
117
- current = parent;
118
- if (current?.id) {
119
- selector = `#${current.id} ${selector}`;
131
+ // Stop if parent has ID
132
+ if (parentEl.id) {
133
+ path.unshift(`#${parentEl.id}`);
120
134
  break;
121
135
  }
122
136
 
123
- // Limit depth
124
- if (selector.split(">").length > 5) break;
137
+ current = parentEl;
125
138
  }
126
139
 
127
- return selector;
140
+ return path.join(" > ");
128
141
  }
129
142
 
130
143
  // Traverse DOM