ocsmarttools 0.1.10 → 0.1.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `ocsmarttools` are documented here.
4
4
 
5
+ ## [0.1.11] - 2026-02-22
6
+
7
+ ### Changed
8
+ - Defaulted `strictRouting` to `true` for new installs/bootstrap paths.
9
+ - Added blocked-site fallback path for `web_fetch` failures:
10
+ - detect anti-bot/rate-limit/access-denied failures
11
+ - attempt browser fallback (`start` -> `open`/`navigate` -> `snapshot`)
12
+ - return native-handoff guidance for browser/skill workflows if fallback fails
13
+ - Added new config keys:
14
+ - `webFetchFallback.enabled`
15
+ - `webFetchFallback.browserTarget`
16
+ - `webFetchFallback.snapshotFormat`
17
+ - `webFetchFallback.snapshotLimit`
18
+ - Updated routing policy to explicitly allow browser/skill-guided recovery on blocked pages.
19
+
5
20
  ## [0.1.10] - 2026-02-22
6
21
 
7
22
  ### Changed
package/README.md CHANGED
@@ -16,6 +16,8 @@ OCSmartTools helps reduce latency and token cost by routing multi-step work thro
16
16
  - Built-in metrics for success/failure/timeout, latency, and estimated savings
17
17
  - Skill-compatible (including browser/Playwright-style workflows)
18
18
  - Auto-managed routing guidance for research/report/coding/data tasks (no per-prompt guardrails needed)
19
+ - Strict routing is default-on for new installs
20
+ - Auto fallback for blocked pages: `web_fetch` -> browser snapshot path
19
21
 
20
22
  ## Install
21
23
 
@@ -96,6 +98,12 @@ Example:
96
98
  - `safe`: requires sandbox and blocks control-plane dispatch
97
99
  - `strictRouting=true`: enforces plugin-managed routing guidance and keeps routing block synced
98
100
 
101
+ Web fetch fallback defaults:
102
+ - `webFetchFallback.enabled=true`
103
+ - `webFetchFallback.browserTarget=auto`
104
+ - `webFetchFallback.snapshotFormat=ai`
105
+ - `webFetchFallback.snapshotLimit=12000`
106
+
99
107
  ## Auto Trigger Intents
100
108
 
101
109
  When tools are needed, routing is auto-biased toward `tool_dispatch` / `tool_batch` for these intent families:
@@ -109,7 +117,7 @@ When tools are needed, routing is auto-biased toward `tool_dispatch` / `tool_bat
109
117
 
110
118
  Skills in OpenClaw are instruction layers, not separate execution engines. OCSmartTools can still dispatch/batch the underlying tools used by skills.
111
119
 
112
- For websites that block plain fetch, use browser-based flows via installed skills/plugins; OCSmartTools can orchestrate those tool calls with the same metrics and shaping behavior.
120
+ For websites that block plain fetch, OCSmartTools now tries browser fallback automatically. If that also fails, it returns a native-handoff recommendation so OpenClaw can continue with browser tools or skill-guided Playwright/stealth workflows.
113
121
 
114
122
  ## Safety Notes
115
123
 
@@ -19,6 +19,16 @@
19
19
  "resultSampleItems": { "type": "integer", "minimum": 1, "maximum": 50 },
20
20
  "requireSandbox": { "type": "boolean" },
21
21
  "denyControlPlane": { "type": "boolean" },
22
+ "webFetchFallback": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "properties": {
26
+ "enabled": { "type": "boolean" },
27
+ "browserTarget": { "type": "string", "enum": ["auto", "sandbox", "host", "node"] },
28
+ "snapshotFormat": { "type": "string", "enum": ["ai", "aria"] },
29
+ "snapshotLimit": { "type": "integer", "minimum": 1000, "maximum": 50000 }
30
+ }
31
+ },
22
32
  "toolSearch": {
23
33
  "type": "object",
24
34
  "additionalProperties": false,
@@ -44,6 +54,10 @@
44
54
  "resultSampleItems": { "label": "Result Sample Items", "advanced": true },
45
55
  "requireSandbox": { "label": "Require Sandbox", "advanced": true },
46
56
  "denyControlPlane": { "label": "Deny Control Plane", "advanced": true },
57
+ "webFetchFallback.enabled": { "label": "Enable Web Fetch Fallback" },
58
+ "webFetchFallback.browserTarget": { "label": "Fallback Browser Target", "advanced": true },
59
+ "webFetchFallback.snapshotFormat": { "label": "Fallback Snapshot Format", "advanced": true },
60
+ "webFetchFallback.snapshotLimit": { "label": "Fallback Snapshot Limit", "advanced": true },
47
61
  "toolSearch.enabled": { "label": "Enable Tool Search" },
48
62
  "toolSearch.defaultLimit": { "label": "Tool Search Default Limit", "advanced": true },
49
63
  "toolSearch.useLiveRegistry": { "label": "Use Live Tool Registry", "advanced": true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocsmarttools",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Provider-agnostic advanced tool orchestration plugin for OpenClaw with search, dispatch, and batching",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -32,6 +32,10 @@ const CONFIG_SPECS: Record<string, ConfigSpec> = {
32
32
  resultSampleItems: { kind: "integer", min: 1, max: 50 },
33
33
  requireSandbox: { kind: "boolean" },
34
34
  denyControlPlane: { kind: "boolean" },
35
+ "webFetchFallback.enabled": { kind: "boolean" },
36
+ "webFetchFallback.browserTarget": { kind: "enum", values: ["auto", "sandbox", "host", "node"] },
37
+ "webFetchFallback.snapshotFormat": { kind: "enum", values: ["ai", "aria"] },
38
+ "webFetchFallback.snapshotLimit": { kind: "integer", min: 1000, max: 50000 },
35
39
  "toolSearch.enabled": { kind: "boolean" },
36
40
  "toolSearch.defaultLimit": { kind: "integer", min: 1, max: 50 },
37
41
  "toolSearch.useLiveRegistry": { kind: "boolean" },
@@ -52,6 +56,10 @@ const DEFAULT_BY_KEY: Record<string, boolean | number | string> = {
52
56
  resultSampleItems: DEFAULT_SETTINGS.resultSampleItems,
53
57
  requireSandbox: DEFAULT_SETTINGS.requireSandbox,
54
58
  denyControlPlane: DEFAULT_SETTINGS.denyControlPlane,
59
+ "webFetchFallback.enabled": DEFAULT_SETTINGS.webFetchFallback.enabled,
60
+ "webFetchFallback.browserTarget": DEFAULT_SETTINGS.webFetchFallback.browserTarget,
61
+ "webFetchFallback.snapshotFormat": DEFAULT_SETTINGS.webFetchFallback.snapshotFormat,
62
+ "webFetchFallback.snapshotLimit": DEFAULT_SETTINGS.webFetchFallback.snapshotLimit,
55
63
  "toolSearch.enabled": DEFAULT_SETTINGS.toolSearch.enabled,
56
64
  "toolSearch.defaultLimit": DEFAULT_SETTINGS.toolSearch.defaultLimit,
57
65
  "toolSearch.useLiveRegistry": DEFAULT_SETTINGS.toolSearch.useLiveRegistry,
@@ -183,6 +191,8 @@ export function renderHelp(): string {
183
191
  "",
184
192
  "Quick examples:",
185
193
  "- /ocsmarttools status",
194
+ "- /ocsmarttools config set webFetchFallback.enabled true",
195
+ "- /ocsmarttools config set webFetchFallback.browserTarget auto",
186
196
  "- /ocsmarttools config set maxResultChars 120000",
187
197
  "- /ocsmarttools strict on",
188
198
  "- /ocsmarttools stats",
@@ -253,6 +263,10 @@ export function renderConfig(api: OpenClawPluginApi): string {
253
263
  `- resultSampleItems: ${s.resultSampleItems}`,
254
264
  `- requireSandbox: ${s.requireSandbox}`,
255
265
  `- denyControlPlane: ${s.denyControlPlane}`,
266
+ `- webFetchFallback.enabled: ${s.webFetchFallback.enabled}`,
267
+ `- webFetchFallback.browserTarget: ${s.webFetchFallback.browserTarget}`,
268
+ `- webFetchFallback.snapshotFormat: ${s.webFetchFallback.snapshotFormat}`,
269
+ `- webFetchFallback.snapshotLimit: ${s.webFetchFallback.snapshotLimit}`,
256
270
  `- toolSearch.enabled: ${s.toolSearch.enabled}`,
257
271
  `- toolSearch.defaultLimit: ${s.toolSearch.defaultLimit}`,
258
272
  `- toolSearch.useLiveRegistry: ${s.toolSearch.useLiveRegistry}`,
@@ -375,7 +389,7 @@ export async function applySetup(api: OpenClawPluginApi, mode: AdvToolsMode): Pr
375
389
 
376
390
  pluginCfg.enabled = true;
377
391
  pluginCfg.mode = mode;
378
- pluginCfg.strictRouting = false;
392
+ pluginCfg.strictRouting = true;
379
393
  pluginCfg.autoInjectRoutingGuide = true;
380
394
  pluginCfg.maxSteps = DEFAULT_SETTINGS.maxSteps;
381
395
  pluginCfg.maxForEach = DEFAULT_SETTINGS.maxForEach;
@@ -386,6 +400,12 @@ export async function applySetup(api: OpenClawPluginApi, mode: AdvToolsMode): Pr
386
400
  pluginCfg.resultSampleItems = DEFAULT_SETTINGS.resultSampleItems;
387
401
  pluginCfg.requireSandbox = mode === "safe";
388
402
  pluginCfg.denyControlPlane = true;
403
+ pluginCfg.webFetchFallback = {
404
+ enabled: DEFAULT_SETTINGS.webFetchFallback.enabled,
405
+ browserTarget: DEFAULT_SETTINGS.webFetchFallback.browserTarget,
406
+ snapshotFormat: DEFAULT_SETTINGS.webFetchFallback.snapshotFormat,
407
+ snapshotLimit: DEFAULT_SETTINGS.webFetchFallback.snapshotLimit,
408
+ };
389
409
  pluginCfg.toolSearch = {
390
410
  enabled: true,
391
411
  defaultLimit: DEFAULT_SETTINGS.toolSearch.defaultLimit,
@@ -401,7 +421,7 @@ export async function applySetup(api: OpenClawPluginApi, mode: AdvToolsMode): Pr
401
421
  );
402
422
 
403
423
  await writeConfig(api, next);
404
- const sync = await syncRoutingGuide(api, next, { strictRouting: false });
424
+ const sync = await syncRoutingGuide(api, next, { strictRouting: true });
405
425
  const syncLine = sync.error
406
426
  ? `- routing guide sync skipped: ${sync.error}`
407
427
  : `- routing guide ${sync.changed ? "synced" : "already up to date"} (${sync.filePath ?? "AGENTS.md"})`;
@@ -409,6 +429,7 @@ export async function applySetup(api: OpenClawPluginApi, mode: AdvToolsMode): Pr
409
429
  return [
410
430
  "OCSmartTools setup applied.",
411
431
  `- mode: ${mode}`,
432
+ "- strictRouting: true",
412
433
  "- autoInjectRoutingGuide: true",
413
434
  syncLine,
414
435
  `- ensured tools.allow includes: ${ADVTOOLS_TOOL_NAMES.join(", ")}`,
@@ -57,6 +57,42 @@ export async function autoBootstrap(api: OpenClawPluginApi): Promise<{ changed:
57
57
  setDefault("resultSampleItems", DEFAULT_SETTINGS.resultSampleItems);
58
58
  setDefault("requireSandbox", DEFAULT_SETTINGS.requireSandbox);
59
59
  setDefault("denyControlPlane", DEFAULT_SETTINGS.denyControlPlane);
60
+ if (pluginCfg.webFetchFallback === undefined) {
61
+ pluginCfg.webFetchFallback = {
62
+ enabled: DEFAULT_SETTINGS.webFetchFallback.enabled,
63
+ browserTarget: DEFAULT_SETTINGS.webFetchFallback.browserTarget,
64
+ snapshotFormat: DEFAULT_SETTINGS.webFetchFallback.snapshotFormat,
65
+ snapshotLimit: DEFAULT_SETTINGS.webFetchFallback.snapshotLimit,
66
+ };
67
+ changed = true;
68
+ notes.push("set default webFetchFallback");
69
+ } else {
70
+ const wf = asObj(pluginCfg.webFetchFallback);
71
+ if (pluginCfg.webFetchFallback !== wf) {
72
+ pluginCfg.webFetchFallback = wf;
73
+ changed = true;
74
+ }
75
+ if (wf.enabled === undefined) {
76
+ wf.enabled = DEFAULT_SETTINGS.webFetchFallback.enabled;
77
+ changed = true;
78
+ notes.push("set default webFetchFallback.enabled");
79
+ }
80
+ if (wf.browserTarget === undefined) {
81
+ wf.browserTarget = DEFAULT_SETTINGS.webFetchFallback.browserTarget;
82
+ changed = true;
83
+ notes.push("set default webFetchFallback.browserTarget");
84
+ }
85
+ if (wf.snapshotFormat === undefined) {
86
+ wf.snapshotFormat = DEFAULT_SETTINGS.webFetchFallback.snapshotFormat;
87
+ changed = true;
88
+ notes.push("set default webFetchFallback.snapshotFormat");
89
+ }
90
+ if (wf.snapshotLimit === undefined) {
91
+ wf.snapshotLimit = DEFAULT_SETTINGS.webFetchFallback.snapshotLimit;
92
+ changed = true;
93
+ notes.push("set default webFetchFallback.snapshotLimit");
94
+ }
95
+ }
60
96
  if (pluginCfg.toolSearch === undefined) {
61
97
  pluginCfg.toolSearch = {
62
98
  enabled: DEFAULT_SETTINGS.toolSearch.enabled,
@@ -16,6 +16,12 @@ export type AdvToolsSettings = {
16
16
  resultSampleItems: number;
17
17
  requireSandbox: boolean;
18
18
  denyControlPlane: boolean;
19
+ webFetchFallback: {
20
+ enabled: boolean;
21
+ browserTarget: "auto" | "sandbox" | "host" | "node";
22
+ snapshotFormat: "ai" | "aria";
23
+ snapshotLimit: number;
24
+ };
19
25
  toolSearch: {
20
26
  enabled: boolean;
21
27
  defaultLimit: number;
@@ -27,7 +33,7 @@ export type AdvToolsSettings = {
27
33
  export const DEFAULT_SETTINGS: AdvToolsSettings = {
28
34
  enabled: true,
29
35
  mode: "standard",
30
- strictRouting: false,
36
+ strictRouting: true,
31
37
  autoInjectRoutingGuide: true,
32
38
  maxSteps: 25,
33
39
  maxForEach: 20,
@@ -38,6 +44,12 @@ export const DEFAULT_SETTINGS: AdvToolsSettings = {
38
44
  resultSampleItems: 8,
39
45
  requireSandbox: false,
40
46
  denyControlPlane: true,
47
+ webFetchFallback: {
48
+ enabled: true,
49
+ browserTarget: "auto",
50
+ snapshotFormat: "ai",
51
+ snapshotLimit: 12000,
52
+ },
41
53
  toolSearch: {
42
54
  enabled: true,
43
55
  defaultLimit: 8,
@@ -67,12 +79,33 @@ function asMode(value: unknown, fallback: AdvToolsMode): AdvToolsMode {
67
79
  return value === "safe" || value === "standard" ? value : fallback;
68
80
  }
69
81
 
82
+ function asBrowserTarget(
83
+ value: unknown,
84
+ fallback: AdvToolsSettings["webFetchFallback"]["browserTarget"],
85
+ ): AdvToolsSettings["webFetchFallback"]["browserTarget"] {
86
+ if (value === "auto" || value === "sandbox" || value === "host" || value === "node") {
87
+ return value;
88
+ }
89
+ return fallback;
90
+ }
91
+
92
+ function asSnapshotFormat(
93
+ value: unknown,
94
+ fallback: AdvToolsSettings["webFetchFallback"]["snapshotFormat"],
95
+ ): AdvToolsSettings["webFetchFallback"]["snapshotFormat"] {
96
+ if (value === "ai" || value === "aria") {
97
+ return value;
98
+ }
99
+ return fallback;
100
+ }
101
+
70
102
  export function resolveSettings(api: OpenClawPluginApi, cfg: OpenClawConfig = api.config): AdvToolsSettings {
71
103
  const plugins = asObj((cfg as Record<string, unknown>).plugins);
72
104
  const entries = asObj(plugins.entries);
73
105
  const entry = asObj(entries[api.id]);
74
106
  const pluginCfg = asObj(entry.config ?? api.pluginConfig);
75
107
  const ts = asObj(pluginCfg.toolSearch);
108
+ const wf = asObj(pluginCfg.webFetchFallback);
76
109
 
77
110
  const strictRouting = asBool(pluginCfg.strictRouting, DEFAULT_SETTINGS.strictRouting);
78
111
  const autoInjectRoutingGuide = strictRouting
@@ -103,6 +136,23 @@ export function resolveSettings(api: OpenClawPluginApi, cfg: OpenClawConfig = ap
103
136
  ),
104
137
  requireSandbox: asBool(pluginCfg.requireSandbox, DEFAULT_SETTINGS.requireSandbox),
105
138
  denyControlPlane: asBool(pluginCfg.denyControlPlane, DEFAULT_SETTINGS.denyControlPlane),
139
+ webFetchFallback: {
140
+ enabled: asBool(wf.enabled, DEFAULT_SETTINGS.webFetchFallback.enabled),
141
+ browserTarget: asBrowserTarget(
142
+ wf.browserTarget,
143
+ DEFAULT_SETTINGS.webFetchFallback.browserTarget,
144
+ ),
145
+ snapshotFormat: asSnapshotFormat(
146
+ wf.snapshotFormat,
147
+ DEFAULT_SETTINGS.webFetchFallback.snapshotFormat,
148
+ ),
149
+ snapshotLimit: asInt(
150
+ wf.snapshotLimit,
151
+ DEFAULT_SETTINGS.webFetchFallback.snapshotLimit,
152
+ 1000,
153
+ 50000,
154
+ ),
155
+ },
106
156
  toolSearch: {
107
157
  enabled: asBool(ts.enabled, DEFAULT_SETTINGS.toolSearch.enabled),
108
158
  defaultLimit: asInt(ts.defaultLimit, DEFAULT_SETTINGS.toolSearch.defaultLimit, 1, 50),
@@ -32,6 +32,7 @@ function buildRoutingBlock(strictRouting: boolean): string {
32
32
  "7. Do not present citations/sources unless they came from successful tool calls in the current run.",
33
33
  "8. If required retrieval fails, return a clear partial/failure report instead of fabricated evidence.",
34
34
  "9. Treat trigger intents below as routing-required unless the request is clearly a no-tool task.",
35
+ "10. If `web_fetch` is blocked (anti-bot/captcha), prefer browser fallback; if fallback also fails, allow native browser/skill-guided flows (for example Playwright stealth skills).",
35
36
  "",
36
37
  "Common large/noisy tools: `web_fetch`, `read` (large files), `exec`, `process`, `browser`, `nodes`.",
37
38
  "",
@@ -51,6 +52,7 @@ function buildRoutingBlock(strictRouting: boolean): string {
51
52
  "7. Do not include citations/sources that were not retrieved by tools in the current run.",
52
53
  "8. If retrieval fails, explicitly report the gap.",
53
54
  "9. Treat trigger intents below as strong routing signals unless the task is clearly no-tool.",
55
+ "10. For blocked `web_fetch` pages, allow browser/skill-guided fallback paths.",
54
56
  "",
55
57
  "Common large/noisy tools: `web_fetch`, `read` (large files), `exec`, `process`, `browser`, `nodes`.",
56
58
  "",
@@ -0,0 +1,181 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import type { AdvToolsSettings } from "./plugin-config.js";
3
+ import type { InvokeResult } from "./invoke.js";
4
+ import { invokeToolViaGateway } from "./invoke.js";
5
+
6
+ const BLOCKED_STATUSES = new Set([401, 403, 406, 409, 410, 412, 418, 425, 429, 451, 503]);
7
+ const BLOCKED_HINTS = [
8
+ "access denied",
9
+ "forbidden",
10
+ "blocked",
11
+ "bot",
12
+ "captcha",
13
+ "cloudflare",
14
+ "challenge",
15
+ "javascript required",
16
+ "enable javascript",
17
+ "rate limit",
18
+ "too many requests",
19
+ ];
20
+
21
+ function asObj(value: unknown): Record<string, unknown> {
22
+ return value && typeof value === "object" && !Array.isArray(value)
23
+ ? (value as Record<string, unknown>)
24
+ : {};
25
+ }
26
+
27
+ function toLowerBlob(value: unknown): string {
28
+ try {
29
+ return JSON.stringify(value).toLowerCase();
30
+ } catch {
31
+ return String(value ?? "").toLowerCase();
32
+ }
33
+ }
34
+
35
+ function isLikelyBlockedFailure(result: InvokeResult): boolean {
36
+ if (BLOCKED_STATUSES.has(result.status)) {
37
+ return true;
38
+ }
39
+ const msg = `${result.error?.message ?? ""}\n${toLowerBlob(result.error?.details ?? "")}`.toLowerCase();
40
+ return BLOCKED_HINTS.some((hint) => msg.includes(hint));
41
+ }
42
+
43
+ function withBrowserTarget(
44
+ args: Record<string, unknown>,
45
+ target: AdvToolsSettings["webFetchFallback"]["browserTarget"],
46
+ ): Record<string, unknown> {
47
+ if (target === "auto") {
48
+ return args;
49
+ }
50
+ return { ...args, target };
51
+ }
52
+
53
+ type BrowserFallbackAttempt = {
54
+ attempted: true;
55
+ ok: boolean;
56
+ status: number;
57
+ latencyMs: number;
58
+ error?: InvokeResult["error"];
59
+ result?: unknown;
60
+ };
61
+
62
+ export type WebFetchFallbackResult =
63
+ | { attempted: false }
64
+ | ({
65
+ strategy: "web_fetch_browser_snapshot_v1";
66
+ primaryStatus: number;
67
+ url: string;
68
+ nativeHandoffRecommended: boolean;
69
+ nativeHandoffReason: string;
70
+ } & BrowserFallbackAttempt);
71
+
72
+ type AttemptParams = {
73
+ cfg: OpenClawConfig;
74
+ settings: AdvToolsSettings;
75
+ toolName: string;
76
+ args: Record<string, unknown>;
77
+ primary: InvokeResult;
78
+ sessionKey?: string;
79
+ channel?: string;
80
+ accountId?: string;
81
+ timeoutMs?: number;
82
+ signal?: AbortSignal;
83
+ };
84
+
85
+ export async function maybeAttemptWebFetchFallback(params: AttemptParams): Promise<WebFetchFallbackResult> {
86
+ const { settings, toolName, args, primary } = params;
87
+ if (!settings.webFetchFallback.enabled || toolName !== "web_fetch" || primary.ok) {
88
+ return { attempted: false };
89
+ }
90
+
91
+ const url = typeof args.url === "string" ? args.url.trim() : "";
92
+ if (!url || !isLikelyBlockedFailure(primary)) {
93
+ return { attempted: false };
94
+ }
95
+
96
+ const startedAt = Date.now();
97
+ const browserTarget = settings.webFetchFallback.browserTarget;
98
+ const invokeCommon = {
99
+ sessionKey: params.sessionKey,
100
+ channel: params.channel,
101
+ accountId: params.accountId,
102
+ timeoutMs: params.timeoutMs,
103
+ signal: params.signal,
104
+ };
105
+
106
+ // Best-effort start; failures are tolerated because browser may already be running.
107
+ await invokeToolViaGateway(params.cfg, {
108
+ tool: "browser",
109
+ action: "start",
110
+ args: withBrowserTarget({}, browserTarget),
111
+ ...invokeCommon,
112
+ });
113
+
114
+ const open = await invokeToolViaGateway(params.cfg, {
115
+ tool: "browser",
116
+ action: "open",
117
+ args: withBrowserTarget({ url }, browserTarget),
118
+ ...invokeCommon,
119
+ });
120
+
121
+ if (!open.ok) {
122
+ // Retry via navigate in case the browser is up but open action is not supported by runtime/profile.
123
+ await invokeToolViaGateway(params.cfg, {
124
+ tool: "browser",
125
+ action: "navigate",
126
+ args: withBrowserTarget({ url }, browserTarget),
127
+ ...invokeCommon,
128
+ });
129
+ }
130
+
131
+ const snapshot = await invokeToolViaGateway(params.cfg, {
132
+ tool: "browser",
133
+ action: "snapshot",
134
+ args: withBrowserTarget(
135
+ {
136
+ format: settings.webFetchFallback.snapshotFormat,
137
+ limit: settings.webFetchFallback.snapshotLimit,
138
+ },
139
+ browserTarget,
140
+ ),
141
+ ...invokeCommon,
142
+ });
143
+
144
+ if (!snapshot.ok) {
145
+ return {
146
+ attempted: true,
147
+ ok: false,
148
+ status: snapshot.status,
149
+ latencyMs: Date.now() - startedAt,
150
+ strategy: "web_fetch_browser_snapshot_v1",
151
+ primaryStatus: primary.status,
152
+ url,
153
+ error: snapshot.error,
154
+ nativeHandoffRecommended: true,
155
+ nativeHandoffReason:
156
+ "Browser fallback failed. Allow native browser calls or skill-guided Playwright/stealth workflow.",
157
+ };
158
+ }
159
+
160
+ const snapshotObj = asObj(snapshot.result);
161
+ return {
162
+ attempted: true,
163
+ ok: true,
164
+ status: snapshot.status,
165
+ latencyMs: Date.now() - startedAt,
166
+ strategy: "web_fetch_browser_snapshot_v1",
167
+ primaryStatus: primary.status,
168
+ url,
169
+ nativeHandoffRecommended: false,
170
+ nativeHandoffReason: "",
171
+ result: {
172
+ url,
173
+ fallback: {
174
+ strategy: "web_fetch_blocked_to_browser_snapshot",
175
+ primaryStatus: primary.status,
176
+ primaryError: primary.error,
177
+ },
178
+ browserSnapshot: snapshotObj,
179
+ },
180
+ };
181
+ }
@@ -1,4 +1,4 @@
1
- import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { AnyAgentTool, OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { jsonResult } from "openclaw/plugin-sdk";
3
3
  import { resolveSettings } from "../lib/plugin-config.js";
4
4
  import { invokeToolViaGateway } from "../lib/invoke.js";
@@ -6,6 +6,7 @@ import type { ResultStore } from "../lib/result-store.js";
6
6
  import { shapeToolResult } from "../lib/result-shaper.js";
7
7
  import { resolveValueTemplates } from "../lib/refs.js";
8
8
  import type { MetricsStore } from "../lib/metrics-store.js";
9
+ import { maybeAttemptWebFetchFallback } from "../lib/web-fetch-fallback.js";
9
10
 
10
11
  const CONTROL_PLANE = new Set(["gateway", "cron"]);
11
12
 
@@ -82,7 +83,7 @@ function parseSteps(raw: unknown): BatchStep[] {
82
83
 
83
84
  async function runCall(params: {
84
85
  api: OpenClawPluginApi;
85
- config: Record<string, unknown>;
86
+ config: OpenClawConfig;
86
87
  settings: ReturnType<typeof resolveSettings>;
87
88
  step: BatchStepCall;
88
89
  context: Record<string, unknown>;
@@ -159,6 +160,48 @@ async function runCall(params: {
159
160
  outcome: invoke.error?.type === "timeout" ? "timeout" : "failure",
160
161
  latencyMs: invoke.latencyMs,
161
162
  });
163
+
164
+ const fallback = await maybeAttemptWebFetchFallback({
165
+ cfg: config,
166
+ settings,
167
+ toolName,
168
+ args,
169
+ primary: invoke,
170
+ sessionKey,
171
+ channel,
172
+ accountId,
173
+ timeoutMs,
174
+ signal: params.signal,
175
+ });
176
+ if (fallback.attempted) {
177
+ params.metrics?.record({
178
+ tool: "browser",
179
+ outcome: fallback.ok ? "success" : "failure",
180
+ latencyMs: fallback.latencyMs,
181
+ });
182
+ if (fallback.ok) {
183
+ const shaped = shapeToolResult({
184
+ toolName: "browser",
185
+ value: fallback.result,
186
+ settings,
187
+ store: params.store,
188
+ });
189
+ return {
190
+ ok: true,
191
+ status: fallback.status,
192
+ tool: toolName,
193
+ fallbackUsed: "browser",
194
+ result: shaped,
195
+ };
196
+ }
197
+ return {
198
+ ok: false,
199
+ status: invoke.status,
200
+ tool: toolName,
201
+ error: invoke.error,
202
+ fallback,
203
+ };
204
+ }
162
205
  return {
163
206
  ok: false,
164
207
  status: invoke.status,
@@ -5,6 +5,7 @@ import { invokeToolViaGateway } from "../lib/invoke.js";
5
5
  import type { ResultStore } from "../lib/result-store.js";
6
6
  import { shapeToolResult } from "../lib/result-shaper.js";
7
7
  import type { MetricsStore } from "../lib/metrics-store.js";
8
+ import { maybeAttemptWebFetchFallback } from "../lib/web-fetch-fallback.js";
8
9
 
9
10
  const CONTROL_PLANE = new Set(["gateway", "cron"]);
10
11
  const INTERNAL_LOOP_TOOLS = new Set(["tool_dispatch", "tool_batch"]);
@@ -122,11 +123,50 @@ export function createToolDispatchTool(
122
123
  outcome: result.error?.type === "timeout" ? "timeout" : "failure",
123
124
  latencyMs: result.latencyMs,
124
125
  });
125
- return jsonResult({
126
- ok: false,
127
- status: result.status,
128
- error: result.error,
126
+
127
+ const fallback = await maybeAttemptWebFetchFallback({
128
+ cfg: loaded,
129
+ settings,
130
+ toolName,
131
+ args: safeArgs as Record<string, unknown>,
132
+ primary: result,
133
+ sessionKey,
134
+ channel,
135
+ accountId,
136
+ timeoutMs: requestedTimeoutMs,
137
+ signal: signal as AbortSignal | undefined,
129
138
  });
139
+
140
+ if (fallback.attempted) {
141
+ options?.metrics?.record({
142
+ tool: "browser",
143
+ outcome: fallback.ok ? "success" : "failure",
144
+ latencyMs: fallback.latencyMs,
145
+ });
146
+ if (fallback.ok) {
147
+ const shaped = shapeToolResult({
148
+ toolName: "browser",
149
+ value: fallback.result,
150
+ settings,
151
+ store: options?.store,
152
+ });
153
+ return jsonResult({
154
+ ok: true,
155
+ status: fallback.status,
156
+ tool: toolName,
157
+ fallbackUsed: "browser",
158
+ result: shaped,
159
+ });
160
+ }
161
+ return jsonResult({
162
+ ok: false,
163
+ status: result.status,
164
+ error: result.error,
165
+ fallback,
166
+ });
167
+ }
168
+
169
+ return jsonResult({ ok: false, status: result.status, error: result.error });
130
170
  }
131
171
 
132
172
  const shaped = shapeToolResult({