foliko 1.1.2 → 1.1.4

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.
Files changed (205) hide show
  1. package/.agent/agents/code-assistant.json +14 -0
  2. package/.agent/agents/email-assistant.json +14 -0
  3. package/.agent/agents/file-assistant.json +15 -0
  4. package/.agent/agents/system-assistant.json +15 -0
  5. package/.agent/agents/web-assistant.json +12 -0
  6. package/.agent/data/ambient/goals.json +50 -0
  7. package/.agent/data/ambient/memories.json +7 -0
  8. package/.agent/data/default.json +3 -412
  9. package/.agent/data/plugins-state.json +174 -173
  10. package/.agent/data/scheduler/tasks.json +1 -0
  11. package/.agent/memory/core.md +1 -0
  12. package/.agent/memory/project/mnn93ogy-ypjn27.md +9 -0
  13. package/.agent/memory/project/mnn98fqy-5nhc1u.md +25 -0
  14. package/.agent/memory/reference/mnq3oenw-46haj6.md +63 -0
  15. package/.agent/memory/reference/mnq5qxm2-mjoooh.md +116 -0
  16. package/.agent/memory/user/mnm67t9m-x8rekk.md +9 -0
  17. package/.agent/memory/user/mnn5mmqh-w6aktx.md +11 -0
  18. package/.agent/memory/user/mnnbfhhn-dk1bd1.md +22 -0
  19. package/.agent/package.json +8 -0
  20. package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
  21. package/.agent/plugins/daytona/README.md +89 -0
  22. package/.agent/plugins/daytona/index.js +377 -0
  23. package/.agent/plugins/daytona/package.json +12 -0
  24. package/.agent/plugins/marknative/README.md +134 -0
  25. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  26. package/.agent/plugins/marknative/index.js +256 -0
  27. package/.agent/plugins/marknative/package.json +12 -0
  28. package/.agent/plugins/marknative/update-readme.js +134 -0
  29. package/.agent/plugins/poster-plugin/emojis/rocket.png +1 -0
  30. package/.agent/plugins/poster-plugin/fonts/SegoeUI Emoji.ttf +0 -0
  31. package/.agent/plugins/poster-plugin/src/elements/text.js +3 -1
  32. package/.agent/plugins/poster-plugin/src/fonts.js +10 -0
  33. package/.agent/plugins/poster-plugin/yarn.lock +1007 -0
  34. package/.agent/plugins/system-info/index.js +387 -0
  35. package/.agent/plugins/system-info/package.json +4 -0
  36. package/.agent/plugins/system-info/test.js +40 -0
  37. package/.agent/plugins.json +11 -5
  38. package/.agent/python-scripts/test_sample.py +24 -0
  39. package/.agent/sessions/cli_default.json +1869 -691
  40. package/.agent/skills/agent-browser/SKILL.md +311 -0
  41. package/.agent/skills/agent-browser/TEST_PLAN.md +200 -0
  42. package/.agent/skills/sysinfo/SKILL.md +38 -0
  43. package/.agent/skills/sysinfo/system-info.sh +130 -0
  44. package/.agent/skills/workflow/SKILL.md +324 -0
  45. package/.agent/weixin.json +6 -0
  46. package/.agent/workflows/email-digest.json +50 -0
  47. package/.agent/workflows/file-backup.json +21 -0
  48. package/.agent/workflows/get-ip-notify.json +32 -0
  49. package/.agent/workflows/news-aggregator.json +93 -0
  50. package/.agent/workflows/news-dashboard-v2.json +94 -0
  51. package/.agent/workflows/notification-batch.json +32 -0
  52. package/.claude/settings.local.json +1 -20
  53. package/.env.example +56 -56
  54. package/README.md +441 -441
  55. package/cli/src/commands/chat.js +22 -13
  56. package/cli/src/ui/chat-ui.js +50 -37
  57. package/output/emoji-segoe-test-v2.png +0 -0
  58. package/output/emoji-segoe-test.png +0 -0
  59. package/output/emoji-test.png +0 -0
  60. package/output/emoji-windows-test.png +0 -0
  61. package/output/foliko-emoji-poster.png +0 -0
  62. package/output/foliko-muji-poster-final.png +0 -0
  63. package/output/foliko-muji-poster-v2.png +0 -0
  64. package/output/foliko-muji-poster.png +0 -0
  65. package/output/foliko-share.png +0 -0
  66. package/output/progress-circle-test.png +0 -0
  67. package/output/vb-agent-poster.png +0 -0
  68. package/package.json +1 -2
  69. package/plugins/default-plugins.js +4 -3
  70. package/plugins/extension-executor-plugin.js +12 -91
  71. package/plugins/file-system-plugin.js +19 -4
  72. package/plugins/memory-plugin.js +33 -4
  73. package/plugins/subagent-plugin.js +14 -37
  74. package/plugins/weixin-plugin.js +40 -168
  75. package/skills/find-skills/AGENTS.md +162 -162
  76. package/skills/find-skills/SKILL.md +133 -133
  77. package/skills/poster-guide/SKILL.md +669 -1426
  78. package/src/core/agent-chat.js +439 -269
  79. package/src/core/agent.js +3 -6
  80. package/.agent/.shared/ui-ux-pro-max/data/charts.csv +0 -26
  81. package/.agent/.shared/ui-ux-pro-max/data/colors.csv +0 -97
  82. package/.agent/.shared/ui-ux-pro-max/data/icons.csv +0 -101
  83. package/.agent/.shared/ui-ux-pro-max/data/landing.csv +0 -31
  84. package/.agent/.shared/ui-ux-pro-max/data/products.csv +0 -97
  85. package/.agent/.shared/ui-ux-pro-max/data/prompts.csv +0 -24
  86. package/.agent/.shared/ui-ux-pro-max/data/react-performance.csv +0 -45
  87. package/.agent/.shared/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  88. package/.agent/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  89. package/.agent/.shared/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
  90. package/.agent/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  91. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -51
  92. package/.agent/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  93. package/.agent/.shared/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  94. package/.agent/.shared/ui-ux-pro-max/data/stacks/react.csv +0 -54
  95. package/.agent/.shared/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
  96. package/.agent/.shared/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  97. package/.agent/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  98. package/.agent/.shared/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  99. package/.agent/.shared/ui-ux-pro-max/data/styles.csv +0 -59
  100. package/.agent/.shared/ui-ux-pro-max/data/typography.csv +0 -58
  101. package/.agent/.shared/ui-ux-pro-max/data/ui-reasoning.csv +0 -101
  102. package/.agent/.shared/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  103. package/.agent/.shared/ui-ux-pro-max/data/web-interface.csv +0 -31
  104. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/core.cpython-313.pyc +0 -0
  105. package/.agent/.shared/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-313.pyc +0 -0
  106. package/.agent/.shared/ui-ux-pro-max/scripts/core.py +0 -258
  107. package/.agent/.shared/ui-ux-pro-max/scripts/design_system.py +0 -1067
  108. package/.agent/.shared/ui-ux-pro-max/scripts/search.py +0 -106
  109. package/.agent/ARCHITECTURE.md +0 -288
  110. package/.agent/agents/ambient-agent.md +0 -57
  111. package/.agent/agents/debugger.md +0 -55
  112. package/.agent/agents/email-assistant.md +0 -49
  113. package/.agent/agents/file-manager.md +0 -42
  114. package/.agent/agents/python-developer.md +0 -60
  115. package/.agent/agents/scheduler.md +0 -59
  116. package/.agent/agents/web-developer.md +0 -45
  117. package/.agent/data/puppeteer-sessions/undefined.json +0 -6
  118. package/.agent/data/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
  119. package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
  120. package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
  121. package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
  122. package/.agent/mcp_config_updated.json +0 -12
  123. package/.agent/plugins/poster-plugin/fonts/NotoColorEmoji-Regular.ttf +0 -0
  124. package/.agent/plugins/puppeteer-plugin/README.md +0 -147
  125. package/.agent/plugins/puppeteer-plugin/index.js +0 -1418
  126. package/.agent/plugins/puppeteer-plugin/package.json +0 -9
  127. package/.agent/rules/GEMINI.md +0 -273
  128. package/.agent/rules/allow-rule.md +0 -77
  129. package/.agent/rules/log-rule.md +0 -83
  130. package/.agent/rules/security-rule.md +0 -93
  131. package/.agent/scripts/auto_preview.py +0 -148
  132. package/.agent/scripts/checklist.py +0 -217
  133. package/.agent/scripts/session_manager.py +0 -120
  134. package/.agent/scripts/verify_all.py +0 -327
  135. package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +0 -11097
  136. package/.agent/skills/api-patterns/SKILL.md +0 -81
  137. package/.agent/skills/api-patterns/api-style.md +0 -42
  138. package/.agent/skills/api-patterns/auth.md +0 -24
  139. package/.agent/skills/api-patterns/documentation.md +0 -26
  140. package/.agent/skills/api-patterns/graphql.md +0 -41
  141. package/.agent/skills/api-patterns/rate-limiting.md +0 -31
  142. package/.agent/skills/api-patterns/response.md +0 -37
  143. package/.agent/skills/api-patterns/rest.md +0 -40
  144. package/.agent/skills/api-patterns/scripts/api_validator.py +0 -211
  145. package/.agent/skills/api-patterns/security-testing.md +0 -122
  146. package/.agent/skills/api-patterns/trpc.md +0 -41
  147. package/.agent/skills/api-patterns/versioning.md +0 -22
  148. package/.agent/skills/app-builder/SKILL.md +0 -75
  149. package/.agent/skills/app-builder/agent-coordination.md +0 -71
  150. package/.agent/skills/app-builder/feature-building.md +0 -53
  151. package/.agent/skills/app-builder/project-detection.md +0 -34
  152. package/.agent/skills/app-builder/scaffolding.md +0 -118
  153. package/.agent/skills/app-builder/tech-stack.md +0 -40
  154. package/.agent/skills/app-builder/templates/SKILL.md +0 -39
  155. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +0 -76
  156. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +0 -92
  157. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +0 -88
  158. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +0 -88
  159. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +0 -83
  160. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +0 -90
  161. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +0 -90
  162. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +0 -122
  163. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +0 -122
  164. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +0 -169
  165. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +0 -134
  166. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +0 -83
  167. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +0 -119
  168. package/.agent/skills/architecture/SKILL.md +0 -55
  169. package/.agent/skills/architecture/context-discovery.md +0 -43
  170. package/.agent/skills/architecture/examples.md +0 -94
  171. package/.agent/skills/architecture/pattern-selection.md +0 -68
  172. package/.agent/skills/architecture/patterns-reference.md +0 -50
  173. package/.agent/skills/architecture/trade-off-analysis.md +0 -77
  174. package/.agent/skills/clean-code/SKILL.md +0 -201
  175. package/.agent/skills/doc.md +0 -177
  176. package/.agent/skills/frontend-design/SKILL.md +0 -418
  177. package/.agent/skills/frontend-design/animation-guide.md +0 -331
  178. package/.agent/skills/frontend-design/color-system.md +0 -311
  179. package/.agent/skills/frontend-design/decision-trees.md +0 -418
  180. package/.agent/skills/frontend-design/motion-graphics.md +0 -306
  181. package/.agent/skills/frontend-design/scripts/accessibility_checker.py +0 -183
  182. package/.agent/skills/frontend-design/scripts/ux_audit.py +0 -722
  183. package/.agent/skills/frontend-design/typography-system.md +0 -345
  184. package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
  185. package/.agent/skills/frontend-design/visual-effects.md +0 -383
  186. package/.agent/skills/i18n-localization/SKILL.md +0 -154
  187. package/.agent/skills/i18n-localization/scripts/i18n_checker.py +0 -241
  188. package/.agent/skills/mcp-builder/SKILL.md +0 -176
  189. package/.agent/skills/web-design-guidelines/SKILL.md +0 -57
  190. package/.agent/workflows/brainstorm.md +0 -113
  191. package/.agent/workflows/create.md +0 -59
  192. package/.agent/workflows/debug.md +0 -103
  193. package/.agent/workflows/deploy.md +0 -176
  194. package/.agent/workflows/enhance.md +0 -63
  195. package/.agent/workflows/orchestrate.md +0 -237
  196. package/.agent/workflows/plan.md +0 -89
  197. package/.agent/workflows/preview.md +0 -81
  198. package/.agent/workflows/simple-test.md +0 -42
  199. package/.agent/workflows/status.md +0 -86
  200. package/.agent/workflows/structured-orchestrate.md +0 -180
  201. package/.agent/workflows/test.md +0 -144
  202. package/.agent/workflows/ui-ux-pro-max.md +0 -296
  203. package/output/beef-love-poster.png +0 -0
  204. package/output/international-news-daily.png +0 -0
  205. package/poster-test-2.png +0 -0
@@ -1,1418 +0,0 @@
1
- const { z } = require('zod');
2
- const path = require('path');
3
- const fs = require('fs');
4
-
5
- // Session 存储路径
6
- const SESSION_DIR = path.join(process.cwd(), '.agent', 'data', 'puppeteer-sessions');
7
- if (!fs.existsSync(SESSION_DIR)) {
8
- fs.mkdirSync(SESSION_DIR, { recursive: true });
9
- }
10
-
11
- module.exports = function (Plugin) {
12
- return class PuppeteerPlugin extends Plugin {
13
- constructor(config = {}) {
14
- super();
15
- this.name = 'puppeteer';
16
- this.version = '1.0.0';
17
- this.description = 'Puppeteer 网页自动化操作插件 - 支持浏览器控制、页面操作、元素交互、Session 管理,你没有识别图片的能力';
18
- this.priority = 10;
19
- this.config = config;
20
-
21
- // 浏览器实例
22
- this.browser = null;
23
- this.pages = new Map();
24
- this.currentPageId = null;
25
-
26
- // Puppeteer 路径
27
- this.puppeteerPath = null;
28
- // 浏览器可执行文件路径
29
- this.executablePath = config.executablePath || null;
30
- }
31
-
32
- // 延迟加载 puppeteer(仅在需要时加载)
33
- async getPuppeteer() {
34
- if (!this.puppeteer) {
35
- // 尝试多个可能的路径
36
- const possiblePaths = [
37
- path.join(__dirname, 'node_modules', 'puppeteer'),
38
- path.join(__dirname, '..', '..', 'node_modules', 'puppeteer'),
39
- path.join(process.cwd(), 'node_modules', 'puppeteer'),
40
- 'puppeteer',
41
- path.join(__dirname, 'node_modules', 'puppeteer-core'),
42
- path.join(__dirname, '..', '..', 'node_modules', 'puppeteer-core'),
43
- path.join(process.cwd(), 'node_modules', 'puppeteer-core'),
44
- 'puppeteer-core'
45
- ];
46
-
47
- for (const p of possiblePaths) {
48
- try {
49
- this.puppeteer = require(p);
50
- this.puppeteerPath = p;
51
- break;
52
- } catch (e) {
53
- continue;
54
- }
55
- }
56
-
57
- if (!this.puppeteer) {
58
- throw new Error('Puppeteer 未安装,请先安装 puppeteer 或 puppeteer-core');
59
- }
60
- }
61
- return this.puppeteer;
62
- }
63
-
64
- // 生成页面ID
65
- generatePageId() {
66
- return `page_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
67
- }
68
-
69
- // 获取当前页面
70
- getCurrentPage() {
71
- if (!this.currentPageId || !this.pages.has(this.currentPageId)) {
72
- throw new Error('没有活动的页面,请先打开页面');
73
- }
74
- return this.pages.get(this.currentPageId);
75
- }
76
-
77
- // ============ 浏览器管理 ============
78
-
79
- tools = {
80
- // 启动浏览器
81
- browser_launch: {
82
- description: '启动 Puppeteer 浏览器实例',
83
- inputSchema: z.object({
84
- headless: z.boolean().optional().describe('是否无头模式,默认 true'),
85
- args: z.array(z.string()).optional().describe('浏览器启动参数'),
86
- executablePath: z.string().optional().describe('浏览器可执行文件路径(如使用puppeteer-core)'),
87
- userDataDir: z.string().optional().describe('用户数据目录(用于保存Session)'),
88
- viewport: z.object({
89
- width: z.number().optional().describe('视口宽度'),
90
- height: z.number().optional().describe('视口高度'),
91
- }).optional().describe('视口设置'),
92
- userAgent: z.string().optional().describe('自定义 User-Agent'),
93
- }),
94
- execute: async (args) => {
95
- const puppeteer = await this.getPuppeteer();
96
-
97
- // 如果浏览器已启动,先关闭
98
- if (this.browser) {
99
- await this.browser.close();
100
- this.browser = null;
101
- this.pages.clear();
102
- }
103
-
104
- const launchOptions = {
105
- headless: args.headless !== false,
106
- args: args.args || [
107
- '--no-sandbox',
108
- '--disable-setuid-sandbox',
109
- '--disable-dev-shm-usage',
110
- '--disable-accelerated-2d-canvas',
111
- '--disable-gpu'
112
- ]
113
- };
114
-
115
- // 支持自定义浏览器路径或使用 puppeteer-core
116
- if (args.executablePath) {
117
- launchOptions.executablePath = args.executablePath;
118
- } else if (this.executablePath) {
119
- launchOptions.executablePath = this.executablePath;
120
- }
121
-
122
- if (args.userDataDir) {
123
- launchOptions.userDataDir = args.userDataDir;
124
- }
125
-
126
- this.browser = await puppeteer.launch(launchOptions);
127
-
128
- // 创建新页面
129
- const page = await this.browser.newPage();
130
- const pageId = this.generatePageId();
131
- this.pages.set(pageId, page);
132
- this.currentPageId = pageId;
133
-
134
- // 设置视口
135
- if (args.viewport) {
136
- await page.setViewport({
137
- width: args.viewport.width || 1920,
138
- height: args.viewport.height || 1080
139
- });
140
- } else {
141
- await page.setViewport({ width: 1920, height: 1080 });
142
- }
143
-
144
- // 设置 User-Agent
145
- if (args.userAgent) {
146
- await page.setUserAgent(args.userAgent);
147
- }
148
-
149
- return {
150
- success: true,
151
- message: '浏览器已启动',
152
- pageId: pageId,
153
- headless: launchOptions.headless
154
- };
155
- }
156
- },
157
-
158
- // 关闭浏览器
159
- browser_close: {
160
- description: '关闭 Puppeteer 浏览器实例',
161
- inputSchema: z.object({}),
162
- execute: async () => {
163
- if (this.browser) {
164
- await this.browser.close();
165
- this.browser = null;
166
- this.pages.clear();
167
- this.currentPageId = null;
168
- }
169
- return { success: true, message: '浏览器已关闭' };
170
- }
171
- },
172
-
173
- // 获取浏览器状态
174
- browser_status: {
175
- description: '获取当前浏览器状态',
176
- inputSchema: z.object({}),
177
- execute: async () => {
178
- return {
179
- success: true,
180
- browserOpen: !!this.browser,
181
- pages: Array.from(this.pages.keys()),
182
- currentPageId: this.currentPageId
183
- };
184
- }
185
- },
186
-
187
- // ============ 页面操作 ============
188
-
189
- // 打开页面
190
- page_open: {
191
- description: '打开新页面或导航到URL',
192
- inputSchema: z.object({
193
- url: z.string().describe('目标URL'),
194
- pageId: z.string().optional().describe('指定页面ID,默认新建页面'),
195
- waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2', 'commit']).optional().describe('等待策略'),
196
- }),
197
- execute: async (args) => {
198
- const puppeteer = await this.getPuppeteer();
199
-
200
- if (!this.browser) {
201
- throw new Error('浏览器未启动,请先调用 browser_launch');
202
- }
203
-
204
- let page;
205
- if (args.pageId && this.pages.has(args.pageId)) {
206
- page = this.pages.get(args.pageId);
207
- } else {
208
- page = await this.browser.newPage();
209
- const newPageId = this.generatePageId();
210
- this.pages.set(newPageId, page);
211
- this.currentPageId = newPageId;
212
- }
213
-
214
- await page.goto(args.url, {
215
- waitUntil: args.waitUntil || 'networkidle2',
216
- timeout: 30000
217
- });
218
-
219
- // 获取标题
220
- const title = await page.title();
221
- const url = page.url();
222
-
223
- return {
224
- success: true,
225
- message: '页面已打开',
226
- pageId: this.currentPageId,
227
- title: title,
228
- url: url
229
- };
230
- }
231
- },
232
-
233
- // 页面导航
234
- page_navigate: {
235
- description: '页面导航操作(前进、后退、刷新)',
236
- inputSchema: z.object({
237
- action: z.enum(['back', 'forward', 'reload']).describe('导航操作'),
238
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
239
- }),
240
- execute: async (args) => {
241
- const page = args.pageId && this.pages.has(args.pageId)
242
- ? this.pages.get(args.pageId)
243
- : this.getCurrentPage();
244
-
245
- switch (args.action) {
246
- case 'back':
247
- await page.goBack({ timeout: 30000 });
248
- break;
249
- case 'forward':
250
- await page.goForward({ timeout: 30000 });
251
- break;
252
- case 'reload':
253
- await page.reload({ timeout: 30000 });
254
- break;
255
- }
256
-
257
- return {
258
- success: true,
259
- action: args.action,
260
- url: page.url(),
261
- title: await page.title()
262
- };
263
- }
264
- },
265
-
266
- // 截图
267
- page_screenshot: {
268
- description: '对页面进行截图,截图只能保存,你没有识别图片的能力',
269
- inputSchema: z.object({
270
- path: z.string().describe('保存路径,默认临时文件'),
271
- fullPage: z.boolean().optional().describe('是否截取整页'),
272
- selector: z.string().optional().describe('截取指定元素的截图'),
273
- type: z.enum(['png', 'jpeg', 'webp']).optional().describe('图片格式'),
274
- quality: z.number().optional().describe('图片质量(1-100)'),
275
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
276
- }),
277
- execute: async (args) => {
278
- const page = args.pageId && this.pages.has(args.pageId)
279
- ? this.pages.get(args.pageId)
280
- : this.getCurrentPage();
281
-
282
- const screenshotOptions = {
283
- type: args.type || 'png',
284
- fullPage: args.fullPage || false
285
- };
286
-
287
- if (args.quality && args.type !== 'png') {
288
- screenshotOptions.quality = args.quality;
289
- }
290
-
291
- let screenshot;
292
- if (args.selector) {
293
- const element = await page.$(args.selector);
294
- if (!element) {
295
- throw new Error(`未找到元素: ${args.selector}`);
296
- }
297
- screenshot = await element.screenshot(screenshotOptions);
298
- } else {
299
- screenshot = await page.screenshot(screenshotOptions);
300
- }
301
-
302
- const fs = require('fs');
303
- const dir = path.dirname(args.path);
304
- if (!fs.existsSync(dir)) {
305
- fs.mkdirSync(dir, { recursive: true });
306
- }
307
- fs.writeFileSync(args.path, screenshot);
308
- return {
309
- success: true,
310
- path: args.path,
311
- message: `截图已保存到 ${args.path}`
312
- };
313
- }
314
- },
315
-
316
- // 获取页面HTML
317
- page_html: {
318
- description: '获取页面HTML内容(注意:复杂页面会导致token超标,建议使用 page_structure 获取页面结构)',
319
- inputSchema: z.object({
320
- contentOnly: z.boolean().optional().describe('仅获取body内容'),
321
- toMarkdown: z.boolean().optional().default(true).describe('是否将HTML转换为Markdown格式,默认开启'),
322
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
323
- }),
324
- execute: async (args) => {
325
- const page = args.pageId && this.pages.has(args.pageId)
326
- ? this.pages.get(args.pageId)
327
- : this.getCurrentPage();
328
- args.toMarkdown=true
329
- let html = args.contentOnly
330
- ? await page.evaluate(() => document.body ? document.body.innerHTML : '')
331
- : await page.content();
332
-
333
- let content = html;
334
- let isMarkdown = false;
335
-
336
- // 如果需要转换为 Markdown(默认为 true)
337
- const shouldConvertToMarkdown = args.toMarkdown !== false;
338
- if (shouldConvertToMarkdown) {
339
- content = this._htmlToMarkdown(html);
340
- isMarkdown = true;
341
- }
342
-
343
- return {
344
- success: true,
345
- html: isMarkdown ? undefined : html,
346
- markdown: isMarkdown ? content : undefined,
347
- content: content,
348
- length: content.length,
349
- isMarkdown: isMarkdown,
350
- url: page.url()
351
- };
352
- }
353
- },
354
-
355
- // HTML 转 Markdown 辅助方法
356
- _htmlToMarkdown(html) {
357
- if (!html) return '';
358
-
359
- // 移除脚本和样式标签内容
360
- let markdown = html
361
- .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
362
- .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
363
- .replace(/<!--[\s\S]*?-->/g, '');
364
-
365
- // 标题转换
366
- markdown = markdown.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '# $1\n\n');
367
- markdown = markdown.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '## $1\n\n');
368
- markdown = markdown.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '### $1\n\n');
369
- markdown = markdown.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '#### $1\n\n');
370
- markdown = markdown.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, '##### $1\n\n');
371
- markdown = markdown.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, '###### $1\n\n');
372
-
373
- // 换行和段落
374
- markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
375
- markdown = markdown.replace(/<\/p>/gi, '\n\n');
376
- markdown = markdown.replace(/<\/div>/gi, '\n');
377
- markdown = markdown.replace(/<\/li>/gi, '\n');
378
-
379
- // 列表
380
- markdown = markdown.replace(/<ul[^>]*>/gi, '\n');
381
- markdown = markdown.replace(/<ol[^>]*>/gi, '\n');
382
- markdown = markdown.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n');
383
-
384
- // 链接和图片
385
- markdown = markdown.replace(/<a[^>]*href=["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
386
- markdown = markdown.replace(/<img[^>]*src=["']([^"']*)["'][^>]*alt=["']([^"']*)["'][^>]*\/?>/gi, '![$2]($1)');
387
- markdown = markdown.replace(/<img[^>]*alt=["']([^"']*)["'][^>]*src=["']([^"']*)["'][^>]*\/?>/gi, '![$1]($2)');
388
- markdown = markdown.replace(/<img[^>]*src=["']([^"']*)["'][^>]*\/?>/gi, '![]($1)');
389
-
390
- // 粗体和斜体
391
- markdown = markdown.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, '**$1**');
392
- markdown = markdown.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, '**$1**');
393
- markdown = markdown.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, '*$1*');
394
- markdown = markdown.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi, '*$1*');
395
-
396
- // 代码
397
- markdown = markdown.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
398
- markdown = markdown.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, '```\n$1\n```');
399
-
400
- // 表格 (简单支持)
401
- const tableRegex = /<table[^>]*>([\s\S]*?)<\/table>/gi;
402
- markdown = markdown.replace(tableRegex, (match, tableContent) => {
403
- let mdTable = '';
404
- const rows = tableContent.match(/<tr[^>]*>([\s\S]*?)<\/tr>/gi) || [];
405
- rows.forEach((row, idx) => {
406
- const cells = row.match(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi) || [];
407
- const mdRow = cells.map(cell => {
408
- return cell.replace(/<t[hd][^>]*>/, '').replace(/<\/t[hd]>/, '').trim();
409
- }).join(' | ');
410
- if (idx === 0) {
411
- mdTable += mdRow + '\n' + mdRow.split('|').map(() => '---').join('|') + '\n';
412
- } else {
413
- mdTable += mdRow + '\n';
414
- }
415
- });
416
- return mdTable + '\n';
417
- });
418
-
419
- // 移除所有剩余 HTML 标签
420
- markdown = markdown.replace(/<[^>]+>/g, '');
421
-
422
- // 清理实体编码
423
- markdown = markdown
424
- .replace(/&nbsp;/g, ' ')
425
- .replace(/&amp;/g, '&')
426
- .replace(/&lt;/g, '<')
427
- .replace(/&gt;/g, '>')
428
- .replace(/&quot;/g, '"')
429
- .replace(/&#39;/g, "'")
430
- .replace(/&nbsp;/g, ' ')
431
- .replace(/&#(\d+);/g, (match, dec) => String.fromCharCode(dec));
432
-
433
- // 清理多余空行
434
- markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
435
-
436
- return markdown;
437
- },
438
-
439
- // 获取页面结构(优化版 - 返回精简的结构化数据,避免token超标)
440
- page_structure: {
441
- description: '获取页面关键结构信息(推荐使用),返回可交互元素的精简结构,避免获取完整HTML导致的token超标问题',
442
- inputSchema: z.object({
443
- maxElements: z.number().optional().describe('最大返回元素数量,默认50'),
444
- includeHidden: z.boolean().optional().describe('是否包含隐藏元素,默认false'),
445
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
446
- }),
447
- execute: async (args) => {
448
- const page = args.pageId && this.pages.has(args.pageId)
449
- ? this.pages.get(args.pageId)
450
- : this.getCurrentPage();
451
-
452
- const maxElements = args.maxElements || 50;
453
- const includeHidden = args.includeHidden || false;
454
-
455
- // 在页面中执行结构提取逻辑
456
- const structure = await page.evaluate(({ maxEls, hidden }) => {
457
- // 可交互元素标签
458
- const interactiveTags = ['a', 'button', 'input', 'select', 'textarea', 'label'];
459
- const clickableTags = ['a', 'button'];
460
-
461
- // 生成唯一选择器
462
- function generateSelector(el) {
463
- if (el.id) return `#${CSS.escape(el.id)}`;
464
-
465
- let selector = el.tagName.toLowerCase();
466
-
467
- // 优先使用class
468
- if (el.className && typeof el.className === 'string' && el.className.trim()) {
469
- const classes = el.className.trim().split(/\s+/).slice(0, 2);
470
- selector += '.' + classes.map(c => CSS.escape(c)).join('.');
471
- }
472
-
473
- // 添加有意义的属性
474
- if (el.name) selector += `[name="${CSS.escape(el.name)}"]`;
475
- if (el.type) selector += `[type="${CSS.escape(el.type)}"]`;
476
- if (el.placeholder) selector += `[placeholder*="${el.placeholder.substring(0, 20)}"]`;
477
-
478
- // 添加data属性
479
- const dataAttr = [...el.attributes].find(a => a.name.startsWith('data-'));
480
- if (dataAttr) selector += `[${dataAttr.name}="${CSS.escape(dataAttr.value.substring(0, 30))}"]`;
481
-
482
- return selector;
483
- }
484
-
485
- // 清理文本,移除多余空白
486
- function cleanText(text) {
487
- if (!text) return '';
488
- return text.replace(/\s+/g, ' ').trim().substring(0, 100);
489
- }
490
-
491
- // 收集可交互元素
492
- const elements = [];
493
- const seen = new Set();
494
-
495
- // 优先收集有明确选择器的元素
496
- const allElements = document.querySelectorAll(
497
- 'a, button, [onclick], [role="button"], input, select, textarea, [tabindex]'
498
- );
499
-
500
- for (const el of allElements) {
501
- if (elements.length >= maxEls) break;
502
-
503
- // 跳过隐藏元素
504
- if (!hidden) {
505
- const style = window.getComputedStyle(el);
506
- if (style.display === 'none' || style.visibility === 'hidden') continue;
507
- }
508
-
509
- // 生成选择器
510
- const selector = generateSelector(el);
511
-
512
- // 去重
513
- if (seen.has(selector)) continue;
514
- seen.add(selector);
515
-
516
- // 获取元素信息
517
- const rect = el.getBoundingClientRect();
518
- const isVisible = rect.width > 0 && rect.height > 0;
519
-
520
- if (!hidden && !isVisible) continue;
521
-
522
- // 获取文本内容
523
- let text = '';
524
- if (el.tagName === 'INPUT') {
525
- text = el.placeholder || el.name || '';
526
- } else if (clickableTags.includes(el.tagName)) {
527
- text = cleanText(el.textContent) || el.alt || '';
528
- } else {
529
- text = cleanText(el.textContent);
530
- }
531
-
532
- // 获取href/src
533
- let href = '';
534
- if (el.tagName === 'A') href = el.href || '';
535
- else if (el.tagName === 'IMG') href = el.src || '';
536
-
537
- // 获取表单信息
538
- let formInfo = {};
539
- if (el.tagName === 'INPUT' || el.tagName === 'SELECT' || el.tagName === 'TEXTAREA') {
540
- formInfo = {
541
- type: el.type || el.tagName,
542
- name: el.name || '',
543
- value: el.value ? el.value.substring(0, 50) : '',
544
- placeholder: el.placeholder || '',
545
- required: el.required,
546
- disabled: el.disabled
547
- };
548
- }
549
-
550
- // 获取aria属性
551
- const aria = {
552
- label: el.getAttribute('aria-label'),
553
- role: el.getAttribute('role'),
554
- describedby: el.getAttribute('aria-describedby')
555
- };
556
-
557
- // 获取data属性
558
- const dataAttrs = {};
559
- [...el.attributes].forEach(attr => {
560
- if (attr.name.startsWith('data-')) {
561
- dataAttrs[attr.name] = attr.value.substring(0, 50);
562
- }
563
- });
564
-
565
- elements.push({
566
- selector: selector,
567
- tag: el.tagName.toLowerCase(),
568
- id: el.id || null,
569
- classes: el.className && typeof el.className === 'string'
570
- ? el.className.trim().split(/\s+/).filter(c => c)
571
- : [],
572
- text: text,
573
- href: href,
574
- position: {
575
- x: Math.round(rect.x),
576
- y: Math.round(rect.y),
577
- width: Math.round(rect.width),
578
- height: Math.round(rect.height)
579
- },
580
- visible: isVisible,
581
- ...formInfo,
582
- aria,
583
- dataAttrs
584
- });
585
- }
586
-
587
- // 收集表单区域结构
588
- const forms = [];
589
- document.querySelectorAll('form').forEach(form => {
590
- const formData = {
591
- selector: generateSelector(form),
592
- id: form.id || null,
593
- action: form.action || '',
594
- method: form.method || 'get',
595
- elements: []
596
- };
597
-
598
- form.querySelectorAll('input, select, textarea').forEach(input => {
599
- formData.elements.push({
600
- selector: generateSelector(input),
601
- tag: input.tagName.toLowerCase(),
602
- type: input.type || 'text',
603
- name: input.name || '',
604
- placeholder: input.placeholder || '',
605
- label: input.labels?.[0]?.textContent?.trim() ||
606
- input.closest('label')?.textContent?.trim() || ''
607
- });
608
- });
609
-
610
- if (formData.elements.length > 0) {
611
- forms.push(formData);
612
- }
613
- });
614
-
615
- // 获取主要区域结构
616
- const regions = [];
617
- const mainSelectors = ['header', 'nav', 'main', 'aside', 'footer', '[role="main"]', '[role="navigation"]'];
618
- mainSelectors.forEach(sel => {
619
- document.querySelectorAll(sel).forEach(el => {
620
- if (regions.length >= 10) return;
621
-
622
- const id = el.id || '';
623
- const classes = el.className && typeof el.className === 'string'
624
- ? el.className.split(/\s+/).slice(0, 2).join(' ')
625
- : '';
626
-
627
- regions.push({
628
- selector: generateSelector(el),
629
- tag: el.tagName.toLowerCase(),
630
- role: el.getAttribute('role') || sel.replace(/[\[\]"]/g, ''),
631
- id: id || null,
632
- classes: classes,
633
- heading: el.querySelector('h1, h2, h3')?.textContent?.trim().substring(0, 50) || null,
634
- childCount: el.children.length
635
- });
636
- });
637
- });
638
-
639
- // 获取标题层级
640
- const headings = [];
641
- document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((h, i) => {
642
- if (headings.length >= 15) return;
643
- headings.push({
644
- level: parseInt(h.tagName[1]),
645
- text: h.textContent.trim().substring(0, 80),
646
- id: h.id || null
647
- });
648
- });
649
-
650
- return {
651
- elements,
652
- forms,
653
- regions,
654
- headings,
655
- totalElements: document.querySelectorAll(
656
- 'a, button, input, select, textarea'
657
- ).length
658
- };
659
- }, { maxEls: maxElements, hidden: includeHidden });
660
-
661
- return {
662
- success: true,
663
- message: `页面结构已提取,共 ${structure.totalElements} 个可交互元素`,
664
- url: page.url(),
665
- title: await page.title(),
666
- summary: {
667
- totalInteractive: structure.totalElements,
668
- returnedElements: structure.elements.length,
669
- forms: structure.forms.length,
670
- regions: structure.regions.length,
671
- headings: structure.headings.length
672
- },
673
- elements: structure.elements,
674
- forms: structure.forms,
675
- regions: structure.regions,
676
- headings: structure.headings,
677
- usageTip: '使用 element_find 或 element_click 工具配合返回的 selector 进行元素操作'
678
- };
679
- }
680
- },
681
-
682
- // 关闭页面
683
- page_close: {
684
- description: '关闭指定页面',
685
- inputSchema: z.object({
686
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
687
- }),
688
- execute: async (args) => {
689
- const pageId = args.pageId || this.currentPageId;
690
- if (!this.pages.has(pageId)) {
691
- throw new Error(`页面不存在: ${pageId}`);
692
- }
693
-
694
- const page = this.pages.get(pageId);
695
- await page.close();
696
- this.pages.delete(pageId);
697
-
698
- // 如果关闭的是当前页面,切换到其他页面
699
- if (this.currentPageId === pageId) {
700
- this.currentPageId = this.pages.keys().next().value || null;
701
- }
702
-
703
- return {
704
- success: true,
705
- message: '页面已关闭',
706
- remainingPages: this.pages.size
707
- };
708
- }
709
- },
710
-
711
- // ============ 元素操作 ============
712
-
713
- // 查找元素
714
- element_find: {
715
- description: '查找页面元素',
716
- inputSchema: z.object({
717
- selector: z.string().describe('CSS选择器或XPath(以//开头)'),
718
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
719
- }),
720
- execute: async (args) => {
721
- const page = args.pageId && this.pages.has(args.pageId)
722
- ? this.pages.get(args.pageId)
723
- : this.getCurrentPage();
724
-
725
- const selector = args.selector;
726
- let element;
727
- let isXPath = selector.startsWith('//') || selector.startsWith('/');
728
-
729
- if (isXPath) {
730
- const elements = await page.$x(selector);
731
- element = elements[0] || null;
732
- } else {
733
- element = await page.$(selector);
734
- }
735
-
736
- if (!element) {
737
- return {
738
- success: true,
739
- found: false,
740
- message: `未找到元素: ${selector}`
741
- };
742
- }
743
-
744
- // 获取元素信息
745
- const box = await element.boundingBox();
746
- const info = await page.evaluate((el) => {
747
- return {
748
- tagName: el.tagName,
749
- text: el.innerText?.substring(0, 200) || '',
750
- href: el.href || el.src || '',
751
- value: el.value || '',
752
- checked: el.checked,
753
- disabled: el.disabled,
754
- visible: el.offsetParent !== null
755
- };
756
- }, element);
757
-
758
- return {
759
- success: true,
760
- found: true,
761
- selector: selector,
762
- boundingBox: box,
763
- ...info
764
- };
765
- }
766
- },
767
-
768
- // 查找所有匹配元素
769
- element_find_all: {
770
- description: '查找所有匹配的元素',
771
- inputSchema: z.object({
772
- selector: z.string().describe('CSS选择器或XPath'),
773
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
774
- }),
775
- execute: async (args) => {
776
- const page = args.pageId && this.pages.has(args.pageId)
777
- ? this.pages.get(args.pageId)
778
- : this.getCurrentPage();
779
-
780
- let elements;
781
- let isXPath = args.selector.startsWith('//') || args.selector.startsWith('/');
782
-
783
- if (isXPath) {
784
- elements = await page.$x(args.selector);
785
- } else {
786
- elements = await page.$$(args.selector);
787
- }
788
-
789
- const results = [];
790
- for (let i = 0; i < Math.min(elements.length, 50); i++) {
791
- const info = await page.evaluate((el) => ({
792
- text: el.innerText?.substring(0, 100) || '',
793
- href: el.href || '',
794
- alt: el.alt || '',
795
- title: el.title || ''
796
- }), elements[i]);
797
- results.push({ index: i, ...info });
798
- }
799
-
800
- return {
801
- success: true,
802
- count: elements.length,
803
- displayed: results.length,
804
- elements: results
805
- };
806
- }
807
- },
808
-
809
- // 点击元素
810
- element_click: {
811
- description: '点击页面元素',
812
- inputSchema: z.object({
813
- selector: z.string().describe('CSS选择器或XPath'),
814
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
815
- button: z.enum(['left', 'right', 'middle']).optional().describe('鼠标按钮'),
816
- clickCount: z.number().optional().describe('点击次数'),
817
- delay: z.number().optional().describe('点击后延迟(ms)'),
818
- force: z.boolean().optional().describe('强制点击(可点击隐藏元素)'),
819
- timeout: z.number().optional().describe('等待元素超时(ms),默认10000'),
820
- scrollIntoView: z.boolean().optional().describe('是否滚动到视口,默认true'),
821
- }),
822
- execute: async (args) => {
823
- const page = args.pageId && this.pages.has(args.pageId)
824
- ? this.pages.get(args.pageId)
825
- : this.getCurrentPage();
826
-
827
- const timeout = args.timeout || 10000;
828
- const selector = args.selector;
829
-
830
- // 改进 XPath 检测:支持 // 和 / 开头
831
- const isXPath = selector.startsWith('//') || selector.startsWith('/');
832
-
833
- try {
834
- let element;
835
-
836
- if (isXPath) {
837
- // XPath 模式:先等待元素,然后使用 page.evaluate 点击
838
- await page.waitForXPath(selector, { timeout, visible: !args.force });
839
- const elements = await page.$x(selector);
840
- element = elements[0];
841
-
842
- if (!element) {
843
- throw new Error(`未找到元素: ${selector}`);
844
- }
845
-
846
- // 使用 page.click 支持的选项
847
- if (args.scrollIntoView !== false) {
848
- await element.evaluate(el => el.scrollIntoViewIfNeeded());
849
- }
850
-
851
- // 通过 evaluate 模拟点击,避免 visibility 检查问题
852
- const clickOptions = {
853
- button: args.button || 'left',
854
- clickCount: args.clickCount || 1
855
- };
856
- await page.evaluate(({ el, options }) => {
857
- const rect = el.getBoundingClientRect();
858
- const event = new MouseEvent('click', {
859
- view: window,
860
- bubbles: true,
861
- cancelable: true,
862
- button: options.button === 'right' ? 2 : options.button === 'middle' ? 1 : 0,
863
- buttons: 1,
864
- clientX: rect.left + rect.width / 2,
865
- clientY: rect.top + rect.height / 2,
866
- detail: options.clickCount || 1
867
- });
868
- el.dispatchEvent(event);
869
- }, { el: element, options: clickOptions });
870
- } else {
871
- // CSS 选择器模式:使用 page.click,它会自动处理元素可见性和滚动
872
- const clickOptions = {
873
- button: args.button || 'left',
874
- clickCount: args.clickCount || 1,
875
- delay: 0,
876
- timeout: timeout,
877
- force: args.force || false
878
- };
879
-
880
- // 如果不是强制点击,设置 hidden 为 false
881
- if (!args.force) {
882
- clickOptions.visible = true;
883
- }
884
-
885
- await page.click(selector, clickOptions);
886
- }
887
-
888
- if (args.delay) {
889
- await new Promise(r => setTimeout(r, args.delay));
890
- }
891
-
892
- return {
893
- success: true,
894
- message: `已点击: ${selector}`,
895
- url: page.url()
896
- };
897
- } catch (error) {
898
- // 提供更详细的错误信息
899
- if (error.message.includes('not visible') || error.message.includes('not displayed')) {
900
- throw new Error(`元素不可见或被遮挡: ${selector}。可尝试设置 force: true 强制点击,或检查元素是否正确渲染。`);
901
- }
902
- if (error.message.includes('not found') || error.message.includes('not exist')) {
903
- throw new Error(`未找到元素: ${selector}。请确认选择器正确,元素已加载。`);
904
- }
905
- if (error.message.includes('timeout')) {
906
- throw new Error(`等待元素超时: ${selector} (${timeout}ms)。请检查元素是否存在或增加 timeout 值。`);
907
- }
908
- throw error;
909
- }
910
- }
911
- },
912
-
913
- // 输入文本
914
- element_type: {
915
- description: '向输入框输入文本',
916
- inputSchema: z.object({
917
- selector: z.string().describe('CSS选择器或XPath'),
918
- text: z.string().describe('输入的文本'),
919
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
920
- delay: z.number().optional().describe('每个字符延迟(ms)'),
921
- clear: z.boolean().optional().describe('输入前清空内容'),
922
- }),
923
- execute: async (args) => {
924
- const page = args.pageId && this.pages.has(args.pageId)
925
- ? this.pages.get(args.pageId)
926
- : this.getCurrentPage();
927
-
928
- let element;
929
- let isXPath = args.selector.startsWith('//') || args.selector.startsWith('/');
930
-
931
- if (isXPath) {
932
- const elements = await page.$x(args.selector);
933
- element = elements[0];
934
- } else {
935
- element = await page.$(args.selector);
936
- }
937
-
938
- if (!element) {
939
- throw new Error(`未找到元素: ${args.selector}`);
940
- }
941
-
942
- if (args.clear) {
943
- await element.click({ clickCount: 3 });
944
- await page.keyboard.press('ControlOrMeta+A');
945
- await page.keyboard.press('Backspace');
946
- }
947
-
948
- await element.type(args.text, { delay: args.delay || 0 });
949
-
950
- return {
951
- success: true,
952
- message: `已输入文本到: ${args.selector}`,
953
- textLength: args.text.length
954
- };
955
- }
956
- },
957
-
958
- // 按键操作
959
- element_press: {
960
- description: '模拟键盘按键',
961
- inputSchema: z.object({
962
- key: z.string().describe('按键名称,如 Enter, Escape, Backspace'),
963
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
964
- modifiers: z.array(z.enum(['Control', 'Shift', 'Alt', 'Meta'])).optional().describe('组合键修饰符'),
965
- }),
966
- execute: async (args) => {
967
- const page = args.pageId && this.pages.has(args.pageId)
968
- ? this.pages.get(args.pageId)
969
- : this.getCurrentPage();
970
-
971
- const modifiers = args.modifiers || [];
972
-
973
- if (modifiers.length > 0) {
974
- await page.keyboard.down(modifiers[0]);
975
- await page.keyboard.press(args.key);
976
- await page.keyboard.up(modifiers[0]);
977
- } else {
978
- await page.keyboard.press(args.key);
979
- }
980
-
981
- return {
982
- success: true,
983
- message: `已按下: ${[...modifiers, args.key].join('+')}`
984
- };
985
- }
986
- },
987
-
988
- // 悬停
989
- element_hover: {
990
- description: '鼠标悬停到元素',
991
- inputSchema: z.object({
992
- selector: z.string().describe('CSS选择器或XPath'),
993
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
994
- }),
995
- execute: async (args) => {
996
- const page = args.pageId && this.pages.has(args.pageId)
997
- ? this.pages.get(args.pageId)
998
- : this.getCurrentPage();
999
-
1000
- let element;
1001
- let isXPath = args.selector.startsWith('//') || args.selector.startsWith('/');
1002
-
1003
- if (isXPath) {
1004
- const elements = await page.$x(args.selector);
1005
- element = elements[0];
1006
- } else {
1007
- element = await page.$(args.selector);
1008
- }
1009
-
1010
- if (!element) {
1011
- throw new Error(`未找到元素: ${args.selector}`);
1012
- }
1013
-
1014
- await element.hover();
1015
-
1016
- return {
1017
- success: true,
1018
- message: `已悬停: ${args.selector}`
1019
- };
1020
- }
1021
- },
1022
-
1023
- // 等待元素
1024
- element_wait: {
1025
- description: '等待元素出现或消失',
1026
- inputSchema: z.object({
1027
- selector: z.string().describe('CSS选择器或XPath'),
1028
- state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional().describe('等待状态'),
1029
- timeout: z.number().optional().describe('超时时间(ms)'),
1030
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
1031
- }),
1032
- execute: async (args) => {
1033
- const page = args.pageId && this.pages.has(args.pageId)
1034
- ? this.pages.get(args.pageId)
1035
- : this.getCurrentPage();
1036
-
1037
- const options = {
1038
- timeout: args.timeout || 30000,
1039
- visible: args.state === 'visible',
1040
- hidden: args.state === 'hidden'
1041
- };
1042
-
1043
- let selector = args.selector;
1044
- if (!selector.startsWith('//') && !selector.startsWith('/') && !selector.startsWith('#') &&
1045
- !selector.startsWith('.') && !selector.startsWith('[')) {
1046
- selector = args.selector;
1047
- }
1048
-
1049
- await page.waitForSelector(selector, options);
1050
-
1051
- return {
1052
- success: true,
1053
- message: `元素已${args.state || '可见'}: ${args.selector}`
1054
- };
1055
- }
1056
- },
1057
-
1058
- // ============ JavaScript ============
1059
-
1060
- // 执行JavaScript
1061
- js_execute: {
1062
- description: '在页面中执行JavaScript代码',
1063
- inputSchema: z.object({
1064
- code: z.string().describe('JavaScript代码(可使用 async/await)'),
1065
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
1066
- }),
1067
- execute: async (args) => {
1068
- const page = args.pageId && this.pages.has(args.pageId)
1069
- ? this.pages.get(args.pageId)
1070
- : this.getCurrentPage();
1071
-
1072
- try {
1073
- const result = await page.evaluate(args.code);
1074
- return {
1075
- success: true,
1076
- result: result,
1077
- type: typeof result
1078
- };
1079
- } catch (error) {
1080
- return {
1081
- success: false,
1082
- error: error.message
1083
- };
1084
- }
1085
- }
1086
- },
1087
-
1088
- // ============ Cookie管理 ============
1089
-
1090
- // 获取Cookie
1091
- cookie_get: {
1092
- description: '获取页面Cookie',
1093
- inputSchema: z.object({
1094
- name: z.string().optional().describe('指定Cookie名称,不填则获取所有'),
1095
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
1096
- }),
1097
- execute: async (args) => {
1098
- const page = args.pageId && this.pages.has(args.pageId)
1099
- ? this.pages.get(args.pageId)
1100
- : this.getCurrentPage();
1101
-
1102
- const cookies = await page.cookies();
1103
-
1104
- if (args.name) {
1105
- const cookie = cookies.find(c => c.name === args.name);
1106
- return {
1107
- success: true,
1108
- cookie: cookie || null,
1109
- message: cookie ? `找到Cookie: ${args.name}` : `未找到Cookie: ${args.name}`
1110
- };
1111
- }
1112
-
1113
- return {
1114
- success: true,
1115
- cookies: cookies,
1116
- count: cookies.length
1117
- };
1118
- }
1119
- },
1120
-
1121
- // 设置Cookie
1122
- cookie_set: {
1123
- description: '设置Cookie',
1124
- inputSchema: z.object({
1125
- name: z.string().describe('Cookie名称'),
1126
- value: z.string().describe('Cookie值'),
1127
- domain: z.string().optional().describe('域名'),
1128
- path: z.string().optional().describe('路径'),
1129
- expires: z.number().optional().describe('过期时间戳'),
1130
- httpOnly: z.boolean().optional().describe('是否HttpOnly'),
1131
- secure: z.boolean().optional().describe('是否仅HTTPS'),
1132
- sameSite: z.enum(['Strict', 'Lax', 'None']).optional().describe('SameSite策略'),
1133
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
1134
- }),
1135
- execute: async (args) => {
1136
- const page = args.pageId && this.pages.has(args.pageId)
1137
- ? this.pages.get(args.pageId)
1138
- : this.getCurrentPage();
1139
-
1140
- const cookie = {
1141
- name: args.name,
1142
- value: args.value
1143
- };
1144
-
1145
- if (args.domain) cookie.domain = args.domain;
1146
- if (args.path) cookie.path = args.path;
1147
- if (args.expires) cookie.expires = args.expires;
1148
- if (args.httpOnly !== undefined) cookie.httpOnly = args.httpOnly;
1149
- if (args.secure !== undefined) cookie.secure = args.secure;
1150
- if (args.sameSite) cookie.sameSite = args.sameSite;
1151
-
1152
- await page.setCookie(cookie);
1153
-
1154
- return {
1155
- success: true,
1156
- message: `已设置Cookie: ${args.name}`,
1157
- cookie: cookie
1158
- };
1159
- }
1160
- },
1161
-
1162
- // 清除Cookie
1163
- cookie_clear: {
1164
- description: '清除Cookie',
1165
- inputSchema: z.object({
1166
- name: z.string().optional().describe('指定Cookie名称,不填则清除所有'),
1167
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
1168
- }),
1169
- execute: async (args) => {
1170
- const page = args.pageId && this.pages.has(args.pageId)
1171
- ? this.pages.get(args.pageId)
1172
- : this.getCurrentPage();
1173
-
1174
- if (args.name) {
1175
- await page.deleteCookie({ name: args.name });
1176
- return {
1177
- success: true,
1178
- message: `已删除Cookie: ${args.name}`
1179
- };
1180
- }
1181
-
1182
- const cookies = await page.cookies();
1183
- await page.deleteCookie(...cookies);
1184
-
1185
- return {
1186
- success: true,
1187
- message: '已清除所有Cookie',
1188
- count: cookies.length
1189
- };
1190
- }
1191
- },
1192
-
1193
- // ============ Session管理 ============
1194
-
1195
- // 保存Session
1196
- session_save: {
1197
- description: '保存当前浏览器会话状态',
1198
- inputSchema: z.object({
1199
- name: z.string().describe('Session名称'),
1200
- includeCookies: z.boolean().optional().describe('是否包含Cookie'),
1201
- includeStorage: z.boolean().optional().describe('是否包含LocalStorage'),
1202
- }),
1203
- execute: async (args) => {
1204
- if (!this.browser) {
1205
- throw new Error('浏览器未启动');
1206
- }
1207
-
1208
- const sessionPath = path.join(SESSION_DIR, `${args.name}.json`);
1209
- const sessionData = {
1210
- name: args.name,
1211
- timestamp: Date.now(),
1212
- url: null,
1213
- cookies: null,
1214
- storage: null
1215
- };
1216
-
1217
- const pages = await this.browser.pages();
1218
- if (pages.length > 0) {
1219
- sessionData.url = pages[0].url();
1220
- }
1221
-
1222
- if (args.includeCookies !== false && pages.length > 0) {
1223
- sessionData.cookies = await pages[0].cookies();
1224
- }
1225
-
1226
- if (args.includeStorage && pages.length > 0) {
1227
- sessionData.storage = await pages[0].evaluate(() => {
1228
- return {
1229
- localStorage: { ...localStorage },
1230
- sessionStorage: { ...sessionStorage }
1231
- };
1232
- });
1233
- }
1234
-
1235
- const fs = require('fs');
1236
- fs.writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2));
1237
-
1238
- return {
1239
- success: true,
1240
- message: `Session已保存: ${args.name}`,
1241
- path: sessionPath,
1242
- size: Buffer.byteLength(JSON.stringify(sessionData))
1243
- };
1244
- }
1245
- },
1246
-
1247
- // 加载Session
1248
- session_load: {
1249
- description: '加载并恢复浏览器会话状态',
1250
- inputSchema: z.object({
1251
- name: z.string().describe('Session名称'),
1252
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
1253
- }),
1254
- execute: async (args) => {
1255
- const sessionPath = path.join(SESSION_DIR, `${args.name}.json`);
1256
- const fs = require('fs');
1257
-
1258
- if (!fs.existsSync(sessionPath)) {
1259
- throw new Error(`Session不存在: ${args.name}`);
1260
- }
1261
-
1262
- const sessionData = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
1263
-
1264
- if (!this.browser) {
1265
- throw new Error('浏览器未启动');
1266
- }
1267
-
1268
- const page = args.pageId && this.pages.has(args.pageId)
1269
- ? this.pages.get(args.pageId)
1270
- : this.getCurrentPage();
1271
-
1272
- // 恢复Cookie
1273
- if (sessionData.cookies && sessionData.cookies.length > 0) {
1274
- await page.setCookie(...sessionData.cookies);
1275
- }
1276
-
1277
- // 恢复LocalStorage
1278
- if (sessionData.storage?.localStorage) {
1279
- await page.evaluate((storage) => {
1280
- for (const [key, value] of Object.entries(storage)) {
1281
- localStorage.setItem(key, value);
1282
- }
1283
- }, sessionData.storage.localStorage);
1284
- }
1285
-
1286
- // 导航到保存的URL
1287
- if (sessionData.url) {
1288
- await page.goto(sessionData.url, { waitUntil: 'networkidle2' });
1289
- }
1290
-
1291
- return {
1292
- success: true,
1293
- message: `Session已加载: ${args.name}`,
1294
- url: sessionData.url,
1295
- cookiesCount: sessionData.cookies?.length || 0
1296
- };
1297
- }
1298
- },
1299
-
1300
- // 列出Session
1301
- session_list: {
1302
- description: '列出所有保存的Session',
1303
- inputSchema: z.object({}),
1304
- execute: async () => {
1305
- const fs = require('fs');
1306
-
1307
- if (!fs.existsSync(SESSION_DIR)) {
1308
- return {
1309
- success: true,
1310
- sessions: [],
1311
- message: '暂无保存的Session'
1312
- };
1313
- }
1314
-
1315
- const files = fs.readdirSync(SESSION_DIR)
1316
- .filter(f => f.endsWith('.json'));
1317
-
1318
- const sessions = files.map(f => {
1319
- const data = JSON.parse(fs.readFileSync(path.join(SESSION_DIR, f), 'utf8'));
1320
- return {
1321
- name: data.name,
1322
- timestamp: data.timestamp,
1323
- url: data.url,
1324
- cookiesCount: data.cookies?.length || 0,
1325
- hasStorage: !!data.storage
1326
- };
1327
- });
1328
-
1329
- return {
1330
- success: true,
1331
- sessions: sessions,
1332
- count: sessions.length
1333
- };
1334
- }
1335
- },
1336
-
1337
- // 删除Session
1338
- session_delete: {
1339
- description: '删除指定的Session',
1340
- inputSchema: z.object({
1341
- name: z.string().describe('Session名称'),
1342
- }),
1343
- execute: async (args) => {
1344
- const sessionPath = path.join(SESSION_DIR, `${args.name}.json`);
1345
- const fs = require('fs');
1346
-
1347
- if (!fs.existsSync(sessionPath)) {
1348
- throw new Error(`Session不存在: ${args.name}`);
1349
- }
1350
-
1351
- fs.unlinkSync(sessionPath);
1352
-
1353
- return {
1354
- success: true,
1355
- message: `已删除Session: ${args.name}`
1356
- };
1357
- }
1358
- },
1359
-
1360
- // ============ 等待 ============
1361
-
1362
- // 等待指定时间
1363
- wait: {
1364
- description: '等待指定时间',
1365
- inputSchema: z.object({
1366
- milliseconds: z.number().describe('等待时间(毫秒)'),
1367
- }),
1368
- execute: async (args) => {
1369
- await new Promise(r => setTimeout(r, args.milliseconds));
1370
- return {
1371
- success: true,
1372
- waited: args.milliseconds
1373
- };
1374
- }
1375
- },
1376
-
1377
- // 等待网络空闲
1378
- wait_network_idle: {
1379
- description: '等待网络空闲',
1380
- inputSchema: z.object({
1381
- timeout: z.number().optional().describe('超时时间(ms)'),
1382
- pageId: z.string().optional().describe('页面ID,默认当前页面'),
1383
- }),
1384
- execute: async (args) => {
1385
- const page = args.pageId && this.pages.has(args.pageId)
1386
- ? this.pages.get(args.pageId)
1387
- : this.getCurrentPage();
1388
-
1389
- await page.waitForNetworkIdle({ timeout: args.timeout || 30000 });
1390
-
1391
- return {
1392
- success: true,
1393
- message: '网络已空闲',
1394
- url: page.url()
1395
- };
1396
- }
1397
- }
1398
- };
1399
-
1400
- // 生命周期方法
1401
- install(framework) {
1402
- return this;
1403
- }
1404
-
1405
- async start(framework) {
1406
- console.log('Puppeteer 插件已启动');
1407
- }
1408
-
1409
- async uninstall(framework) {
1410
- // 关闭浏览器
1411
- if (this.browser) {
1412
- await this.browser.close();
1413
- this.browser = null;
1414
- this.pages.clear();
1415
- }
1416
- }
1417
- };
1418
- };