Compare commits

...

2 Commits

Author SHA1 Message Date
Zou-Seay
9f22a730df fix(extension): resolve race condition in hub config and fix tab initialization crash
Some checks failed
CI / test (24) (push) Has been cancelled
Deploy Demo / deploy (push) Has been cancelled
add: erp scrape python demo
2026-06-25 15:31:41 +08:00
Zou-Seay
e26589bc54 feat: 增加 ERP 订单自动化抓取自定义工具及相关开发文档
Some checks failed
CI / test (24) (push) Has been cancelled
Deploy Demo / deploy (push) Has been cancelled
2026-06-24 16:02:08 +08:00
8 changed files with 260 additions and 447 deletions

58
demo_erp_scrape.py Normal file
View 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
View 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/` 界面中覆盖加载该文件夹,并点击“刷新”图标即可生效。大模型立刻就能使用你的新工具了。

442
package-lock.json generated
View File

@@ -807,448 +807,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",

View File

@@ -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()

View File

@@ -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',

View 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}`
}
},
})

View File

@@ -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 }

View File

@@ -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',