Compare commits
12 Commits
235fa1bc26
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f22a730df | ||
|
|
e26589bc54 | ||
|
|
fb4b8fd067 | ||
|
|
2bed20cd85 | ||
|
|
c48c7c524e | ||
|
|
32f27e73e3 | ||
|
|
71738f1966 | ||
|
|
e44455ba80 | ||
|
|
d2ee260d82 | ||
|
|
1f738bc41c | ||
|
|
2802ab3dea | ||
|
|
e6a2e07640 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
node-version: [24]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
2
.github/workflows/deploy-demo.yml
vendored
2
.github/workflows/deploy-demo.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
58
demo_erp_scrape.py
Normal file
58
demo_erp_scrape.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import asyncio
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
# 加载当前目录下的 .env 文件
|
||||
load_dotenv()
|
||||
|
||||
async def run_browser_automation():
|
||||
print("⏳ 正在启动 Page Agent MCP 服务器...")
|
||||
print("👉 如果你没有打开浏览器,系统会自动帮你唤起默认浏览器!\n")
|
||||
|
||||
# 1. 配置 MCP 服务端
|
||||
# 它会自动读取当前系统的 PATH,调用 npx 启动 @page-agent/mcp
|
||||
server_params = StdioServerParameters(
|
||||
command="npx",
|
||||
args=["-y", "@page-agent/mcp"],
|
||||
env={
|
||||
**os.environ,
|
||||
"LLM_BASE_URL": os.getenv("LLM_BASE_URL", ""),
|
||||
"LLM_API_KEY": os.getenv("LLM_API_KEY", ""),
|
||||
"LLM_MODEL_NAME": os.getenv("LLM_MODEL_NAME", "qwen-plus")
|
||||
}
|
||||
)
|
||||
|
||||
# 2. 通过标准输入输出(Stdio)连接到插件的 MCP 服务
|
||||
async with stdio_client(server_params) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
# 初始化握手
|
||||
await session.initialize()
|
||||
|
||||
print("✅ 成功连接到了 MCP 服务端!正在等待浏览器插件握手...")
|
||||
|
||||
# 轮询等待浏览器插件连上来
|
||||
for _ in range(15):
|
||||
status_res = await session.call_tool("get_status", {})
|
||||
if '"connected": true' in status_res.content[0].text:
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
print("❌ 浏览器插件连接超时!请确保 Chrome 浏览器正常运行,并启用了 Page Agent 插件。")
|
||||
return
|
||||
|
||||
task_prompt = "请你先导航到 https://preview.pro.ant.design/list/table-list/ 页面,然后使用自定义工具 scrapeErpOrdersTool 来抓取当前页面的所有分页表格数据。抓取完成后,请告诉我一共抓取到了多少条数据,并列出前三条作为示例。"
|
||||
print(f"🤖 派发自然语言任务: 【{task_prompt}】\n")
|
||||
print("👁️🗨️ 正在观测浏览器执行... (你可以切回浏览器看着它自动操作,大概需要几十秒)")
|
||||
|
||||
# 调用 MCP 的 execute_task 工具
|
||||
result = await session.call_tool("execute_task", {
|
||||
"task": task_prompt
|
||||
})
|
||||
|
||||
print("\n🎉 浏览器任务执行完毕,大模型返回的最终结果是:")
|
||||
print(result.content[0].text)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_browser_automation())
|
||||
113
docs/custom-tools-guide.md
Normal file
113
docs/custom-tools-guide.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Page Agent 自定义工具注册指南
|
||||
|
||||
在 Page Agent 体系中,你可以通过注册自定义工具(Custom Tools),让大模型能够执行你编写的特定 JavaScript 代码(如:复杂的批量爬虫、与系统底层的原生交互等)。
|
||||
|
||||
## 核心架构原理
|
||||
Page Agent 的 Extension 版本使用了 `MultiPageAgent` 类来初始化。大模型与浏览器的交互是通过 `PageController` 实现的。
|
||||
要增加新的工具,我们只需两步:**编写工具逻辑** -> **在实例化时注入配置**。
|
||||
|
||||
## 步骤一:创建你的自定义工具
|
||||
|
||||
在 `packages/extension/src/agent/` 目录下(或者你自己的工具管理目录),创建一个新的工具文件,例如 `customTools.ts`。
|
||||
|
||||
你需要引入 `@page-agent/core` 中的 `tool` 辅助函数,以及 `zod` 来定义参数格式:
|
||||
|
||||
```typescript
|
||||
// 文件路径:packages/extension/src/agent/customTools.ts
|
||||
import { tool } from '@page-agent/core'
|
||||
import * as z from 'zod/v4'
|
||||
|
||||
export const myCustomTool = tool({
|
||||
// 【必填】Description: 给大模型看的自然语言说明书。写得越清楚,大模型调用的时机越准确。
|
||||
description: '当需要执行特定的复杂业务逻辑(如静默抓取数据)时,调用此工具。',
|
||||
|
||||
// 【必填】InputSchema: 如果工具需要参数(比如你要抓取的目标页数),在这里定义。不需要参数写 z.object({})
|
||||
inputSchema: z.object({
|
||||
targetCount: z.number().optional().describe("目标抓取数量")
|
||||
}),
|
||||
|
||||
// 【必填】Execute: 工具被大模型触发时执行的后台代码
|
||||
execute: async function (input, { signal }) {
|
||||
// 由于这段代码跑在 Chrome Extension 的 Background Worker 中,不能直接操纵网页 DOM。
|
||||
// 同时出于安全限制,插件环境禁用了底层的 executeJavascript 方法。
|
||||
// 我们必须使用 Chrome 原生的 chrome.scripting API 来向目标网页注入脚本。
|
||||
|
||||
// 1. 获取当前大模型正在操作的页面 Tab ID
|
||||
const tabId = (this.pageController as any).currentTabId;
|
||||
if (!tabId) return "报错:未找到激活的页面";
|
||||
|
||||
// 2. 将代码注入目标网页执行
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: tabId },
|
||||
func: async () => {
|
||||
// ... 这段代码将直接跑在网页(如金蝶 ERP)的控制台内部
|
||||
return new Promise((resolve) => {
|
||||
const data = "抓取完成的数据";
|
||||
resolve(data); // 务必把数据 resolve 回后台
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 取出网页传回来的数据
|
||||
const resultData = results[0]?.result;
|
||||
|
||||
// 4. 将数据发送给后端的数据库 API (边抓边存,或最后存)
|
||||
// await fetch('https://api.yourdomain.com/save', { method: 'POST', body: JSON.stringify(resultData) });
|
||||
|
||||
// 5. 返回给大模型汇报工作
|
||||
return \`工具执行成功,已处理数据:\${resultData}\`;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
> **🚨 极其关键的安全权限配置:**
|
||||
> 使用 `chrome.scripting` API 前,你**必须**在插件配置文件 `packages/extension/wxt.config.js` 的 `permissions` 数组中添加 `'scripting'` 权限,否则会一直报错!
|
||||
>
|
||||
> ```javascript
|
||||
> // wxt.config.js
|
||||
> permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage', 'scripting'],
|
||||
> ```
|
||||
|
||||
## 步骤二:将工具注册到 Agent 实例中
|
||||
|
||||
编写完工具后,你必须告诉 Page Agent 存在这个工具。
|
||||
|
||||
打开 `packages/extension/src/agent/MultiPageAgent.ts`(或者你实例化 `PageAgent` 的地方)。在 `customTools` 属性中注册你的工具。
|
||||
|
||||
```typescript
|
||||
// 文件路径:packages/extension/src/agent/MultiPageAgent.ts
|
||||
import { createTabTools } from './tabTools'
|
||||
import { myCustomTool } from './customTools' // 1. 引入你的工具
|
||||
|
||||
export class MultiPageAgent extends PageAgentCore {
|
||||
constructor(config: MultiPageAgentConfig) {
|
||||
// ... (原有的 controller 初始化代码)
|
||||
|
||||
// 2. 将你的工具合并到 customTools 对象中
|
||||
const customTools = {
|
||||
...createTabTools(tabsController), // 保留原有的内置 Tab 切换工具
|
||||
my_custom_tool: myCustomTool, // 注册你的新工具,键名 (my_custom_tool) 就是工具对外的 ID
|
||||
}
|
||||
|
||||
super({
|
||||
// ... (原有的其他配置)
|
||||
customTools: customTools, // 3. 将工具对象传递给底层配置
|
||||
});
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 步骤三:编译与发布插件
|
||||
|
||||
代码修改完毕后,你需要重新构建浏览器插件:
|
||||
|
||||
在项目的根目录(`/Users/seay/Documents/code/page-agent`)执行以下命令:
|
||||
|
||||
```bash
|
||||
npm run build:ext
|
||||
```
|
||||
|
||||
- 该命令会自动编译压缩代码,并在 `packages/extension/.output/` 目录下生成打包好的 `.zip` 文件(例如 `.output/page-agent-ext-1.10.0-chrome.zip`)。
|
||||
- **更新插件:** 将此 `.zip` 文件发给客户解压。在浏览器的 `chrome://extensions/` 界面中覆盖加载该文件夹,并点击“刷新”图标即可生效。大模型立刻就能使用你的新工具了。
|
||||
1359
package-lock.json
generated
1359
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -45,27 +45,28 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^21.0.1",
|
||||
"@commitlint/config-conventional": "^21.0.1",
|
||||
"@eslint-react/eslint-plugin": "^5.8.16",
|
||||
"@eslint-react/eslint-plugin": "^5.9.2",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@microsoft/api-extractor": "^7.58.8",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@microsoft/api-extractor": "^7.58.9",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"@types/node": "^25.9.2",
|
||||
"@types/node": "^26.0.0",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"chalk": "^5.6.2",
|
||||
"concurrently": "^10.0.3",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "^10.4.0",
|
||||
"globals": "^17.6.0",
|
||||
"eslint": "^10.5.0",
|
||||
"globals": "^17.7.0",
|
||||
"happy-dom": "^20.10.6",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^17.0.5",
|
||||
"prettier": "^3.8.3",
|
||||
"lint-staged": "^17.0.8",
|
||||
"prettier": "^3.8.4",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"typescript-eslint": "^8.62.0",
|
||||
"unplugin-dts": "^1.0.1",
|
||||
"vite": "^8.0.14",
|
||||
"vite-plugin-css-injected-by-js": "^5.0.1",
|
||||
"vitest": "^4.1.8"
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"overrides": {
|
||||
"typescript": "^6.0.3",
|
||||
|
||||
@@ -10,27 +10,27 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-hover-card": "^1.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.17",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.9",
|
||||
"@radix-ui/react-separator": "^1.1.9",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@types/chrome": "^0.1.43",
|
||||
"@radix-ui/react-label": "^2.1.10",
|
||||
"@radix-ui/react-separator": "^1.1.10",
|
||||
"@radix-ui/react-slot": "^1.3.0",
|
||||
"@radix-ui/react-switch": "^1.3.1",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@types/chrome": "^0.2.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@wxt-dev/module-react": "^1.2.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"idb": "^8.0.3",
|
||||
"lucide-react": "^1.16.0",
|
||||
"lucide-react": "^1.21.0",
|
||||
"motion": "^12.40.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"rough-notation": "^0.5.1",
|
||||
"simple-icons": "^16.23.0",
|
||||
"simple-icons": "^16.24.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type AgentConfig, PageAgentCore } from '@page-agent/core'
|
||||
|
||||
import { RemotePageController } from './RemotePageController'
|
||||
import { TabsController } from './TabsController'
|
||||
import { scrapeErpOrdersTool } from './customTools'
|
||||
import SYSTEM_PROMPT from './system_prompt.md?raw'
|
||||
import { createTabTools } from './tabTools'
|
||||
|
||||
@@ -26,7 +27,12 @@ export class MultiPageAgent extends PageAgentCore {
|
||||
// multi page controller
|
||||
const tabsController = new TabsController()
|
||||
const pageController = new RemotePageController(tabsController)
|
||||
const customTools = createTabTools(tabsController)
|
||||
|
||||
// 注册内置工具以及我们自定义的 ERP 抓取工具
|
||||
const customTools = {
|
||||
...createTabTools(tabsController),
|
||||
scrape_erp_orders: scrapeErpOrdersTool,
|
||||
}
|
||||
|
||||
// system prompt - auto-detect language if not specified
|
||||
const language = config.language ?? detectLanguage()
|
||||
|
||||
@@ -47,7 +47,7 @@ export class RemotePageController {
|
||||
}
|
||||
|
||||
async getLastUpdateTime(): Promise<number> {
|
||||
if (!this.currentTabId) throw new Error('tabsController not initialized.')
|
||||
if (!this.currentTabId) return Date.now()
|
||||
return sendMessage({
|
||||
type: 'PAGE_CONTROL',
|
||||
action: 'get_last_update_time',
|
||||
|
||||
64
packages/extension/src/agent/customTools.ts
Normal file
64
packages/extension/src/agent/customTools.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { tool } from '@page-agent/core'
|
||||
import * as z from 'zod/v4'
|
||||
|
||||
export const scrapeErpOrdersTool = tool({
|
||||
description:
|
||||
'当到达 ERP 订单列表或查询列表的第一页时,调用此工具。该工具会自动接管浏览器,静默抓取所有分页表格数据。',
|
||||
inputSchema: z.object({}),
|
||||
execute: async function (input, { signal }) {
|
||||
console.log('🚀 大模型已切断,原生爬虫代码接管...')
|
||||
|
||||
// 获取当前正在被 Agent 控制的标签页 ID
|
||||
const tabId = (this.pageController as any).currentTabId
|
||||
if (!tabId) {
|
||||
return '❌ 错误:没有找到激活的标签页,无法执行抓取。'
|
||||
}
|
||||
|
||||
try {
|
||||
// 在 Chrome 插件中,不能使用 executeJavascript,必须使用 Chrome 原生的 scripting API 注入代码
|
||||
const results = await chrome.scripting.executeScript({
|
||||
target: { tabId: tabId },
|
||||
func: async () => {
|
||||
// --- 以下代码将原封不动地在金蝶/AntDesign的网页环境内部执行 ---
|
||||
let allData: any[] = []
|
||||
let pageCount = 1
|
||||
while (true) {
|
||||
const rows = document.querySelectorAll('tr.ant-table-row')
|
||||
const pageData = Array.from(rows).map((row) => {
|
||||
const cells = row.querySelectorAll('td')
|
||||
return {
|
||||
ruleName: cells[1] ? (cells[1] as HTMLElement).innerText.trim() : '',
|
||||
desc: cells[2] ? (cells[2] as HTMLElement).innerText.trim() : '',
|
||||
}
|
||||
})
|
||||
|
||||
allData = allData.concat(pageData)
|
||||
|
||||
const nextLi = document.querySelector('li.ant-pagination-next')
|
||||
if (!nextLi || nextLi.classList.contains('ant-pagination-disabled')) {
|
||||
return allData // 把数据回传
|
||||
}
|
||||
|
||||
const nextBtn = nextLi.querySelector('button')
|
||||
if (nextBtn) nextBtn.click()
|
||||
else (nextLi as HTMLElement).click()
|
||||
|
||||
pageCount++
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
}
|
||||
// --- 网页内部代码结束 ---
|
||||
},
|
||||
})
|
||||
|
||||
// 提取网页返回的数据
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const extractedOrders = (results && results[0] ? results[0].result : []) as any
|
||||
console.log('✅ 成功拿到了网页返回的全部数据:', extractedOrders)
|
||||
|
||||
return `成功抓取了所有 ${extractedOrders?.length || 0} 条订单数据,并已打印到后台控制台日志中。`
|
||||
} catch (error) {
|
||||
console.error('执行脚本失败:', error)
|
||||
return `❌ 抓取失败: ${error}`
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -218,9 +218,23 @@ export function useHubWs(
|
||||
Number(wsPort),
|
||||
{
|
||||
onExecute: async (task, incomingConfig) => {
|
||||
const { execute, configure, config } = latestRef.current
|
||||
let { execute } = latestRef.current
|
||||
const { configure, config } = latestRef.current
|
||||
if (incomingConfig) {
|
||||
await configure({ ...config, ...incomingConfig } as ExtConfig)
|
||||
let changed = false
|
||||
for (const key of Object.keys(incomingConfig)) {
|
||||
if (incomingConfig[key] !== (config as any)?.[key]) {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
await configure({ ...config, ...incomingConfig } as ExtConfig)
|
||||
// Wait a bit for React to re-render and instantiate the new MultiPageAgent
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
// Re-fetch the newly generated execute function after re-render
|
||||
execute = latestRef.current.execute
|
||||
}
|
||||
}
|
||||
const result = await execute(task)
|
||||
return { success: result.success, data: result.data }
|
||||
|
||||
@@ -45,7 +45,7 @@ export default defineConfig({
|
||||
name: '__MSG_extName__',
|
||||
description: '__MSG_extDescription__',
|
||||
homepage_url: 'https://alibaba.github.io/page-agent/',
|
||||
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage'],
|
||||
permissions: ['tabs', 'tabGroups', 'sidePanel', 'storage', 'scripting'],
|
||||
host_permissions: ['<all_urls>'],
|
||||
icons: {
|
||||
64: 'assets/page-agent-64.png',
|
||||
|
||||
@@ -48,8 +48,5 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ai-motion": "^0.4.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"happy-dom": "^20.10.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,21 +10,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-separator": "^1.1.9",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.3.0",
|
||||
"@radix-ui/react-tooltip": "^1.2.9",
|
||||
"@radix-ui/react-separator": "^1.1.10",
|
||||
"@radix-ui/react-slot": "^1.3.0",
|
||||
"@radix-ui/react-switch": "^1.3.1",
|
||||
"@radix-ui/react-tooltip": "^1.2.10",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.16.0",
|
||||
"lucide-react": "^1.21.0",
|
||||
"motion": "^12.40.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"rough-notation": "^0.5.1",
|
||||
"simple-icons": "^16.23.0",
|
||||
"simple-icons": "^16.24.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
|
||||
Reference in New Issue
Block a user