@swarmclawai/swarmclaw 0.7.8 → 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 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -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 = {
|
|
@@ -171,11 +183,202 @@ export function inferWebActionFromArgs(params: {
|
|
|
171
183
|
return undefined
|
|
172
184
|
}
|
|
173
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
|
+
|
|
174
377
|
/**
|
|
175
378
|
* Unified Web Execution Logic
|
|
176
379
|
*/
|
|
177
380
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
|
-
async function executeWebAction(args: Record<string, unknown
|
|
381
|
+
async function executeWebAction(args: Record<string, unknown>) {
|
|
179
382
|
const normalized = normalizeToolInputArgs(args)
|
|
180
383
|
const { query, url, maxResults } = normalized as { query?: string; url?: string; maxResults?: number }
|
|
181
384
|
const action = inferWebActionFromArgs({
|
|
@@ -193,12 +396,7 @@ async function executeWebAction(args: Record<string, unknown>, bctx: any) {
|
|
|
193
396
|
const provider = await getSearchProvider(settings)
|
|
194
397
|
const results = await provider.search(searchQuery, limit)
|
|
195
398
|
if (results.length === 0) return 'No results found.'
|
|
196
|
-
|
|
197
|
-
if (raw.length > 2000) {
|
|
198
|
-
const compressed = await compressSearchResults(results, searchQuery, bctx)
|
|
199
|
-
if (compressed) return compressed
|
|
200
|
-
}
|
|
201
|
-
return raw
|
|
399
|
+
return formatWebSearchResults(searchQuery, results)
|
|
202
400
|
} else if (action === 'fetch') {
|
|
203
401
|
const fetchUrl = url || query
|
|
204
402
|
if (!fetchUrl) return 'Error: "url" is required for fetch action.'
|
|
@@ -255,7 +453,7 @@ const WebPlugin: Plugin = {
|
|
|
255
453
|
},
|
|
256
454
|
required: ['action']
|
|
257
455
|
},
|
|
258
|
-
execute: async (args
|
|
456
|
+
execute: async (args) => executeWebAction(args)
|
|
259
457
|
}
|
|
260
458
|
]
|
|
261
459
|
}
|
|
@@ -272,7 +470,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
272
470
|
if (bctx.hasPlugin('web')) {
|
|
273
471
|
tools.push(
|
|
274
472
|
tool(
|
|
275
|
-
async (args) => executeWebAction(args
|
|
473
|
+
async (args) => executeWebAction(args),
|
|
276
474
|
{
|
|
277
475
|
name: 'web',
|
|
278
476
|
description: WebPlugin.tools![0].description,
|
|
@@ -346,13 +544,30 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
346
544
|
pendingBrowserInitializations.set(sessionKey, connectPromise)
|
|
347
545
|
const entry = await connectPromise
|
|
348
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
|
+
}
|
|
349
564
|
upsertBrowserSessionRecord({
|
|
350
565
|
sessionId: sessionKey,
|
|
351
566
|
profileId: profileInfo.profileId,
|
|
352
567
|
profileDir,
|
|
353
568
|
inheritedFromSessionId: profileInfo.inheritedFromSessionId,
|
|
354
569
|
status: 'active',
|
|
355
|
-
lastAction: 'browser_open',
|
|
570
|
+
lastAction: restoreUrl && restoreUrl !== 'about:blank' ? 'browser_restore' : 'browser_open',
|
|
356
571
|
})
|
|
357
572
|
} finally {
|
|
358
573
|
if (pendingBrowserInitializations.get(sessionKey)) {
|
|
@@ -421,6 +636,105 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
421
636
|
function: fn,
|
|
422
637
|
})
|
|
423
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
|
+
|
|
424
738
|
const captureStructuredObservation = async () => {
|
|
425
739
|
const expression = `() => {
|
|
426
740
|
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
@@ -811,9 +1125,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
811
1125
|
const fields = Array.isArray(params.fields)
|
|
812
1126
|
? params.fields
|
|
813
1127
|
: (() => {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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]) => {
|
|
817
1131
|
const escapedId = String(key).replace(/[^a-zA-Z0-9_-]/g, '')
|
|
818
1132
|
const escapedAttr = String(key).replace(/["\\]/g, '\\$&')
|
|
819
1133
|
const inferredType = typeof value === 'boolean'
|
|
@@ -838,11 +1152,43 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
838
1152
|
const ref = typeof entry.ref === 'string' ? entry.ref : undefined
|
|
839
1153
|
const element = typeof entry.element === 'string' ? entry.element : undefined
|
|
840
1154
|
const fieldType = String(entry.type || 'text').toLowerCase()
|
|
841
|
-
const value = entry.value
|
|
1155
|
+
const value = entry.value ?? entry.text
|
|
842
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
|
+
}
|
|
843
1190
|
if (fieldType === 'select') {
|
|
844
|
-
|
|
845
|
-
await callMcpTool('browser_select_option', { ref, element, values })
|
|
1191
|
+
await callMcpTool('browser_select_option', { ref, element, values: selectValues })
|
|
846
1192
|
} else if (fieldType === 'checkbox' || fieldType === 'radio') {
|
|
847
1193
|
if (value === true || value === 'true' || value === 'on' || value === 'checked') {
|
|
848
1194
|
await callMcpTool('browser_click', { ref, element })
|
|
@@ -866,11 +1212,19 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
866
1212
|
}
|
|
867
1213
|
|
|
868
1214
|
const submitForm = async (params: Record<string, unknown>) => {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
+
}
|
|
874
1228
|
} else {
|
|
875
1229
|
await callBrowserEvaluate(`() => {
|
|
876
1230
|
const form = document.forms[0];
|
|
@@ -1057,8 +1411,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1057
1411
|
const completeWebTask = async (params: Record<string, unknown>) => {
|
|
1058
1412
|
const steps: string[] = []
|
|
1059
1413
|
if (typeof params.url === 'string' && params.url.trim()) {
|
|
1060
|
-
|
|
1061
|
-
|
|
1414
|
+
const navigationTarget = resolveBrowserNavigationTarget(cwd, params.url.trim())
|
|
1415
|
+
await callMcpTool('browser_navigate', { url: navigationTarget })
|
|
1416
|
+
steps.push(`navigate:${navigationTarget}`)
|
|
1062
1417
|
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
1063
1418
|
}
|
|
1064
1419
|
|
|
@@ -1073,7 +1428,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1073
1428
|
if (scroll.ok) initialPage = scroll.page
|
|
1074
1429
|
}
|
|
1075
1430
|
|
|
1076
|
-
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))) {
|
|
1077
1432
|
const filled = await performFillForm(params)
|
|
1078
1433
|
if (!filled.ok) return filled
|
|
1079
1434
|
steps.push('fill_form')
|
|
@@ -1137,7 +1492,7 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1137
1492
|
tools.push(
|
|
1138
1493
|
tool(
|
|
1139
1494
|
async (rawParams) => {
|
|
1140
|
-
const params =
|
|
1495
|
+
const params = normalizeBrowserActionParams((rawParams ?? {}) as Record<string, unknown>)
|
|
1141
1496
|
try {
|
|
1142
1497
|
const action = String(params.action || '').trim()
|
|
1143
1498
|
|
|
@@ -1175,9 +1530,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1175
1530
|
}
|
|
1176
1531
|
|
|
1177
1532
|
if (action === 'read_page') {
|
|
1178
|
-
const
|
|
1179
|
-
if (
|
|
1180
|
-
await callMcpTool('browser_navigate', { url })
|
|
1533
|
+
const target = pickBrowserTargetFromParams(params)
|
|
1534
|
+
if (target) {
|
|
1535
|
+
await callMcpTool('browser_navigate', { url: resolveBrowserNavigationTarget(cwd, target) })
|
|
1181
1536
|
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
1182
1537
|
}
|
|
1183
1538
|
return stringifyStructured(await captureStructuredObservation())
|
|
@@ -1252,6 +1607,18 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1252
1607
|
if (v !== undefined && v !== null && v !== '') args[k] = v
|
|
1253
1608
|
}
|
|
1254
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
|
+
|
|
1255
1622
|
if (action === 'tabs') {
|
|
1256
1623
|
args.action = typeof params.tabAction === 'string' ? params.tabAction : 'list'
|
|
1257
1624
|
delete args.tabAction
|
|
@@ -1264,9 +1631,13 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1264
1631
|
args.values = Array.isArray(args.option) ? args.option : [String(args.option)]
|
|
1265
1632
|
delete args.option
|
|
1266
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
|
+
}
|
|
1267
1638
|
|
|
1268
1639
|
if ((action === 'screenshot' || action === 'snapshot') && args.url) {
|
|
1269
|
-
const navUrl = args.url
|
|
1640
|
+
const navUrl = resolveBrowserNavigationTarget(cwd, String(args.url))
|
|
1270
1641
|
delete args.url
|
|
1271
1642
|
await callMcpTool('browser_navigate', { url: navUrl })
|
|
1272
1643
|
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
@@ -1288,6 +1659,14 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1288
1659
|
try { await dismissCookieBanners(callMcpTool) } catch { /* ignore */ }
|
|
1289
1660
|
}
|
|
1290
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
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1291
1670
|
let result = await callMcpTool(mcpTool, args, { saveTo: typeof params.saveTo === 'string' ? params.saveTo : undefined })
|
|
1292
1671
|
if (action === 'navigate' && result.includes('ERR_ABORTED')) {
|
|
1293
1672
|
await new Promise((r) => setTimeout(r, 1000))
|
|
@@ -1366,9 +1745,34 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
|
|
|
1366
1745
|
const cmdArgs = (normalized.args ?? normalized.arguments) as string | undefined
|
|
1367
1746
|
try {
|
|
1368
1747
|
if (!command) return 'Error: command is required.'
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
|
|
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)
|
|
1372
1776
|
if (result.status !== 0) return `Error (exit ${result.status}): ${result.stderr || result.stdout || 'unknown'}`
|
|
1373
1777
|
return truncate(result.stdout || '(no output)', MAX_OUTPUT)
|
|
1374
1778
|
} catch (err: unknown) { return `Error: ${err instanceof Error ? err.message : String(err)}` }
|