@chen-rmag/ai-runner 0.1.0
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 +263 -0
- package/SUMMARY_USAGE.md +359 -0
- package/TOOLS_INTEGRATION_SUMMARY.md +206 -0
- package/dist/agents/error-analyzer.d.ts +62 -0
- package/dist/agents/error-analyzer.d.ts.map +1 -0
- package/dist/agents/error-analyzer.js +168 -0
- package/dist/agents/error-analyzer.js.map +1 -0
- package/dist/agents/heal-agent.d.ts +30 -0
- package/dist/agents/heal-agent.d.ts.map +1 -0
- package/dist/agents/heal-agent.js +76 -0
- package/dist/agents/heal-agent.js.map +1 -0
- package/dist/agents/healer.d.ts +73 -0
- package/dist/agents/healer.d.ts.map +1 -0
- package/dist/agents/healer.js +538 -0
- package/dist/agents/healer.js.map +1 -0
- package/dist/agents/langgraph-agent.d.ts +44 -0
- package/dist/agents/langgraph-agent.d.ts.map +1 -0
- package/dist/agents/langgraph-agent.js +328 -0
- package/dist/agents/langgraph-agent.js.map +1 -0
- package/dist/agents/react-agent.d.ts +52 -0
- package/dist/agents/react-agent.d.ts.map +1 -0
- package/dist/agents/react-agent.js +262 -0
- package/dist/agents/react-agent.js.map +1 -0
- package/dist/agents/tools/form.d.ts +22 -0
- package/dist/agents/tools/form.d.ts.map +1 -0
- package/dist/agents/tools/form.js +134 -0
- package/dist/agents/tools/form.js.map +1 -0
- package/dist/agents/tools/index.d.ts +13 -0
- package/dist/agents/tools/index.d.ts.map +1 -0
- package/dist/agents/tools/index.js +33 -0
- package/dist/agents/tools/index.js.map +1 -0
- package/dist/agents/tools/navigate.d.ts +22 -0
- package/dist/agents/tools/navigate.d.ts.map +1 -0
- package/dist/agents/tools/navigate.js +74 -0
- package/dist/agents/tools/navigate.js.map +1 -0
- package/dist/agents/tools/snapshot.d.ts +22 -0
- package/dist/agents/tools/snapshot.d.ts.map +1 -0
- package/dist/agents/tools/snapshot.js +110 -0
- package/dist/agents/tools/snapshot.js.map +1 -0
- package/dist/agents/tools/verify.d.ts +34 -0
- package/dist/agents/tools/verify.d.ts.map +1 -0
- package/dist/agents/tools/verify.js +169 -0
- package/dist/agents/tools/verify.js.map +1 -0
- package/dist/agents/tools/wait.d.ts +22 -0
- package/dist/agents/tools/wait.d.ts.map +1 -0
- package/dist/agents/tools/wait.js +104 -0
- package/dist/agents/tools/wait.js.map +1 -0
- package/dist/agents/types.d.ts +51 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +6 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/core/ai-heal.d.ts +89 -0
- package/dist/core/ai-heal.d.ts.map +1 -0
- package/dist/core/ai-heal.js +468 -0
- package/dist/core/ai-heal.js.map +1 -0
- package/dist/core/execution-engine.d.ts +16 -0
- package/dist/core/execution-engine.d.ts.map +1 -0
- package/dist/core/execution-engine.js +44 -0
- package/dist/core/execution-engine.js.map +1 -0
- package/dist/core/runner.d.ts +195 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +658 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/types/external.d.ts +6 -0
- package/dist/types/external.d.ts.map +1 -0
- package/dist/types/external.js +7 -0
- package/dist/types/external.js.map +1 -0
- package/dist/types/index.d.ts +153 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +26 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/object-registry.d.ts +48 -0
- package/dist/utils/object-registry.d.ts.map +1 -0
- package/dist/utils/object-registry.js +133 -0
- package/dist/utils/object-registry.js.map +1 -0
- package/package.json +37 -0
- package/playwright.config.ts +38 -0
- package/src/agents/heal-agent.ts +85 -0
- package/src/agents/healer.ts +619 -0
- package/src/agents/tools/EXAMPLES.md +347 -0
- package/src/agents/tools/README.md +207 -0
- package/src/agents/tools/form.ts +138 -0
- package/src/agents/tools/index.ts +29 -0
- package/src/agents/tools/navigate.ts +69 -0
- package/src/agents/tools/snapshot.ts +109 -0
- package/src/agents/tools/verify.ts +168 -0
- package/src/agents/tools/wait.ts +103 -0
- package/src/agents/types.ts +79 -0
- package/src/core/runner.ts +756 -0
- package/src/index.ts +29 -0
- package/src/types/external.ts +7 -0
- package/src/types/index.ts +200 -0
- package/tests/agent/test-heal-agent.spec.ts +81 -0
- package/tests/tools/README.md +227 -0
- package/tests/tools/TEST_SUMMARY.md +214 -0
- package/tests/tools/quick-test.ts +88 -0
- package/tests/tools/tools.test.ts +491 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# LangChain Playwright 工具使用示例
|
|
2
|
+
|
|
3
|
+
## 完整示例:在 Healer 中使用工具
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Page } from 'playwright';
|
|
7
|
+
import { createAgent } from '@langchain/core';
|
|
8
|
+
import { getAllTools } from './tools';
|
|
9
|
+
|
|
10
|
+
class Healer {
|
|
11
|
+
private llm: BaseChatModel;
|
|
12
|
+
private page: Page;
|
|
13
|
+
|
|
14
|
+
constructor(llm: BaseChatModel, page: Page) {
|
|
15
|
+
this.llm = llm;
|
|
16
|
+
this.page = page;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(objective: string) {
|
|
20
|
+
// 获取所有工具
|
|
21
|
+
const tools = getAllTools(this.page);
|
|
22
|
+
|
|
23
|
+
// 创建 Agent
|
|
24
|
+
const agent = createAgent({
|
|
25
|
+
model: this.llm,
|
|
26
|
+
tools: tools,
|
|
27
|
+
systemPrompt: `
|
|
28
|
+
你是一个浏览器自动化专家。
|
|
29
|
+
使用提供的工具完成用户的目标。
|
|
30
|
+
|
|
31
|
+
可用工具包括:
|
|
32
|
+
- 导航工具:browser_navigate, browser_navigate_back
|
|
33
|
+
- 交互工具:browser_click, browser_hover
|
|
34
|
+
- 表单工具:browser_fill, browser_select_option
|
|
35
|
+
- 等待工具:browser_wait_for_selector, browser_wait_for_time
|
|
36
|
+
- 验证工具:browser_get_url, browser_screenshot, browser_verify_text
|
|
37
|
+
|
|
38
|
+
请根据目标选择合适的工具。
|
|
39
|
+
`
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 执行 Agent
|
|
43
|
+
const result = await agent.invoke({
|
|
44
|
+
messages: [{
|
|
45
|
+
role: 'user',
|
|
46
|
+
content: objective
|
|
47
|
+
}]
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 使用示例
|
|
55
|
+
const page = await browser.newPage();
|
|
56
|
+
const healer = new Healer(llm, page);
|
|
57
|
+
await healer.execute('打开 example.com 并点击第一个链接');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 单独使用工具
|
|
61
|
+
|
|
62
|
+
### 导航工具
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { createNavigateTool, createGoBackTool } from './tools/navigate';
|
|
66
|
+
|
|
67
|
+
const navigateTool = createNavigateTool(page);
|
|
68
|
+
const result = await navigateTool.invoke('https://example.com');
|
|
69
|
+
console.log(result); // "Navigated to https://example.com"
|
|
70
|
+
|
|
71
|
+
const goBackTool = createGoBackTool(page);
|
|
72
|
+
await goBackTool.invoke('');
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 交互工具
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { createClickTool, createHoverTool } from './tools/snapshot';
|
|
79
|
+
|
|
80
|
+
const clickTool = createClickTool(page);
|
|
81
|
+
|
|
82
|
+
// 点击按钮
|
|
83
|
+
await clickTool.invoke('button');
|
|
84
|
+
|
|
85
|
+
// 使用多个选择器
|
|
86
|
+
await clickTool.invoke('button[type="submit"], #submit, .submit-btn');
|
|
87
|
+
|
|
88
|
+
const hoverTool = createHoverTool(page);
|
|
89
|
+
await hoverTool.invoke('nav.menu');
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 表单工具
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import {
|
|
96
|
+
createFillFormTool,
|
|
97
|
+
createSelectOptionTool,
|
|
98
|
+
createSetCheckedTool
|
|
99
|
+
} from './tools/form';
|
|
100
|
+
|
|
101
|
+
const fillTool = createFillFormTool(page);
|
|
102
|
+
await fillTool.invoke('{"selector": "#username", "value": "john"}');
|
|
103
|
+
|
|
104
|
+
const selectTool = createSelectOptionTool(page);
|
|
105
|
+
await selectTool.invoke('{"selector": "select#country", "value": "China"}');
|
|
106
|
+
|
|
107
|
+
const checkTool = createSetCheckedTool(page);
|
|
108
|
+
await checkTool.invoke('{"selector": "#agree", "checked": true}');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 等待工具
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import {
|
|
115
|
+
createWaitForTimeTool,
|
|
116
|
+
createWaitForSelectorTool,
|
|
117
|
+
createWaitForURLTool
|
|
118
|
+
} from './tools/wait';
|
|
119
|
+
|
|
120
|
+
const waitTimeTool = createWaitForTimeTool(page);
|
|
121
|
+
await waitTimeTool.invoke('5000'); // 等待 5 秒
|
|
122
|
+
|
|
123
|
+
const waitSelectorTool = createWaitForSelectorTool(page);
|
|
124
|
+
await waitSelectorTool.invoke('button, timeout=5000'); // 等待按钮出现,超时 5 秒
|
|
125
|
+
|
|
126
|
+
const waitURLTool = createWaitForURLTool(page);
|
|
127
|
+
await waitURLTool.invoke('**/login'); // 等待 URL 匹配
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 验证工具
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import {
|
|
134
|
+
createGetURLTool,
|
|
135
|
+
createGetTextTool,
|
|
136
|
+
createScreenshotTool,
|
|
137
|
+
createVerifyTextVisibleTool
|
|
138
|
+
} from './tools/verify';
|
|
139
|
+
|
|
140
|
+
const getURLTool = createGetURLTool(page);
|
|
141
|
+
const url = await getURLTool.invoke('');
|
|
142
|
+
console.log(url); // "Current URL: https://example.com"
|
|
143
|
+
|
|
144
|
+
const getTextTool = createGetTextTool(page);
|
|
145
|
+
const text = await getTextTool.invoke('h1');
|
|
146
|
+
console.log(text); // "Text from element "h1": Example Domain"
|
|
147
|
+
|
|
148
|
+
const screenshotTool = createScreenshotTool(page);
|
|
149
|
+
await screenshotTool.invoke(''); // 保存截图
|
|
150
|
+
|
|
151
|
+
const verifyTool = createVerifyTextVisibleTool(page);
|
|
152
|
+
await verifyTool.invoke('Welcome'); // 验证文本可见
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## 在现有 Healer 中集成
|
|
156
|
+
|
|
157
|
+
### 替换现有的 createPlaywrightTools
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// healer.ts
|
|
161
|
+
import { getAllTools } from './tools';
|
|
162
|
+
|
|
163
|
+
class Healer {
|
|
164
|
+
private createPlaywrightTools(page: Page): DynamicTool[] {
|
|
165
|
+
// 使用新的工具集
|
|
166
|
+
return getAllTools(page);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async execute(state: HealAgentState) {
|
|
170
|
+
const page = state.context.page;
|
|
171
|
+
|
|
172
|
+
// 获取工具
|
|
173
|
+
const tools = this.createPlaywrightTools(page);
|
|
174
|
+
|
|
175
|
+
// 创建 Agent
|
|
176
|
+
const agent = createAgent({
|
|
177
|
+
model: this.llm,
|
|
178
|
+
tools: tools,
|
|
179
|
+
systemPrompt: this.buildSystemPrompt(state)
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 执行...
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 按需选择工具
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { navigateTools, snapshotTools, waitTools } from './tools';
|
|
191
|
+
|
|
192
|
+
class Healer {
|
|
193
|
+
private createMinimalTools(page: Page): DynamicTool[] {
|
|
194
|
+
// 只使用必要的工具
|
|
195
|
+
return [
|
|
196
|
+
...navigateTools(page),
|
|
197
|
+
...snapshotTools(page).slice(0, 2), // 只使用 snapshot 和 click
|
|
198
|
+
...waitTools(page).slice(0, 1), // 只使用 wait_for_time
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 高级用法
|
|
205
|
+
|
|
206
|
+
### 自定义工具组合
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import {
|
|
210
|
+
createNavigateTool,
|
|
211
|
+
createClickTool,
|
|
212
|
+
createFillFormTool,
|
|
213
|
+
createWaitForSelectorTool
|
|
214
|
+
} from './tools';
|
|
215
|
+
|
|
216
|
+
const customToolSet = (page: Page) => [
|
|
217
|
+
createNavigateTool(page),
|
|
218
|
+
createClickTool(page),
|
|
219
|
+
createFillFormTool(page),
|
|
220
|
+
createWaitForSelectorTool(page),
|
|
221
|
+
];
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 包装工具以添加日志
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { createClickTool } from './tools/snapshot';
|
|
228
|
+
|
|
229
|
+
function createClickToolWithLogging(page: Page) {
|
|
230
|
+
const tool = createClickTool(page);
|
|
231
|
+
|
|
232
|
+
return new DynamicTool({
|
|
233
|
+
name: tool.name,
|
|
234
|
+
description: tool.description,
|
|
235
|
+
func: async (input: string) => {
|
|
236
|
+
console.log(`[Tool] Calling ${tool.name} with input: ${input}`);
|
|
237
|
+
const startTime = Date.now();
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const result = await tool.invoke(input);
|
|
241
|
+
const duration = Date.now() - startTime;
|
|
242
|
+
console.log(`[Tool] ${tool.name} completed in ${duration}ms`);
|
|
243
|
+
return result;
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error(`[Tool] ${tool.name} failed:`, error);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 组合多个工具
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { createClickTool, createFillFormTool } from './tools';
|
|
257
|
+
|
|
258
|
+
async function fillAndSubmitForm(page: Page) {
|
|
259
|
+
const fillTool = createFillFormTool(page);
|
|
260
|
+
const clickTool = createClickTool(page);
|
|
261
|
+
|
|
262
|
+
// 填写表单
|
|
263
|
+
await fillTool.invoke('{"selector": "#email", "value": "test@example.com"}');
|
|
264
|
+
await fillTool.invoke('{"selector": "#password", "value": "secret"}');
|
|
265
|
+
|
|
266
|
+
// 点击提交按钮
|
|
267
|
+
await clickTool.invoke('button[type="submit"]');
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## 错误处理
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { createClickTool } from './tools/snapshot';
|
|
275
|
+
|
|
276
|
+
async function safeClick(page: Page, selector: string) {
|
|
277
|
+
const tool = createClickTool(page);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const result = await tool.invoke(selector);
|
|
281
|
+
console.log('Success:', result);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('Failed to click:', (error as Error).message);
|
|
284
|
+
|
|
285
|
+
// 尝试备选方案
|
|
286
|
+
console.log('Trying alternative selector...');
|
|
287
|
+
await tool.wait(1000); // 等待一下
|
|
288
|
+
await tool.invoke(selector);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## 性能优化
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { getAllTools } from './tools';
|
|
297
|
+
|
|
298
|
+
// 缓存工具实例
|
|
299
|
+
class ToolCache {
|
|
300
|
+
private tools: Map<Page, DynamicTool[]> = new Map();
|
|
301
|
+
|
|
302
|
+
get(page: Page): DynamicTool[] {
|
|
303
|
+
if (!this.tools.has(page)) {
|
|
304
|
+
this.tools.set(page, getAllTools(page));
|
|
305
|
+
}
|
|
306
|
+
return this.tools.get(page)!;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const cache = new ToolCache();
|
|
311
|
+
const tools = cache.get(page);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## 调试技巧
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { createClickTool } from './tools';
|
|
318
|
+
|
|
319
|
+
// 添加详细日志
|
|
320
|
+
async function debugClick(page: Page) {
|
|
321
|
+
const tool = createClickTool(page);
|
|
322
|
+
|
|
323
|
+
console.log('Available tools:', [tool.name]);
|
|
324
|
+
console.log('Tool description:', tool.description);
|
|
325
|
+
|
|
326
|
+
const selectors = ['button', 'a', 'input[type="submit"]'];
|
|
327
|
+
|
|
328
|
+
for (const selector of selectors) {
|
|
329
|
+
console.log(`Trying selector: ${selector}`);
|
|
330
|
+
try {
|
|
331
|
+
const result = await tool.invoke(selector);
|
|
332
|
+
console.log(`✓ Success with "${selector}":`, result);
|
|
333
|
+
break;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.log(`✗ Failed with "${selector}"`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## 注意事项
|
|
342
|
+
|
|
343
|
+
1. **超时处理**:大多数工具有默认超时,注意处理超时异常
|
|
344
|
+
2. **选择器优先级**:工具会按优先级尝试不同类型的选择器
|
|
345
|
+
3. **返回值**:所有工具返回字符串格式的结果
|
|
346
|
+
4. **JSON 输入**:某些工具需要 JSON 格式的输入,注意格式正确性
|
|
347
|
+
5. **错误传播**:工具失败会抛出异常,需要在外部捕获处理
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# LangChain 兼容的 Playwright 工具
|
|
2
|
+
|
|
3
|
+
这是从 Playwright MCP 工具转换而来的 LangChain 兼容工具集,专门用于自愈 Agent。
|
|
4
|
+
|
|
5
|
+
## 工具分类
|
|
6
|
+
|
|
7
|
+
### 1. 导航工具 (`navigate.ts`)
|
|
8
|
+
|
|
9
|
+
- **browser_navigate** - 导航到指定 URL
|
|
10
|
+
- **browser_navigate_back** - 返回上一页
|
|
11
|
+
- **browser_navigate_forward** - 前进到下一页
|
|
12
|
+
|
|
13
|
+
### 2. 快照和交互工具 (`snapshot.ts`)
|
|
14
|
+
|
|
15
|
+
- **browser_snapshot** - 获取页面的可访问性快照
|
|
16
|
+
- **browser_click** - 点击元素(支持多个选择器)
|
|
17
|
+
- **browser_hover** - 悬停在元素上
|
|
18
|
+
|
|
19
|
+
### 3. 表单工具 (`form.ts`)
|
|
20
|
+
|
|
21
|
+
- **browser_fill** - 填写表单字段
|
|
22
|
+
- **browser_select_option** - 选择下拉选项
|
|
23
|
+
- **browser_set_checked** - 勾选/取消复选框
|
|
24
|
+
|
|
25
|
+
### 4. 等待工具 (`wait.ts`)
|
|
26
|
+
|
|
27
|
+
- **browser_wait_for_time** - 等待指定时间
|
|
28
|
+
- **browser_wait_for_selector** - 等待元素出现
|
|
29
|
+
- **browser_wait_for_url** - 等待 URL 匹配
|
|
30
|
+
|
|
31
|
+
### 5. 验证工具 (`verify.ts`)
|
|
32
|
+
|
|
33
|
+
- **browser_get_url** - 获取当前 URL
|
|
34
|
+
- **browser_get_content** - 获取页面内容
|
|
35
|
+
- **browser_get_text** - 获取元素文本
|
|
36
|
+
- **browser_screenshot** - 截取页面截图
|
|
37
|
+
- **browser_verify_text** - 验证文本可见
|
|
38
|
+
- **browser_verify_element** - 验证元素可见
|
|
39
|
+
|
|
40
|
+
## 使用方法
|
|
41
|
+
|
|
42
|
+
### 基本用法
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { Page } from 'playwright';
|
|
46
|
+
import { allTools } from './tools';
|
|
47
|
+
|
|
48
|
+
const page: Page = await browser.newPage();
|
|
49
|
+
|
|
50
|
+
// 获取所有工具
|
|
51
|
+
const tools = allTools(page);
|
|
52
|
+
|
|
53
|
+
// 在 LangChain Agent 中使用
|
|
54
|
+
import { createReactAgent } from 'langchain/agents';
|
|
55
|
+
|
|
56
|
+
const agent = createReactAgent({
|
|
57
|
+
llm: yourLLM,
|
|
58
|
+
tools: tools,
|
|
59
|
+
verbose: true
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 按需导入
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// 只导入导航工具
|
|
67
|
+
import { navigateTools } from './tools/navigate';
|
|
68
|
+
|
|
69
|
+
const tools = navigateTools(page);
|
|
70
|
+
|
|
71
|
+
// 只导入交互工具
|
|
72
|
+
import { snapshotTools } from './tools/snapshot';
|
|
73
|
+
|
|
74
|
+
const tools = snapshotTools(page);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 工具特性
|
|
78
|
+
|
|
79
|
+
### 1. 多选择器支持
|
|
80
|
+
|
|
81
|
+
大多数交互工具支持多个选择器(用逗号分隔):
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// 工具会依次尝试每个选择器,直到成功
|
|
85
|
+
await clickTool.run('button, input[type="submit"], #submit-btn');
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. 智能选择器匹配
|
|
89
|
+
|
|
90
|
+
工具会自动尝试不同类型的选择器:
|
|
91
|
+
|
|
92
|
+
1. **文本选择器** - 通过文本内容查找元素
|
|
93
|
+
2. **标签选择器** - 通过 label 关联查找表单元素
|
|
94
|
+
3. **CSS 选择器** - 使用标准 CSS 选择器
|
|
95
|
+
|
|
96
|
+
### 3. 错误处理
|
|
97
|
+
|
|
98
|
+
所有工具都有完善的错误处理:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
try {
|
|
102
|
+
const result = await clickTool.run('button');
|
|
103
|
+
console.log(result);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Tool failed:', error.message);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 与自愈 Agent 集成
|
|
110
|
+
|
|
111
|
+
在 Healer 中使用这些工具:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { Healer } from './healer';
|
|
115
|
+
import { allTools } from './tools';
|
|
116
|
+
|
|
117
|
+
class Healer {
|
|
118
|
+
private createTools(page: Page): DynamicTool[] {
|
|
119
|
+
return allTools(page);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async execute(state: HealAgentState) {
|
|
123
|
+
const tools = this.createTools(state.context.page);
|
|
124
|
+
|
|
125
|
+
const agent = createAgent({
|
|
126
|
+
model: this.llm,
|
|
127
|
+
tools: tools,
|
|
128
|
+
systemPrompt: this.buildSystemPrompt(state)
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ... 执行 Agent
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## 工具输入格式
|
|
137
|
+
|
|
138
|
+
### JSON 格式(用于复杂参数)
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// 填写表单
|
|
142
|
+
await fillTool.run('{"selector": "#username", "value": "john"}');
|
|
143
|
+
|
|
144
|
+
// 选择选项
|
|
145
|
+
await selectOptionTool.run('{"selector": "select#country", "value": "China"}');
|
|
146
|
+
|
|
147
|
+
// 勾选复选框
|
|
148
|
+
await setCheckedTool.run('{"selector": "#agree", "checked": true}');
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 带参数的选择器
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// 带超时的等待
|
|
155
|
+
await waitForSelectorTool.run('button, timeout=5000');
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 简单字符串输入
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// 导航
|
|
162
|
+
await navigateTool.run('https://example.com');
|
|
163
|
+
|
|
164
|
+
// 点击
|
|
165
|
+
await clickTool.run('button');
|
|
166
|
+
|
|
167
|
+
// 获取文本
|
|
168
|
+
await getTextTool.run('h1');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 从 MCP 转换说明
|
|
172
|
+
|
|
173
|
+
这些工具是从 Playwright MCP 工具转换而来,主要变化:
|
|
174
|
+
|
|
175
|
+
1. **移除了复杂的上下文系统** - 直接使用 Playwright Page 对象
|
|
176
|
+
2. **简化了输入格式** - 使用简单的字符串或 JSON,而不是 Zod schema
|
|
177
|
+
3. **LangChain 集成** - 使用 `DynamicTool` 包装,兼容 LangChain Agent
|
|
178
|
+
4. **错误处理改进** - 统一的错误处理和消息格式
|
|
179
|
+
5. **多选择器支持** - 增强的元素查找能力
|
|
180
|
+
|
|
181
|
+
## 注意事项
|
|
182
|
+
|
|
183
|
+
1. **超时设置** - 大多数工具有默认的超时时间(通常 30 秒)
|
|
184
|
+
2. **元素查找** - 工具会按优先级尝试不同类型的选择器
|
|
185
|
+
3. **返回值** - 所有工具返回字符串格式的结果消息
|
|
186
|
+
4. **异常处理** - 工具执行失败会抛出异常,需要在外部处理
|
|
187
|
+
|
|
188
|
+
## 扩展工具
|
|
189
|
+
|
|
190
|
+
如果需要添加新工具:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { DynamicTool } from '@langchain/core/tools';
|
|
194
|
+
|
|
195
|
+
export function createYourTool(page: Page): DynamicTool {
|
|
196
|
+
return new DynamicTool({
|
|
197
|
+
name: 'browser_your_tool',
|
|
198
|
+
description: 'Your tool description',
|
|
199
|
+
func: async (input: string) => {
|
|
200
|
+
// 实现你的逻辑
|
|
201
|
+
return 'result message';
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
然后在对应的分类文件中导出,并在 `index.ts` 中注册。
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表单工具 - Form
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DynamicTool } from '@langchain/core/tools';
|
|
6
|
+
import type { Page } from 'playwright';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 创建填写表单工具
|
|
10
|
+
*/
|
|
11
|
+
export function createFillFormTool(page: Page): DynamicTool {
|
|
12
|
+
return new DynamicTool({
|
|
13
|
+
name: 'browser_fill',
|
|
14
|
+
description: 'Fill in a form field. Input should be in JSON format: {"selector": "css-selector-or-text", "value": "value-to-fill"}. For checkboxes, use "true" or "false" as the value.',
|
|
15
|
+
func: async (input: string) => {
|
|
16
|
+
try {
|
|
17
|
+
const params = JSON.parse(input);
|
|
18
|
+
const { selector, value } = params;
|
|
19
|
+
|
|
20
|
+
// 尝试多个选择器
|
|
21
|
+
const selectors = selector.split(',').map((s: string) => s.trim());
|
|
22
|
+
|
|
23
|
+
for (const sel of selectors) {
|
|
24
|
+
try {
|
|
25
|
+
// 尝试作为文本选择器
|
|
26
|
+
const locator = page.getByLabel(sel).or(page.getByPlaceholder(sel));
|
|
27
|
+
const count = await locator.count();
|
|
28
|
+
if (count > 0) {
|
|
29
|
+
await locator.fill(value);
|
|
30
|
+
return `Filled field with label: "${sel}" with value: "${value}"`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 尝试作为 CSS 选择器
|
|
34
|
+
await page.fill(sel, value);
|
|
35
|
+
return `Filled field: "${sel}" with value: "${value}"`;
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error(`No field found matching: ${selector}`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`Failed to fill form: ${(error as Error).message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 创建选择选项工具
|
|
51
|
+
*/
|
|
52
|
+
export function createSelectOptionTool(page: Page): DynamicTool {
|
|
53
|
+
return new DynamicTool({
|
|
54
|
+
name: 'browser_select_option',
|
|
55
|
+
description: 'Select an option in a dropdown. Input should be in JSON format: {"selector": "css-selector-or-text", "value": "option-value"}.',
|
|
56
|
+
func: async (input: string) => {
|
|
57
|
+
try {
|
|
58
|
+
const params = JSON.parse(input);
|
|
59
|
+
const { selector, value } = params;
|
|
60
|
+
|
|
61
|
+
// 尝试多个选择器
|
|
62
|
+
const selectors = selector.split(',').map((s: string) => s.trim());
|
|
63
|
+
|
|
64
|
+
for (const sel of selectors) {
|
|
65
|
+
try {
|
|
66
|
+
// 尝试作为文本选择器
|
|
67
|
+
const locator = page.getByLabel(sel);
|
|
68
|
+
const count = await locator.count();
|
|
69
|
+
if (count > 0) {
|
|
70
|
+
await locator.selectOption(value);
|
|
71
|
+
return `Selected option "${value}" in dropdown with label: "${sel}"`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 尝试作为 CSS 选择器
|
|
75
|
+
await page.selectOption(sel, value);
|
|
76
|
+
return `Selected option "${value}" in dropdown: "${sel}"`;
|
|
77
|
+
} catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new Error(`No dropdown found matching: ${selector}`);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(`Failed to select option: ${(error as Error).message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 创建复选框工具
|
|
92
|
+
*/
|
|
93
|
+
export function createSetCheckedTool(page: Page): DynamicTool {
|
|
94
|
+
return new DynamicTool({
|
|
95
|
+
name: 'browser_set_checked',
|
|
96
|
+
description: 'Check or uncheck a checkbox. Input should be in JSON format: {"selector": "css-selector-or-text", "checked": true|false}.',
|
|
97
|
+
func: async (input: string) => {
|
|
98
|
+
try {
|
|
99
|
+
const params = JSON.parse(input);
|
|
100
|
+
const { selector, checked } = params;
|
|
101
|
+
|
|
102
|
+
// 尝试多个选择器
|
|
103
|
+
const selectors = selector.split(',').map((s: string) => s.trim());
|
|
104
|
+
|
|
105
|
+
for (const sel of selectors) {
|
|
106
|
+
try {
|
|
107
|
+
// 尝试作为文本选择器
|
|
108
|
+
const locator = page.getByLabel(sel);
|
|
109
|
+
const count = await locator.count();
|
|
110
|
+
if (count > 0) {
|
|
111
|
+
await locator.setChecked(checked);
|
|
112
|
+
return `${checked ? 'Checked' : 'Unchecked'} checkbox with label: "${sel}"`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 尝试作为 CSS 选择器
|
|
116
|
+
await page.setChecked(sel, checked);
|
|
117
|
+
return `${checked ? 'Checked' : 'Unchecked'} checkbox: "${sel}"`;
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
throw new Error(`No checkbox found matching: ${selector}`);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`Failed to set checked: ${(error as Error).message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 表单工具集
|
|
133
|
+
*/
|
|
134
|
+
export const formTools = (page: Page): DynamicTool[] => [
|
|
135
|
+
createFillFormTool(page),
|
|
136
|
+
createSelectOptionTool(page),
|
|
137
|
+
createSetCheckedTool(page),
|
|
138
|
+
];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain 兼容的 Playwright 工具集
|
|
3
|
+
* 从 Playwright MCP 工具转换而来,用于自愈 Agent
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DynamicTool } from '@langchain/core/tools';
|
|
7
|
+
import type { Page } from 'playwright';
|
|
8
|
+
import { navigateTools } from './navigate';
|
|
9
|
+
import { snapshotTools } from './snapshot';
|
|
10
|
+
import { formTools } from './form';
|
|
11
|
+
import { waitTools } from './wait';
|
|
12
|
+
import { verifyTools } from './verify';
|
|
13
|
+
|
|
14
|
+
// 重新导出工具工厂函数
|
|
15
|
+
export { navigateTools } from './navigate';
|
|
16
|
+
export { snapshotTools } from './snapshot';
|
|
17
|
+
export { formTools } from './form';
|
|
18
|
+
export { waitTools } from './wait';
|
|
19
|
+
export { verifyTools } from './verify';
|
|
20
|
+
|
|
21
|
+
// 获取所有工具的工厂函数
|
|
22
|
+
export const getAllTools = (page: Page): DynamicTool[] => [
|
|
23
|
+
...navigateTools(page),
|
|
24
|
+
...snapshotTools(page),
|
|
25
|
+
...formTools(page),
|
|
26
|
+
...waitTools(page),
|
|
27
|
+
...verifyTools(page),
|
|
28
|
+
];
|
|
29
|
+
|