@swarmclawai/swarmclaw 0.7.7 → 0.8.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 +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -2,12 +2,13 @@ import { z } from 'zod'
|
|
|
2
2
|
import { tool, type StructuredToolInterface } from '@langchain/core/tools'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
5
6
|
import * as cheerio from 'cheerio'
|
|
6
7
|
import { UPLOAD_DIR } from '../storage'
|
|
7
8
|
import type { ToolBuildContext } from './context'
|
|
8
9
|
import { spawnSync } from 'child_process'
|
|
9
10
|
import { safePath, truncate, MAX_OUTPUT, findBinaryOnPath } from './context'
|
|
10
|
-
import { getSearchProvider } from './search-providers'
|
|
11
|
+
import { getSearchProvider, type SearchResult } from './search-providers'
|
|
11
12
|
import { dedupeScreenshotMarkdownLines } from './web-output'
|
|
12
13
|
import { withRetry } from '../tool-retry'
|
|
13
14
|
import type { Plugin, PluginHooks } from '@/types'
|
|
@@ -16,44 +17,55 @@ import { normalizeToolInputArgs } from './normalize-tool-args'
|
|
|
16
17
|
import {
|
|
17
18
|
ensureSessionBrowserProfileId,
|
|
18
19
|
getBrowserProfileDir,
|
|
20
|
+
loadBrowserSessionRecord,
|
|
19
21
|
markBrowserSessionClosed,
|
|
20
22
|
recordBrowserObservation,
|
|
21
23
|
removeBrowserSessionRecord,
|
|
22
24
|
upsertBrowserSessionRecord,
|
|
23
25
|
} from '../browser-state'
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
function cleanSearchField(value: string | undefined): string {
|
|
28
|
+
return (value || '').replace(/\s+/g, ' ').trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatWebSearchResults(query: string, results: SearchResult[], maxChars = MAX_OUTPUT): string {
|
|
32
|
+
const cleanedQuery = cleanSearchField(query)
|
|
33
|
+
const header = cleanedQuery ? `Search results for: ${cleanedQuery}` : 'Search results'
|
|
34
|
+
const sections: string[] = [header]
|
|
35
|
+
const joinSections = (items: string[]) => items.filter(Boolean).join('\n\n')
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < results.length; index++) {
|
|
38
|
+
const result = results[index]
|
|
39
|
+
const title = cleanSearchField(result?.title) || cleanSearchField(result?.url) || `Result ${index + 1}`
|
|
40
|
+
const url = cleanSearchField(result?.url)
|
|
41
|
+
const snippet = cleanSearchField(result?.snippet)
|
|
42
|
+
const lines = [`${index + 1}. ${title}`]
|
|
43
|
+
if (url) lines.push(`URL: ${url}`)
|
|
44
|
+
if (snippet) lines.push(`Snippet: ${snippet}`)
|
|
45
|
+
const candidate = joinSections([...sections, lines.join('\n')])
|
|
46
|
+
if (candidate.length <= maxChars) {
|
|
47
|
+
sections.push(lines.join('\n'))
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (url) {
|
|
52
|
+
const minimalLines = [`${index + 1}. ${title}`, `URL: ${url}`]
|
|
53
|
+
const minimalCandidate = joinSections([...sections, minimalLines.join('\n')])
|
|
54
|
+
if (minimalCandidate.length <= maxChars) {
|
|
55
|
+
sections.push(minimalLines.join('\n'))
|
|
52
56
|
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const omitted = results.length - index
|
|
60
|
+
if (omitted > 0) {
|
|
61
|
+
const remainingNotice = `(${omitted} additional result${omitted === 1 ? '' : 's'} omitted for brevity)`
|
|
62
|
+
const withNotice = joinSections([...sections, remainingNotice])
|
|
63
|
+
if (withNotice.length <= maxChars) sections.push(remainingNotice)
|
|
64
|
+
}
|
|
65
|
+
return truncate(joinSections(sections), maxChars)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return truncate(joinSections(sections), maxChars)
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
type BrowserRuntimeEntry = {
|
|
@@ -159,13 +171,221 @@ export function cleanupSessionBrowser(sessionId: string): void {
|
|
|
159
171
|
export function getActiveBrowserCount(): number { return activeBrowsers.size }
|
|
160
172
|
export function hasActiveBrowser(sessionId: string): boolean { return activeBrowsers.has(sessionId) }
|
|
161
173
|
|
|
174
|
+
export function inferWebActionFromArgs(params: {
|
|
175
|
+
action?: string
|
|
176
|
+
query?: string
|
|
177
|
+
url?: string
|
|
178
|
+
}): 'search' | 'fetch' | undefined {
|
|
179
|
+
if (params.action === 'search' || params.action === 'fetch') return params.action
|
|
180
|
+
if (typeof params.url === 'string' && /^https?:\/\//i.test(params.url.trim())) return 'fetch'
|
|
181
|
+
if (typeof params.query === 'string' && params.query.trim()) return 'search'
|
|
182
|
+
if (typeof params.url === 'string' && params.url.trim()) return 'search'
|
|
183
|
+
return undefined
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseStructuredJsonValue(value: unknown): unknown {
|
|
187
|
+
if (typeof value !== 'string') return value
|
|
188
|
+
const trimmed = value.trim()
|
|
189
|
+
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return value
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(trimmed)
|
|
192
|
+
} catch {
|
|
193
|
+
return value
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseJsonObjectValue(value: unknown): Record<string, unknown> | null {
|
|
198
|
+
const parsed = parseStructuredJsonValue(value)
|
|
199
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
200
|
+
? parsed as Record<string, unknown>
|
|
201
|
+
: null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function parseJsonArrayValue(value: unknown): unknown[] | null {
|
|
205
|
+
const parsed = parseStructuredJsonValue(value)
|
|
206
|
+
return Array.isArray(parsed) ? parsed : null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function pickNonEmptyBrowserString(...values: unknown[]): string | undefined {
|
|
210
|
+
for (const value of values) {
|
|
211
|
+
if (typeof value !== 'string') continue
|
|
212
|
+
const trimmed = value.trim()
|
|
213
|
+
if (trimmed) return trimmed
|
|
214
|
+
}
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function wrapBrowserEvaluateFunction(code: string): string {
|
|
219
|
+
const trimmed = code.trim()
|
|
220
|
+
if (!trimmed) return trimmed
|
|
221
|
+
if (/^(?:async\s+)?function\b/.test(trimmed)) return trimmed
|
|
222
|
+
if (/^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed)) return trimmed
|
|
223
|
+
return /[;{}]/.test(trimmed)
|
|
224
|
+
? `() => { ${trimmed} }`
|
|
225
|
+
: `() => (${trimmed})`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function wrapBrowserRunCodeFunction(code: string): string {
|
|
229
|
+
const trimmed = code.trim()
|
|
230
|
+
if (!trimmed) return trimmed
|
|
231
|
+
if (/^(?:async\s+)?function\b/.test(trimmed)) return trimmed
|
|
232
|
+
if (/^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed)) return trimmed
|
|
233
|
+
return /[;{}]/.test(trimmed)
|
|
234
|
+
? `async (page) => { ${trimmed} }`
|
|
235
|
+
: `async (page) => (${trimmed})`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function normalizeBrowserActionParams(rawParams: Record<string, unknown>): Record<string, unknown> {
|
|
239
|
+
const normalized = normalizeToolInputArgs(rawParams)
|
|
240
|
+
const action = String(normalized.action || '').trim().toLowerCase()
|
|
241
|
+
const params: Record<string, unknown> = { ...normalized }
|
|
242
|
+
|
|
243
|
+
const parsedFields = parseJsonArrayValue(params.fields)
|
|
244
|
+
if (parsedFields) params.fields = parsedFields
|
|
245
|
+
|
|
246
|
+
const parsedForm = parseJsonObjectValue(params.form)
|
|
247
|
+
if (parsedForm) params.form = parsedForm
|
|
248
|
+
|
|
249
|
+
if (typeof params.selector === 'string' && !pickNonEmptyBrowserString(params.element)) {
|
|
250
|
+
params.element = params.selector
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (action === 'submit_form' && typeof params.selector === 'string' && !pickNonEmptyBrowserString(params.submitElement)) {
|
|
254
|
+
params.submitElement = params.selector
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (action === 'select') {
|
|
258
|
+
const parsedValues = parseJsonArrayValue(params.values ?? params.option ?? params.value)
|
|
259
|
+
if (parsedValues) params.values = parsedValues
|
|
260
|
+
else if (params.values === undefined) {
|
|
261
|
+
const scalar = pickNonEmptyBrowserString(params.option, params.value, params.text)
|
|
262
|
+
if (scalar) params.values = [scalar]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (action === 'evaluate' && !pickNonEmptyBrowserString(params.function)) {
|
|
267
|
+
const code = pickNonEmptyBrowserString(params.code, params.script, params.javascript, params.js)
|
|
268
|
+
if (code) params.function = wrapBrowserEvaluateFunction(code)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (action === 'run_code') {
|
|
272
|
+
const code = pickNonEmptyBrowserString(params.code, params.function, params.script, params.javascript, params.js)
|
|
273
|
+
if (code) params.code = wrapBrowserRunCodeFunction(code)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return params
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function pickBrowserTargetFromParams(params: Record<string, unknown>): string | null {
|
|
280
|
+
for (const value of [
|
|
281
|
+
params.url,
|
|
282
|
+
params.filePath,
|
|
283
|
+
params.path,
|
|
284
|
+
params.href,
|
|
285
|
+
params.link,
|
|
286
|
+
params.target,
|
|
287
|
+
params.page,
|
|
288
|
+
]) {
|
|
289
|
+
if (typeof value !== 'string') continue
|
|
290
|
+
const trimmed = value.trim()
|
|
291
|
+
if (trimmed) return trimmed
|
|
292
|
+
}
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveUploadFilePath(target: string): string | null {
|
|
297
|
+
const normalized = target.replace(/^sandbox:/, '')
|
|
298
|
+
const match = normalized.match(/^\/api\/uploads\/([^?#]+)/)
|
|
299
|
+
if (!match) return null
|
|
300
|
+
let decoded = match[1]
|
|
301
|
+
try {
|
|
302
|
+
decoded = decodeURIComponent(decoded)
|
|
303
|
+
} catch {
|
|
304
|
+
// keep raw segment
|
|
305
|
+
}
|
|
306
|
+
const safeName = decoded.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
307
|
+
const resolved = path.join(UPLOAD_DIR, safeName)
|
|
308
|
+
return fs.existsSync(resolved) ? resolved : null
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resolveBrowserFileUrlPath(target: string): string | null {
|
|
312
|
+
if (!/^file:/i.test(target)) return null
|
|
313
|
+
try {
|
|
314
|
+
const resolved = fileURLToPath(target)
|
|
315
|
+
return fs.existsSync(resolved) ? resolved : null
|
|
316
|
+
} catch {
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function tryResolveBrowserLocalPath(cwd: string, target: string): string | null {
|
|
322
|
+
const uploadPath = resolveUploadFilePath(target)
|
|
323
|
+
if (uploadPath) return uploadPath
|
|
324
|
+
|
|
325
|
+
const fileUrlPath = resolveBrowserFileUrlPath(target)
|
|
326
|
+
if (fileUrlPath) return fileUrlPath
|
|
327
|
+
|
|
328
|
+
if (/^(?:https?:|about:|data:)/i.test(target)) return null
|
|
329
|
+
|
|
330
|
+
const normalized = target.replace(/^sandbox:/, '')
|
|
331
|
+
const looksLikePath = normalized.startsWith('/')
|
|
332
|
+
|| normalized.startsWith('./')
|
|
333
|
+
|| normalized.startsWith('../')
|
|
334
|
+
|| normalized.includes('/')
|
|
335
|
+
|| /\.(?:html?|xhtml|txt|md|json|ya?ml|csv|ts|tsx|js|jsx|mjs|cjs|css|png|jpe?g|gif|webp|svg|pdf)$/i.test(normalized)
|
|
336
|
+
if (!looksLikePath) return null
|
|
337
|
+
|
|
338
|
+
const candidates = new Set<string>()
|
|
339
|
+
if (path.isAbsolute(normalized)) candidates.add(normalized)
|
|
340
|
+
try { candidates.add(safePath(cwd, normalized)) } catch { /* ignore */ }
|
|
341
|
+
try { candidates.add(path.resolve(cwd, normalized)) } catch { /* ignore */ }
|
|
342
|
+
|
|
343
|
+
for (const candidate of candidates) {
|
|
344
|
+
if (!candidate || !fs.existsSync(candidate)) continue
|
|
345
|
+
const stat = fs.statSync(candidate)
|
|
346
|
+
if (stat.isDirectory()) {
|
|
347
|
+
const indexPath = path.join(candidate, 'index.html')
|
|
348
|
+
if (fs.existsSync(indexPath)) return indexPath
|
|
349
|
+
return null
|
|
350
|
+
}
|
|
351
|
+
return candidate
|
|
352
|
+
}
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function localHtmlFileToDataUrl(filePath: string): string | null {
|
|
357
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
358
|
+
if (ext !== '.html' && ext !== '.htm') return null
|
|
359
|
+
try {
|
|
360
|
+
const html = fs.readFileSync(filePath, 'utf8')
|
|
361
|
+
const hasRelativeAssetReferences = /<(?:script|img|source|video|audio)\b[^>]+\b(?:src|poster)\s*=\s*["'](?![a-z]+:|\/\/|#|\/)([^"']+)["']|<link\b[^>]+\bhref\s*=\s*["'](?![a-z]+:|\/\/|#|\/)([^"']+)["']/i.test(html)
|
|
362
|
+
if (hasRelativeAssetReferences) return null
|
|
363
|
+
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`
|
|
364
|
+
} catch {
|
|
365
|
+
return null
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function resolveBrowserNavigationTarget(cwd: string, target: string): string {
|
|
370
|
+
const trimmed = target.trim()
|
|
371
|
+
if (!trimmed) return trimmed
|
|
372
|
+
const localPath = tryResolveBrowserLocalPath(cwd, trimmed)
|
|
373
|
+
if (localPath) return localHtmlFileToDataUrl(localPath) || pathToFileURL(localPath).toString()
|
|
374
|
+
return trimmed.replace(/^sandbox:/, '')
|
|
375
|
+
}
|
|
376
|
+
|
|
162
377
|
/**
|
|
163
378
|
* Unified Web Execution Logic
|
|
164
379
|
*/
|
|
165
380
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
-
async function executeWebAction(args: Record<string, unknown
|
|
381
|
+
async function executeWebAction(args: Record<string, unknown>) {
|
|
167
382
|
const normalized = normalizeToolInputArgs(args)
|
|
168
|
-
const {
|
|
383
|
+
const { query, url, maxResults } = normalized as { query?: string; url?: string; maxResults?: number }
|
|
384
|
+
const action = inferWebActionFromArgs({
|
|
385
|
+
action: (normalized as { action?: string }).action,
|
|
386
|
+
query,
|
|
387
|
+
url,
|
|
388
|
+
})
|
|
169
389
|
try {
|
|
170
390
|
if (action === 'search') {
|
|
171
391
|
const searchQuery = query || url
|
|
@@ -176,12 +396,7 @@ async function executeWebAction(args: Record<string, unknown>, bctx: any) {
|
|
|
176
396
|
const provider = await getSearchProvider(settings)
|
|
177
397
|
const results = await provider.search(searchQuery, limit)
|
|
178
398
|
if (results.length === 0) return 'No results found.'
|
|
179
|
-
|
|
180
|
-
if (raw.length > 2000) {
|
|
181
|
-
const compressed = await compressSearchResults(results, searchQuery, bctx)
|
|
182
|
-
if (compressed) return compressed
|
|
183
|
-
}
|
|
184
|
-
return raw
|
|
399
|
+
return formatWebSearchResults(searchQuery, results)
|
|
185
400
|
} else if (action === 'fetch') {
|
|
186
401
|
const fetchUrl = url || query
|
|
187
402
|
if (!fetchUrl) return 'Error: "url" is required for fetch action.'
|
|
@@ -238,7 +453,7 @@ const WebPlugin: Plugin = {
|
|
|
238
453
|
},
|
|
239
454
|
required: ['action']
|
|
240
455
|
},
|
|
241
|
-
execute: async (args
|
|
456
|
+
execute: async (args) => executeWebAction(args)
|
|
242
457
|
}
|
|
243
458
|
]
|
|
244
459
|
}
|
|
@@ -255,7 +470,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
255
470
|
if (bctx.hasPlugin('web')) {
|
|
256
471
|
tools.push(
|
|
257
472
|
tool(
|
|
258
|
-
async (args) => executeWebAction(args
|
|
473
|
+
async (args) => executeWebAction(args),
|
|
259
474
|
{
|
|
260
475
|
name: 'web',
|
|
261
476
|
description: WebPlugin.tools![0].description,
|
|
@@ -329,13 +544,30 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
329
544
|
pendingBrowserInitializations.set(sessionKey, connectPromise)
|
|
330
545
|
const entry = await connectPromise
|
|
331
546
|
acquireExistingEntry(entry)
|
|
547
|
+
const lastState = loadBrowserSessionRecord(sessionKey)
|
|
548
|
+
const restoreUrl = typeof lastState?.currentUrl === 'string' ? lastState.currentUrl.trim() : ''
|
|
549
|
+
if (restoreUrl && restoreUrl !== 'about:blank') {
|
|
550
|
+
try {
|
|
551
|
+
await entry.client.callTool({ name: 'browser_navigate', arguments: { url: restoreUrl } })
|
|
552
|
+
} catch (err: unknown) {
|
|
553
|
+
upsertBrowserSessionRecord({
|
|
554
|
+
sessionId: sessionKey,
|
|
555
|
+
profileId: profileInfo.profileId,
|
|
556
|
+
profileDir,
|
|
557
|
+
inheritedFromSessionId: profileInfo.inheritedFromSessionId,
|
|
558
|
+
status: 'error',
|
|
559
|
+
lastAction: 'browser_restore',
|
|
560
|
+
lastError: err instanceof Error ? err.message : String(err),
|
|
561
|
+
})
|
|
562
|
+
}
|
|
563
|
+
}
|
|
332
564
|
upsertBrowserSessionRecord({
|
|
333
565
|
sessionId: sessionKey,
|
|
334
566
|
profileId: profileInfo.profileId,
|
|
335
567
|
profileDir,
|
|
336
568
|
inheritedFromSessionId: profileInfo.inheritedFromSessionId,
|
|
337
569
|
status: 'active',
|
|
338
|
-
lastAction: 'browser_open',
|
|
570
|
+
lastAction: restoreUrl && restoreUrl !== 'about:blank' ? 'browser_restore' : 'browser_open',
|
|
339
571
|
})
|
|
340
572
|
} finally {
|
|
341
573
|
if (pendingBrowserInitializations.get(sessionKey)) {
|
|
@@ -404,6 +636,105 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
404
636
|
function: fn,
|
|
405
637
|
})
|
|
406
638
|
|
|
639
|
+
const performSelectorDomAction = async (
|
|
640
|
+
action: 'click' | 'type' | 'select' | 'hover',
|
|
641
|
+
params: Record<string, unknown>,
|
|
642
|
+
): Promise<{ ok: true; output: string } | { ok: false; error: string } | null> => {
|
|
643
|
+
const selector = pickNonEmptyBrowserString(params.element, params.selector)
|
|
644
|
+
if (!selector) return null
|
|
645
|
+
if (typeof params.ref === 'string' && params.ref.trim()) return null
|
|
646
|
+
|
|
647
|
+
const payload =
|
|
648
|
+
action === 'click'
|
|
649
|
+
? `() => {
|
|
650
|
+
const selector = ${JSON.stringify(selector)};
|
|
651
|
+
try {
|
|
652
|
+
const element = document.querySelector(selector);
|
|
653
|
+
if (!element) return { ok: false, error: 'No element matched selector.' };
|
|
654
|
+
element.click?.();
|
|
655
|
+
return { ok: true, action: 'click', selector };
|
|
656
|
+
} catch (error) {
|
|
657
|
+
return { ok: false, error: String(error) };
|
|
658
|
+
}
|
|
659
|
+
}`
|
|
660
|
+
: action === 'hover'
|
|
661
|
+
? `() => {
|
|
662
|
+
const selector = ${JSON.stringify(selector)};
|
|
663
|
+
try {
|
|
664
|
+
const element = document.querySelector(selector);
|
|
665
|
+
if (!element) return { ok: false, error: 'No element matched selector.' };
|
|
666
|
+
element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
667
|
+
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
668
|
+
return { ok: true, action: 'hover', selector };
|
|
669
|
+
} catch (error) {
|
|
670
|
+
return { ok: false, error: String(error) };
|
|
671
|
+
}
|
|
672
|
+
}`
|
|
673
|
+
: action === 'type'
|
|
674
|
+
? `() => {
|
|
675
|
+
const selector = ${JSON.stringify(selector)};
|
|
676
|
+
const value = ${JSON.stringify(String(params.text ?? params.value ?? ''))};
|
|
677
|
+
const submit = ${params.submit === true ? 'true' : 'false'};
|
|
678
|
+
try {
|
|
679
|
+
const element = document.querySelector(selector);
|
|
680
|
+
if (!element) return { ok: false, error: 'No element matched selector.' };
|
|
681
|
+
element.focus?.();
|
|
682
|
+
if ('value' in element) element.value = value;
|
|
683
|
+
else if (element.isContentEditable) element.textContent = value;
|
|
684
|
+
else return { ok: false, error: 'Matched element is not editable.' };
|
|
685
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
686
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
687
|
+
if (submit) {
|
|
688
|
+
if (element.form?.requestSubmit) element.form.requestSubmit();
|
|
689
|
+
else {
|
|
690
|
+
element.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
691
|
+
element.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true }));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return { ok: true, action: 'type', selector, valueLength: value.length };
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return { ok: false, error: String(error) };
|
|
697
|
+
}
|
|
698
|
+
}`
|
|
699
|
+
: `() => {
|
|
700
|
+
const selector = ${JSON.stringify(selector)};
|
|
701
|
+
const desired = ${JSON.stringify(
|
|
702
|
+
Array.isArray(params.values)
|
|
703
|
+
? params.values.map((value) => String(value ?? ''))
|
|
704
|
+
: [String(params.value ?? params.option ?? '')],
|
|
705
|
+
)};
|
|
706
|
+
try {
|
|
707
|
+
const element = document.querySelector(selector);
|
|
708
|
+
if (!element) return { ok: false, error: 'No element matched selector.' };
|
|
709
|
+
if (!(element instanceof HTMLSelectElement)) return { ok: false, error: 'Matched element is not a <select>.' };
|
|
710
|
+
const selected = [];
|
|
711
|
+
for (const option of Array.from(element.options)) {
|
|
712
|
+
const match = desired.includes(option.value) || desired.includes(option.text);
|
|
713
|
+
option.selected = match;
|
|
714
|
+
if (match) selected.push(option.value || option.text || '');
|
|
715
|
+
}
|
|
716
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
717
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
718
|
+
return { ok: true, action: 'select', selector, selected };
|
|
719
|
+
} catch (error) {
|
|
720
|
+
return { ok: false, error: String(error) };
|
|
721
|
+
}
|
|
722
|
+
}`
|
|
723
|
+
|
|
724
|
+
const raw = await callBrowserEvaluate(payload)
|
|
725
|
+
const parsed = extractJsonPayload(raw)
|
|
726
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
727
|
+
return { ok: false, error: cleanPlaywrightOutput(raw) || `DOM ${action} fallback failed.` }
|
|
728
|
+
}
|
|
729
|
+
if ((parsed as Record<string, unknown>).ok !== true) {
|
|
730
|
+
return {
|
|
731
|
+
ok: false,
|
|
732
|
+
error: String((parsed as Record<string, unknown>).error || `DOM ${action} fallback failed.`),
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return { ok: true, output: stringifyStructured(parsed) }
|
|
736
|
+
}
|
|
737
|
+
|
|
407
738
|
const captureStructuredObservation = async () => {
|
|
408
739
|
const expression = `() => {
|
|
409
740
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
@@ -650,12 +981,141 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
650
981
|
}
|
|
651
982
|
|
|
652
983
|
const dismissCookieBanners = async (mcpCall: (toolName: string, args: Record<string, unknown>) => Promise<string>) => {
|
|
653
|
-
await new Promise((r) => setTimeout(r,
|
|
984
|
+
await new Promise((r) => setTimeout(r, 1200))
|
|
654
985
|
const js = `() => {
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
const
|
|
658
|
-
|
|
986
|
+
const docs = [document];
|
|
987
|
+
const roots = [document];
|
|
988
|
+
const seenRoots = new Set([document]);
|
|
989
|
+
const pushRoot = (root) => {
|
|
990
|
+
if (!root || seenRoots.has(root)) return;
|
|
991
|
+
seenRoots.add(root);
|
|
992
|
+
roots.push(root);
|
|
993
|
+
};
|
|
994
|
+
const collectFrames = (doc) => {
|
|
995
|
+
try {
|
|
996
|
+
const frames = doc.querySelectorAll('iframe');
|
|
997
|
+
for (const frame of frames) {
|
|
998
|
+
try {
|
|
999
|
+
const child = frame.contentDocument || frame.contentWindow?.document;
|
|
1000
|
+
if (child && !docs.includes(child)) {
|
|
1001
|
+
docs.push(child);
|
|
1002
|
+
pushRoot(child);
|
|
1003
|
+
}
|
|
1004
|
+
} catch {}
|
|
1005
|
+
}
|
|
1006
|
+
} catch {}
|
|
1007
|
+
};
|
|
1008
|
+
const collectShadowRoots = () => {
|
|
1009
|
+
for (const root of [...roots]) {
|
|
1010
|
+
try {
|
|
1011
|
+
const all = root.querySelectorAll('*');
|
|
1012
|
+
for (const el of all) {
|
|
1013
|
+
if (el.shadowRoot) pushRoot(el.shadowRoot);
|
|
1014
|
+
}
|
|
1015
|
+
} catch {}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
collectFrames(document);
|
|
1019
|
+
collectShadowRoots();
|
|
1020
|
+
const allRoots = [...new Set([...docs, ...roots])];
|
|
1021
|
+
const visible = (el) => {
|
|
1022
|
+
if (!el) return false;
|
|
1023
|
+
const style = window.getComputedStyle(el);
|
|
1024
|
+
if (!style || style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
1025
|
+
const rect = el.getBoundingClientRect();
|
|
1026
|
+
return rect.width > 0 && rect.height > 0;
|
|
1027
|
+
};
|
|
1028
|
+
const normalizedText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
1029
|
+
const candidateSelectors = [
|
|
1030
|
+
'#onetrust-reject-all-handler',
|
|
1031
|
+
'#CybotCookiebotDialogBodyButtonDecline',
|
|
1032
|
+
'#didomi-notice-disagree-button',
|
|
1033
|
+
'.qc-cmp2-summary-buttons button:first-child',
|
|
1034
|
+
'button.sp_choice_type_12',
|
|
1035
|
+
'button[id*="reject" i]',
|
|
1036
|
+
'button[class*="reject" i]',
|
|
1037
|
+
'button[id*="decline" i]',
|
|
1038
|
+
'button[class*="decline" i]',
|
|
1039
|
+
'button[id*="consent" i]',
|
|
1040
|
+
'button[class*="consent" i]',
|
|
1041
|
+
'a[id*="reject" i]',
|
|
1042
|
+
'a[class*="reject" i]',
|
|
1043
|
+
'a[id*="decline" i]',
|
|
1044
|
+
'a[class*="decline" i]'
|
|
1045
|
+
];
|
|
1046
|
+
for (const root of allRoots) {
|
|
1047
|
+
for (const selector of candidateSelectors) {
|
|
1048
|
+
try {
|
|
1049
|
+
const el = root.querySelector(selector);
|
|
1050
|
+
if (el && visible(el)) {
|
|
1051
|
+
el.click();
|
|
1052
|
+
return 'clicked:' + selector;
|
|
1053
|
+
}
|
|
1054
|
+
} catch {}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const buttonSelector = 'button, a[role="button"], [role="button"], input[type="button"], input[type="submit"]';
|
|
1058
|
+
const rejectRe = /^(reject|reject all|decline|decline all|deny|deny all|refuse|no,? thanks|only necessary|necessary only|use necessary cookies only)$/i;
|
|
1059
|
+
const acceptRe = /^(accept|accept all|allow all|agree|i agree|okay|ok|got it|continue|consent)$/i;
|
|
1060
|
+
const closeRe = /^(close|dismiss|skip|not now|x|×)$/i;
|
|
1061
|
+
const clickMatching = (matcher, label) => {
|
|
1062
|
+
for (const root of allRoots) {
|
|
1063
|
+
let buttons = [];
|
|
1064
|
+
try { buttons = [...root.querySelectorAll(buttonSelector)]; } catch {}
|
|
1065
|
+
for (const button of buttons) {
|
|
1066
|
+
const txt = normalizedText(button.textContent || button.getAttribute?.('aria-label') || button.getAttribute?.('value'));
|
|
1067
|
+
if (!txt || !matcher.test(txt) || !visible(button)) continue;
|
|
1068
|
+
try {
|
|
1069
|
+
button.click();
|
|
1070
|
+
return label + ':' + txt.slice(0, 80);
|
|
1071
|
+
} catch {}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return null;
|
|
1075
|
+
};
|
|
1076
|
+
const clicked = clickMatching(rejectRe, 'reject') || clickMatching(acceptRe, 'accept') || clickMatching(closeRe, 'close');
|
|
1077
|
+
if (clicked) return clicked;
|
|
1078
|
+
const overlaySelectors = [
|
|
1079
|
+
'#onetrust-banner-sdk',
|
|
1080
|
+
'#onetrust-consent-sdk',
|
|
1081
|
+
'#CybotCookiebotDialog',
|
|
1082
|
+
'.didomi-popup-container',
|
|
1083
|
+
'.fc-consent-root',
|
|
1084
|
+
'[id*="cookie" i]',
|
|
1085
|
+
'[class*="cookie" i]',
|
|
1086
|
+
'[id*="consent" i]',
|
|
1087
|
+
'[class*="consent" i]',
|
|
1088
|
+
'[id*="privacy" i]',
|
|
1089
|
+
'[class*="privacy" i]',
|
|
1090
|
+
'[id*="sp_message" i]',
|
|
1091
|
+
'[class*="sp_message" i]'
|
|
1092
|
+
];
|
|
1093
|
+
const hidden = [];
|
|
1094
|
+
for (const root of allRoots) {
|
|
1095
|
+
for (const selector of overlaySelectors) {
|
|
1096
|
+
let nodes = [];
|
|
1097
|
+
try { nodes = [...root.querySelectorAll(selector)]; } catch {}
|
|
1098
|
+
for (const node of nodes) {
|
|
1099
|
+
if (!visible(node)) continue;
|
|
1100
|
+
const text = normalizedText(node.textContent).toLowerCase();
|
|
1101
|
+
const attrs = normalizedText(node.id + ' ' + node.className).toLowerCase();
|
|
1102
|
+
if (!text.includes('cookie') && !text.includes('consent') && !text.includes('privacy') && !attrs.includes('cookie') && !attrs.includes('consent') && !attrs.includes('privacy') && !attrs.includes('onetrust') && !attrs.includes('didomi') && !attrs.includes('sp_message')) continue;
|
|
1103
|
+
try {
|
|
1104
|
+
node.style.setProperty('display', 'none', 'important');
|
|
1105
|
+
node.style.setProperty('visibility', 'hidden', 'important');
|
|
1106
|
+
node.style.setProperty('pointer-events', 'none', 'important');
|
|
1107
|
+
hidden.push(selector);
|
|
1108
|
+
} catch {}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (hidden.length) {
|
|
1113
|
+
try {
|
|
1114
|
+
document.documentElement.style.removeProperty('overflow');
|
|
1115
|
+
document.body.style.removeProperty('overflow');
|
|
1116
|
+
} catch {}
|
|
1117
|
+
return 'hidden:' + hidden[0];
|
|
1118
|
+
}
|
|
659
1119
|
return 'none';
|
|
660
1120
|
}`
|
|
661
1121
|
await mcpCall('browser_evaluate', { function: js })
|
|
@@ -665,9 +1125,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
665
1125
|
const fields = Array.isArray(params.fields)
|
|
666
1126
|
? params.fields
|
|
667
1127
|
: (() => {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1128
|
+
const form = params.form
|
|
1129
|
+
if (!form || typeof form !== 'object' || Array.isArray(form)) return []
|
|
1130
|
+
return Object.entries(form as Record<string, unknown>).map(([key, value]) => {
|
|
671
1131
|
const escapedId = String(key).replace(/[^a-zA-Z0-9_-]/g, '')
|
|
672
1132
|
const escapedAttr = String(key).replace(/["\\]/g, '\\$&')
|
|
673
1133
|
const inferredType = typeof value === 'boolean'
|
|
@@ -692,11 +1152,43 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
692
1152
|
const ref = typeof entry.ref === 'string' ? entry.ref : undefined
|
|
693
1153
|
const element = typeof entry.element === 'string' ? entry.element : undefined
|
|
694
1154
|
const fieldType = String(entry.type || 'text').toLowerCase()
|
|
695
|
-
const value = entry.value
|
|
1155
|
+
const value = entry.value ?? entry.text
|
|
696
1156
|
if (!ref && !element) continue
|
|
1157
|
+
const selectValues = Array.isArray(entry.values)
|
|
1158
|
+
? entry.values.map((item) => String(item ?? ''))
|
|
1159
|
+
: Array.isArray(parseJsonArrayValue(entry.values ?? entry.option ?? value))
|
|
1160
|
+
? (parseJsonArrayValue(entry.values ?? entry.option ?? value) as unknown[]).map((item) => String(item ?? ''))
|
|
1161
|
+
: [String(entry.option ?? value ?? '')]
|
|
1162
|
+
if (!ref && element) {
|
|
1163
|
+
if (fieldType === 'select') {
|
|
1164
|
+
const result = await performSelectorDomAction('select', { element, values: selectValues })
|
|
1165
|
+
if (!result) return { ok: false, error: 'Selector fallback failed for select field.' }
|
|
1166
|
+
if (!result.ok) return result
|
|
1167
|
+
} else if (fieldType === 'checkbox' || fieldType === 'radio') {
|
|
1168
|
+
if (value === true || value === 'true' || value === 'on' || value === 'checked') {
|
|
1169
|
+
const result = await performSelectorDomAction('click', { element })
|
|
1170
|
+
if (!result) return { ok: false, error: 'Selector fallback failed for checkbox field.' }
|
|
1171
|
+
if (!result.ok) return result
|
|
1172
|
+
}
|
|
1173
|
+
} else {
|
|
1174
|
+
const result = await performSelectorDomAction('type', {
|
|
1175
|
+
element,
|
|
1176
|
+
text: String(value ?? ''),
|
|
1177
|
+
submit: params.submit === true,
|
|
1178
|
+
})
|
|
1179
|
+
if (!result) return { ok: false, error: 'Selector fallback failed for input field.' }
|
|
1180
|
+
if (!result.ok) return result
|
|
1181
|
+
}
|
|
1182
|
+
filled.push({
|
|
1183
|
+
ref: null,
|
|
1184
|
+
element,
|
|
1185
|
+
type: fieldType,
|
|
1186
|
+
value: value ?? null,
|
|
1187
|
+
})
|
|
1188
|
+
continue
|
|
1189
|
+
}
|
|
697
1190
|
if (fieldType === 'select') {
|
|
698
|
-
|
|
699
|
-
await callMcpTool('browser_select_option', { ref, element, values })
|
|
1191
|
+
await callMcpTool('browser_select_option', { ref, element, values: selectValues })
|
|
700
1192
|
} else if (fieldType === 'checkbox' || fieldType === 'radio') {
|
|
701
1193
|
if (value === true || value === 'true' || value === 'on' || value === 'checked') {
|
|
702
1194
|
await callMcpTool('browser_click', { ref, element })
|
|
@@ -720,11 +1212,19 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
720
1212
|
}
|
|
721
1213
|
|
|
722
1214
|
const submitForm = async (params: Record<string, unknown>) => {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1215
|
+
const submitRef = pickNonEmptyBrowserString(params.submitRef)
|
|
1216
|
+
const submitElement = pickNonEmptyBrowserString(params.submitElement, params.selector, params.element)
|
|
1217
|
+
if (submitRef || submitElement) {
|
|
1218
|
+
if (submitRef) {
|
|
1219
|
+
await callMcpTool('browser_click', {
|
|
1220
|
+
ref: submitRef,
|
|
1221
|
+
element: submitElement,
|
|
1222
|
+
})
|
|
1223
|
+
} else if (submitElement) {
|
|
1224
|
+
const result = await performSelectorDomAction('click', { element: submitElement })
|
|
1225
|
+
if (!result) return { ok: false, error: 'submitElement is required for selector-based submit.' }
|
|
1226
|
+
if (!result.ok) return result
|
|
1227
|
+
}
|
|
728
1228
|
} else {
|
|
729
1229
|
await callBrowserEvaluate(`() => {
|
|
730
1230
|
const form = document.forms[0];
|
|
@@ -911,8 +1411,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
911
1411
|
const completeWebTask = async (params: Record<string, unknown>) => {
|
|
912
1412
|
const steps: string[] = []
|
|
913
1413
|
if (typeof params.url === 'string' && params.url.trim()) {
|
|
914
|
-
|
|
915
|
-
|
|
1414
|
+
const navigationTarget = resolveBrowserNavigationTarget(cwd, params.url.trim())
|
|
1415
|
+
await callMcpTool('browser_navigate', { url: navigationTarget })
|
|
1416
|
+
steps.push(`navigate:${navigationTarget}`)
|
|
916
1417
|
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
917
1418
|
}
|
|
918
1419
|
|
|
@@ -927,7 +1428,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
927
1428
|
if (scroll.ok) initialPage = scroll.page
|
|
928
1429
|
}
|
|
929
1430
|
|
|
930
|
-
if (Array.isArray(params.fields) && params.fields.length > 0) {
|
|
1431
|
+
if ((Array.isArray(params.fields) && params.fields.length > 0) || (params.form && typeof params.form === 'object' && !Array.isArray(params.form))) {
|
|
931
1432
|
const filled = await performFillForm(params)
|
|
932
1433
|
if (!filled.ok) return filled
|
|
933
1434
|
steps.push('fill_form')
|
|
@@ -991,7 +1492,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
991
1492
|
tools.push(
|
|
992
1493
|
tool(
|
|
993
1494
|
async (rawParams) => {
|
|
994
|
-
const params =
|
|
1495
|
+
const params = normalizeBrowserActionParams((rawParams ?? {}) as Record<string, unknown>)
|
|
995
1496
|
try {
|
|
996
1497
|
const action = String(params.action || '').trim()
|
|
997
1498
|
|
|
@@ -1029,9 +1530,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1029
1530
|
}
|
|
1030
1531
|
|
|
1031
1532
|
if (action === 'read_page') {
|
|
1032
|
-
const
|
|
1033
|
-
if (
|
|
1034
|
-
await callMcpTool('browser_navigate', { url })
|
|
1533
|
+
const target = pickBrowserTargetFromParams(params)
|
|
1534
|
+
if (target) {
|
|
1535
|
+
await callMcpTool('browser_navigate', { url: resolveBrowserNavigationTarget(cwd, target) })
|
|
1035
1536
|
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
1036
1537
|
}
|
|
1037
1538
|
return stringifyStructured(await captureStructuredObservation())
|
|
@@ -1106,6 +1607,18 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1106
1607
|
if (v !== undefined && v !== null && v !== '') args[k] = v
|
|
1107
1608
|
}
|
|
1108
1609
|
|
|
1610
|
+
if (action === 'navigate') {
|
|
1611
|
+
const target = pickBrowserTargetFromParams(params)
|
|
1612
|
+
if (!target) return 'Error: url or filePath is required for navigate.'
|
|
1613
|
+
args.url = resolveBrowserNavigationTarget(cwd, target)
|
|
1614
|
+
delete args.filePath
|
|
1615
|
+
delete args.path
|
|
1616
|
+
delete args.href
|
|
1617
|
+
delete args.link
|
|
1618
|
+
delete args.target
|
|
1619
|
+
delete args.page
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1109
1622
|
if (action === 'tabs') {
|
|
1110
1623
|
args.action = typeof params.tabAction === 'string' ? params.tabAction : 'list'
|
|
1111
1624
|
delete args.tabAction
|
|
@@ -1118,9 +1631,13 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1118
1631
|
args.values = Array.isArray(args.option) ? args.option : [String(args.option)]
|
|
1119
1632
|
delete args.option
|
|
1120
1633
|
}
|
|
1634
|
+
if (action === 'select' && args.values === undefined && args.value !== undefined) {
|
|
1635
|
+
args.values = Array.isArray(args.value) ? args.value : [String(args.value)]
|
|
1636
|
+
delete args.value
|
|
1637
|
+
}
|
|
1121
1638
|
|
|
1122
1639
|
if ((action === 'screenshot' || action === 'snapshot') && args.url) {
|
|
1123
|
-
const navUrl = args.url
|
|
1640
|
+
const navUrl = resolveBrowserNavigationTarget(cwd, String(args.url))
|
|
1124
1641
|
delete args.url
|
|
1125
1642
|
await callMcpTool('browser_navigate', { url: navUrl })
|
|
1126
1643
|
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
@@ -1139,6 +1656,15 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1139
1656
|
} catch {
|
|
1140
1657
|
await new Promise((r) => setTimeout(r, 1200))
|
|
1141
1658
|
}
|
|
1659
|
+
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (['click', 'hover', 'type', 'select'].includes(action) && !args.ref && typeof args.element === 'string') {
|
|
1663
|
+
const selectorResult = await performSelectorDomAction(action as 'click' | 'hover' | 'type' | 'select', args)
|
|
1664
|
+
if (!selectorResult) return `Error: ${action} requires a target element.`
|
|
1665
|
+
if (!selectorResult.ok) return `Error: ${selectorResult.error}`
|
|
1666
|
+
try { await captureStructuredObservation() } catch { /* ignore */ }
|
|
1667
|
+
return selectorResult.output
|
|
1142
1668
|
}
|
|
1143
1669
|
|
|
1144
1670
|
let result = await callMcpTool(mcpTool, args, { saveTo: typeof params.saveTo === 'string' ? params.saveTo : undefined })
|
|
@@ -1219,9 +1745,34 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1219
1745
|
const cmdArgs = (normalized.args ?? normalized.arguments) as string | undefined
|
|
1220
1746
|
try {
|
|
1221
1747
|
if (!command) return 'Error: command is required.'
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1748
|
+
const parsedArgs = cmdArgs ? cmdArgs.split(/\s+/).filter(Boolean) : []
|
|
1749
|
+
const runBrowserCommand = (browserCommand: string, browserArgs: string[]) => {
|
|
1750
|
+
const spawnArgs = ['browser', '--json', browserCommand, ...browserArgs]
|
|
1751
|
+
return spawnSync(openclawPath, spawnArgs, {
|
|
1752
|
+
encoding: 'utf-8',
|
|
1753
|
+
timeout: 60_000,
|
|
1754
|
+
maxBuffer: MAX_OUTPUT,
|
|
1755
|
+
})
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (command === 'capture') {
|
|
1759
|
+
const outputs: string[] = []
|
|
1760
|
+
if (parsedArgs.length > 0) {
|
|
1761
|
+
const openResult = runBrowserCommand('open', parsedArgs)
|
|
1762
|
+
if (openResult.status !== 0) {
|
|
1763
|
+
return `Error (exit ${openResult.status}): ${openResult.stderr || openResult.stdout || 'unknown'}`
|
|
1764
|
+
}
|
|
1765
|
+
if (openResult.stdout?.trim()) outputs.push(openResult.stdout.trim())
|
|
1766
|
+
}
|
|
1767
|
+
const screenshotResult = runBrowserCommand('screenshot', [])
|
|
1768
|
+
if (screenshotResult.status !== 0) {
|
|
1769
|
+
return `Error (exit ${screenshotResult.status}): ${screenshotResult.stderr || screenshotResult.stdout || 'unknown'}`
|
|
1770
|
+
}
|
|
1771
|
+
if (screenshotResult.stdout?.trim()) outputs.push(screenshotResult.stdout.trim())
|
|
1772
|
+
return truncate(outputs.join('\n').trim() || '(no output)', MAX_OUTPUT)
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const result = runBrowserCommand(command, parsedArgs)
|
|
1225
1776
|
if (result.status !== 0) return `Error (exit ${result.status}): ${result.stderr || result.stdout || 'unknown'}`
|
|
1226
1777
|
return truncate(result.stdout || '(no output)', MAX_OUTPUT)
|
|
1227
1778
|
} catch (err: unknown) { return `Error: ${err instanceof Error ? err.message : String(err)}` }
|