web-agent-bridge 3.0.0 → 3.3.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/LICENSE +72 -21
- package/README.ar.md +1286 -1073
- package/README.md +1764 -1535
- package/bin/agent-runner.js +474 -474
- package/bin/cli.js +237 -138
- package/bin/wab.js +80 -80
- package/examples/bidi-agent.js +119 -119
- package/examples/cross-site-agent.js +91 -91
- package/examples/mcp-agent.js +94 -94
- package/examples/next-app-router/README.md +44 -44
- package/examples/puppeteer-agent.js +108 -108
- package/examples/saas-dashboard/README.md +55 -55
- package/examples/shopify-hydrogen/README.md +74 -74
- package/examples/vision-agent.js +171 -171
- package/examples/wordpress-elementor/README.md +77 -77
- package/package.json +17 -3
- package/public/.well-known/agent-tools.json +180 -180
- package/public/.well-known/ai-assets.json +59 -59
- package/public/.well-known/ai-plugin.json +28 -0
- package/public/.well-known/security.txt +8 -0
- package/public/agent-workspace.html +349 -347
- package/public/ai.html +198 -196
- package/public/api.html +413 -0
- package/public/browser.html +486 -484
- package/public/commander-dashboard.html +243 -243
- package/public/cookies.html +210 -208
- package/public/css/agent-workspace.css +1713 -1713
- package/public/css/premium.css +317 -317
- package/public/css/styles.css +1235 -1235
- package/public/dashboard.html +706 -704
- package/public/demo.html +1770 -1
- package/public/dns.html +507 -0
- package/public/docs.html +587 -585
- package/public/feed.xml +89 -89
- package/public/growth.html +463 -0
- package/public/index.html +341 -9
- package/public/integrations.html +556 -0
- package/public/js/agent-workspace.js +1740 -1740
- package/public/js/auth-nav.js +31 -31
- package/public/js/auth-redirect.js +12 -12
- package/public/js/cookie-consent.js +56 -56
- package/public/js/wab-demo-page.js +721 -721
- package/public/js/ws-client.js +74 -74
- package/public/llms-full.txt +360 -309
- package/public/llms.txt +125 -86
- package/public/login.html +85 -83
- package/public/mesh-dashboard.html +328 -328
- package/public/openapi.json +580 -580
- package/public/phone-shield.html +281 -0
- package/public/premium-dashboard.html +2489 -2487
- package/public/premium.html +793 -791
- package/public/privacy.html +297 -295
- package/public/register.html +105 -103
- package/public/robots.txt +87 -87
- package/public/script/wab-consent.d.ts +36 -36
- package/public/script/wab-consent.js +104 -104
- package/public/script/wab-schema.js +131 -131
- package/public/script/wab.d.ts +108 -108
- package/public/script/wab.min.js +580 -580
- package/public/security.txt +8 -0
- package/public/terms.html +256 -254
- package/script/ai-agent-bridge.js +1754 -1754
- package/sdk/README.md +99 -99
- package/sdk/agent-mesh.js +449 -449
- package/sdk/commander.js +262 -262
- package/sdk/index.d.ts +464 -464
- package/sdk/index.js +18 -1
- package/sdk/multi-agent.js +318 -318
- package/sdk/package.json +12 -1
- package/sdk/safety-shield.js +219 -0
- package/sdk/schema-discovery.js +83 -83
- package/server/adapters/index.js +520 -520
- package/server/config/plans.js +367 -367
- package/server/config/secrets.js +102 -102
- package/server/control-plane/index.js +301 -301
- package/server/data-plane/index.js +354 -354
- package/server/index.js +175 -19
- package/server/llm/index.js +404 -404
- package/server/middleware/adminAuth.js +35 -35
- package/server/middleware/auth.js +50 -50
- package/server/middleware/featureGate.js +88 -88
- package/server/middleware/rateLimits.js +100 -100
- package/server/middleware/sensitiveAction.js +157 -0
- package/server/migrations/001_add_analytics_indexes.sql +7 -7
- package/server/migrations/002_premium_features.sql +418 -418
- package/server/migrations/003_ads_integer_cents.sql +33 -33
- package/server/migrations/004_agent_os.sql +158 -158
- package/server/migrations/005_marketplace_metering.sql +126 -126
- package/server/models/adapters/index.js +33 -33
- package/server/models/adapters/mysql.js +183 -183
- package/server/models/adapters/postgresql.js +172 -172
- package/server/models/adapters/sqlite.js +7 -7
- package/server/models/db.js +681 -681
- package/server/observability/failure-analysis.js +337 -337
- package/server/observability/index.js +394 -394
- package/server/protocol/capabilities.js +223 -223
- package/server/protocol/index.js +243 -243
- package/server/protocol/schema.js +584 -584
- package/server/registry/certification.js +271 -271
- package/server/registry/index.js +326 -326
- package/server/routes/admin-premium.js +671 -671
- package/server/routes/admin.js +261 -261
- package/server/routes/ads.js +130 -130
- package/server/routes/agent-workspace.js +540 -378
- package/server/routes/api.js +150 -150
- package/server/routes/auth.js +71 -71
- package/server/routes/billing.js +45 -45
- package/server/routes/commander.js +316 -316
- package/server/routes/demo-showcase.js +332 -0
- package/server/routes/demo-store.js +154 -0
- package/server/routes/discovery.js +417 -406
- package/server/routes/gateway.js +173 -0
- package/server/routes/license.js +251 -240
- package/server/routes/mesh.js +469 -469
- package/server/routes/noscript.js +543 -543
- package/server/routes/premium-v2.js +686 -686
- package/server/routes/premium.js +724 -724
- package/server/routes/runtime.js +2148 -2147
- package/server/routes/sovereign.js +465 -385
- package/server/routes/universal.js +200 -177
- package/server/routes/wab-api.js +850 -491
- package/server/runtime/container-worker.js +111 -111
- package/server/runtime/container.js +448 -448
- package/server/runtime/distributed-worker.js +362 -362
- package/server/runtime/event-bus.js +210 -210
- package/server/runtime/index.js +253 -253
- package/server/runtime/queue.js +599 -599
- package/server/runtime/replay.js +666 -666
- package/server/runtime/sandbox.js +266 -266
- package/server/runtime/scheduler.js +534 -534
- package/server/runtime/session-engine.js +293 -293
- package/server/runtime/state-manager.js +188 -188
- package/server/security/cross-site-redactor.js +196 -0
- package/server/security/dry-run.js +180 -0
- package/server/security/human-gate-rate-limit.js +147 -0
- package/server/security/human-gate-transports.js +178 -0
- package/server/security/human-gate.js +281 -0
- package/server/security/index.js +368 -368
- package/server/security/intent-engine.js +245 -0
- package/server/security/reward-guard.js +171 -0
- package/server/security/rollback-store.js +239 -0
- package/server/security/token-scope.js +404 -0
- package/server/security/url-policy.js +139 -0
- package/server/services/agent-chat.js +506 -506
- package/server/services/agent-learning.js +601 -575
- package/server/services/agent-memory.js +625 -625
- package/server/services/agent-mesh.js +555 -539
- package/server/services/agent-symphony.js +717 -717
- package/server/services/agent-tasks.js +1807 -1807
- package/server/services/api-key-engine.js +292 -0
- package/server/services/cluster.js +894 -894
- package/server/services/commander.js +738 -738
- package/server/services/edge-compute.js +440 -440
- package/server/services/email.js +204 -204
- package/server/services/hosted-runtime.js +205 -205
- package/server/services/lfd.js +635 -616
- package/server/services/local-ai.js +389 -389
- package/server/services/marketplace.js +270 -270
- package/server/services/metering.js +182 -182
- package/server/services/modules/affiliate-intelligence.js +93 -0
- package/server/services/modules/agent-firewall.js +90 -0
- package/server/services/modules/bounty.js +89 -0
- package/server/services/modules/collective-bargaining.js +92 -0
- package/server/services/modules/dark-pattern.js +66 -0
- package/server/services/modules/gov-intelligence.js +45 -0
- package/server/services/modules/neural.js +55 -0
- package/server/services/modules/notary.js +49 -0
- package/server/services/modules/price-time-machine.js +86 -0
- package/server/services/modules/protocol.js +104 -0
- package/server/services/negotiation.js +439 -439
- package/server/services/plugins.js +771 -771
- package/server/services/premium.js +1 -1
- package/server/services/price-intelligence.js +566 -565
- package/server/services/price-shield.js +1137 -1137
- package/server/services/reputation.js +465 -465
- package/server/services/search-engine.js +357 -357
- package/server/services/security.js +513 -513
- package/server/services/self-healing.js +843 -843
- package/server/services/sovereign-shield.js +542 -0
- package/server/services/stripe.js +192 -192
- package/server/services/swarm.js +788 -788
- package/server/services/universal-scraper.js +662 -661
- package/server/services/verification.js +481 -481
- package/server/services/vision.js +1163 -1163
- package/server/utils/cache.js +125 -125
- package/server/utils/migrate.js +81 -81
- package/server/utils/safe-fetch.js +228 -0
- package/server/utils/secureFields.js +50 -50
- package/server/ws.js +161 -161
- package/templates/artisan-marketplace.yaml +104 -104
- package/templates/book-price-scout.yaml +98 -98
- package/templates/electronics-price-tracker.yaml +108 -108
- package/templates/flight-deal-hunter.yaml +113 -113
- package/templates/freelancer-direct.yaml +116 -116
- package/templates/grocery-price-compare.yaml +93 -93
- package/templates/hotel-direct-booking.yaml +113 -113
- package/templates/local-services.yaml +98 -98
- package/templates/olive-oil-tunisia.yaml +88 -88
- package/templates/organic-farm-fresh.yaml +101 -101
- package/templates/restaurant-direct.yaml +97 -97
- package/server/services/fairness-engine.js +0 -409
- package/server/services/fairness.js +0 -420
package/sdk/package.json
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web-agent-bridge-sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "SDK for building AI agents that interact with Web Agent Bridge (WAB)",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"license": "MIT",
|
|
8
|
+
"author": "Web Agent Bridge <dev@webagentbridge.com>",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/abokenan444/web-agent-bridge.git",
|
|
12
|
+
"directory": "sdk"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://www.webagentbridge.com",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/abokenan444/web-agent-bridge/issues"
|
|
17
|
+
},
|
|
7
18
|
"keywords": [
|
|
8
19
|
"wab",
|
|
9
20
|
"ai-agent",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAB Safety-Shield Client Helper (SPEC §8.10–§8.13)
|
|
3
|
+
*
|
|
4
|
+
* Wraps the 2-phase dry-run + human-gate protocol for HTTP API consumers
|
|
5
|
+
* so agents don't have to reimplement plan_id / confirmation_id juggling.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { SafetyShieldClient } = require('@webagentbridge/sdk/safety-shield');
|
|
9
|
+
* const client = new SafetyShieldClient({
|
|
10
|
+
* baseUrl: 'https://webagentbridge.com',
|
|
11
|
+
* sessionToken: '<bearer-token>',
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Single call — handles dry-run automatically, returns plan for review:
|
|
15
|
+
* const plan = await client.dryRun('deleteUser', { id: 42 });
|
|
16
|
+
* console.log(plan.simulation.summary);
|
|
17
|
+
*
|
|
18
|
+
* // Confirm with the plan_id (and code if human-gate engaged):
|
|
19
|
+
* const result = await client.confirmAction(plan, { code: '123456' });
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const DEFAULT_HEADERS = { 'Content-Type': 'application/json' };
|
|
25
|
+
|
|
26
|
+
class SafetyShieldClient {
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.baseUrl — e.g. 'https://webagentbridge.com'
|
|
30
|
+
* @param {string} opts.sessionToken — Bearer token from /sessions
|
|
31
|
+
* @param {string} [opts.actionsPath='/api/wab/actions'] — endpoint root
|
|
32
|
+
* @param {string} [opts.humanGatePath='/api/wab/human-gate'] — endpoint root
|
|
33
|
+
* @param {function} [opts.fetchImpl=globalThis.fetch]
|
|
34
|
+
*/
|
|
35
|
+
constructor(opts = {}) {
|
|
36
|
+
if (!opts.baseUrl) throw new Error('SafetyShieldClient: baseUrl required');
|
|
37
|
+
if (!opts.sessionToken) throw new Error('SafetyShieldClient: sessionToken required');
|
|
38
|
+
this.baseUrl = String(opts.baseUrl).replace(/\/$/, '');
|
|
39
|
+
this.sessionToken = opts.sessionToken;
|
|
40
|
+
this.actionsPath = opts.actionsPath || '/api/wab/actions';
|
|
41
|
+
this.humanGatePath = opts.humanGatePath || '/api/wab/human-gate';
|
|
42
|
+
this.fetch = opts.fetchImpl || globalThis.fetch;
|
|
43
|
+
if (typeof this.fetch !== 'function') {
|
|
44
|
+
throw new Error('SafetyShieldClient: no fetch implementation available (Node 18+ or pass fetchImpl)');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_headers() {
|
|
49
|
+
return {
|
|
50
|
+
...DEFAULT_HEADERS,
|
|
51
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async _post(path, body) {
|
|
56
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: this._headers(),
|
|
59
|
+
body: JSON.stringify(body || {}),
|
|
60
|
+
});
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
let json = null;
|
|
63
|
+
try { json = text ? JSON.parse(text) : null; } catch { /* leave null */ }
|
|
64
|
+
return { status: res.status, ok: res.ok, body: json, raw: text };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async _get(path) {
|
|
68
|
+
const res = await this.fetch(`${this.baseUrl}${path}`, { headers: this._headers() });
|
|
69
|
+
const text = await res.text();
|
|
70
|
+
let json = null;
|
|
71
|
+
try { json = text ? JSON.parse(text) : null; } catch { /* leave null */ }
|
|
72
|
+
return { status: res.status, ok: res.ok, body: json };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Phase 1: Submit a dry-run for an action and return the plan envelope.
|
|
77
|
+
* Throws if the server returns an error other than a successful plan.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} actionName
|
|
80
|
+
* @param {object} [params]
|
|
81
|
+
* @returns {Promise<{plan_id:string, simulation:object, expires_at:string, raw:object}>}
|
|
82
|
+
*/
|
|
83
|
+
async dryRun(actionName, params = {}) {
|
|
84
|
+
const r = await this._post(`${this.actionsPath}/${encodeURIComponent(actionName)}`, {
|
|
85
|
+
params,
|
|
86
|
+
dry_run: true,
|
|
87
|
+
});
|
|
88
|
+
if (!r.ok) {
|
|
89
|
+
throw shieldError('dry_run_failed', r);
|
|
90
|
+
}
|
|
91
|
+
const result = (r.body && (r.body.result || r.body)) || {};
|
|
92
|
+
if (!result.plan_id) {
|
|
93
|
+
throw shieldError('dry_run_no_plan', r);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
action: actionName,
|
|
97
|
+
params,
|
|
98
|
+
plan_id: result.plan_id,
|
|
99
|
+
simulation: result.simulation || null,
|
|
100
|
+
expires_at: result.expires_at || null,
|
|
101
|
+
raw: r.body,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Phase 2: Execute the action, automatically handling the human-gate
|
|
107
|
+
* loop if the server returns HUMAN_GATE_REQUIRED.
|
|
108
|
+
*
|
|
109
|
+
* If a 6-digit `code` is supplied AND a challenge is issued, the helper
|
|
110
|
+
* will call /human-gate/approve and then retry execution. If no code is
|
|
111
|
+
* supplied and a challenge is issued, the helper returns a `pending`
|
|
112
|
+
* envelope so the caller can prompt the human, then resume by calling
|
|
113
|
+
* `confirmAction(plan, { code })` again.
|
|
114
|
+
*
|
|
115
|
+
* @param {{action:string, params?:object, plan_id:string}} plan — from dryRun()
|
|
116
|
+
* @param {object} [opts]
|
|
117
|
+
* @param {string} [opts.code] — 6-digit human-gate code
|
|
118
|
+
* @param {string} [opts.confirmation_id] — pre-existing approval id
|
|
119
|
+
* @returns {Promise<object>}
|
|
120
|
+
*/
|
|
121
|
+
async confirmAction(plan, opts = {}) {
|
|
122
|
+
if (!plan || !plan.action || !plan.plan_id) {
|
|
123
|
+
throw new Error('confirmAction: plan must include {action, plan_id}');
|
|
124
|
+
}
|
|
125
|
+
const body = {
|
|
126
|
+
params: plan.params || {},
|
|
127
|
+
dry_run: false,
|
|
128
|
+
plan_id: plan.plan_id,
|
|
129
|
+
};
|
|
130
|
+
if (opts.confirmation_id) body.confirmation_id = opts.confirmation_id;
|
|
131
|
+
|
|
132
|
+
const r = await this._post(
|
|
133
|
+
`${this.actionsPath}/${encodeURIComponent(plan.action)}`,
|
|
134
|
+
body
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const code = r.body && r.body.error && r.body.error.code;
|
|
138
|
+
|
|
139
|
+
// Happy path — HTTP 2xx with no error envelope.
|
|
140
|
+
if (r.ok && !code) return r.body;
|
|
141
|
+
|
|
142
|
+
// Human-gate flow
|
|
143
|
+
if (code === 'HUMAN_GATE_REQUIRED' && r.status === 202) {
|
|
144
|
+
const err = r.body.error || {};
|
|
145
|
+
const challengeId = err.challenge_id || err.details?.challenge_id;
|
|
146
|
+
if (!challengeId) throw shieldError('human_gate_malformed', r);
|
|
147
|
+
|
|
148
|
+
// No code provided — bubble up so caller can prompt human.
|
|
149
|
+
if (!opts.code) {
|
|
150
|
+
return {
|
|
151
|
+
status: 'pending_human_gate',
|
|
152
|
+
challenge_id: challengeId,
|
|
153
|
+
expires_at: err.expires_at || err.details?.expires_at || null,
|
|
154
|
+
dispatched_to: err.dispatched_to || err.details?.dispatched_to || null,
|
|
155
|
+
plan,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Code supplied — approve and retry.
|
|
160
|
+
const approve = await this._post(`${this.humanGatePath}/approve`, {
|
|
161
|
+
challenge_id: challengeId,
|
|
162
|
+
code: String(opts.code),
|
|
163
|
+
});
|
|
164
|
+
if (!approve.ok) {
|
|
165
|
+
throw shieldError('human_gate_approve_failed', approve);
|
|
166
|
+
}
|
|
167
|
+
const confirmationId =
|
|
168
|
+
approve.body?.result?.confirmation_id ||
|
|
169
|
+
approve.body?.confirmation_id;
|
|
170
|
+
if (!confirmationId) throw shieldError('human_gate_no_confirmation', approve);
|
|
171
|
+
return this.confirmAction(plan, { confirmation_id: confirmationId });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (code === 'HUMAN_GATE_PENDING') {
|
|
175
|
+
// Retry by polling status.
|
|
176
|
+
return {
|
|
177
|
+
status: 'pending_human_gate',
|
|
178
|
+
challenge_id: opts.confirmation_id,
|
|
179
|
+
plan,
|
|
180
|
+
message: r.body.error.message,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw shieldError(code || 'execute_failed', r);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Convenience: dry-run + confirm in one call.
|
|
189
|
+
* If a human-gate is required and no code is passed, returns the pending
|
|
190
|
+
* envelope. Caller resumes by calling confirmAction(envelope.plan, {code}).
|
|
191
|
+
*/
|
|
192
|
+
async safeExecute(actionName, params, opts = {}) {
|
|
193
|
+
const plan = await this.dryRun(actionName, params);
|
|
194
|
+
return this.confirmAction(plan, opts);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Poll a human-gate challenge status (rarely needed — confirmAction
|
|
199
|
+
* handles the round-trip). Returns the raw status envelope.
|
|
200
|
+
*/
|
|
201
|
+
async humanGateStatus(challengeId) {
|
|
202
|
+
const r = await this._get(
|
|
203
|
+
`${this.humanGatePath}/${encodeURIComponent(challengeId)}/status`
|
|
204
|
+
);
|
|
205
|
+
return r.body;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function shieldError(code, response) {
|
|
210
|
+
const msg = response?.body?.error?.message ||
|
|
211
|
+
`WAB safety-shield error: ${code} (HTTP ${response?.status})`;
|
|
212
|
+
const err = new Error(msg);
|
|
213
|
+
err.code = code;
|
|
214
|
+
err.status = response?.status;
|
|
215
|
+
err.response = response?.body;
|
|
216
|
+
return err;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { SafetyShieldClient };
|
package/sdk/schema-discovery.js
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-side / Node: extract schema.org Product nodes from HTML (JSON-LD blocks).
|
|
3
|
-
* No extra dependencies — regex-based script extraction (same semantics as browser WABSchema).
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* const { extractProductsFromHtml, suggestWabActionsFromProducts } = require('./schema-discovery');
|
|
7
|
-
* const products = extractProductsFromHtml(htmlString);
|
|
8
|
-
* const hints = suggestWabActionsFromProducts(products);
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
function extractJsonLdBlocks(html) {
|
|
12
|
-
if (!html || typeof html !== 'string') return [];
|
|
13
|
-
const re = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
14
|
-
const blocks = [];
|
|
15
|
-
let m;
|
|
16
|
-
while ((m = re.exec(html)) !== null) {
|
|
17
|
-
blocks.push(m[1].trim());
|
|
18
|
-
}
|
|
19
|
-
return blocks;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function flattenGraph(data) {
|
|
23
|
-
if (Array.isArray(data)) return data;
|
|
24
|
-
if (data && Array.isArray(data['@graph'])) return data['@graph'];
|
|
25
|
-
return [data];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {string} html
|
|
30
|
-
* @returns {Array<{ type: string, name?: string, sku?: string, offers?: unknown }>}
|
|
31
|
-
*/
|
|
32
|
-
function extractProductsFromHtml(html) {
|
|
33
|
-
const out = [];
|
|
34
|
-
const blocks = extractJsonLdBlocks(html);
|
|
35
|
-
for (const text of blocks) {
|
|
36
|
-
let data;
|
|
37
|
-
try {
|
|
38
|
-
data = JSON.parse(text);
|
|
39
|
-
} catch {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
const items = flattenGraph(data);
|
|
43
|
-
for (const node of items) {
|
|
44
|
-
if (!node || typeof node !== 'object') continue;
|
|
45
|
-
let types = node['@type'];
|
|
46
|
-
if (typeof types === 'string') types = [types];
|
|
47
|
-
if (!Array.isArray(types)) types = [];
|
|
48
|
-
if (!types.includes('Product')) continue;
|
|
49
|
-
out.push({
|
|
50
|
-
type: 'Product',
|
|
51
|
-
name: node.name,
|
|
52
|
-
sku: node.sku,
|
|
53
|
-
offers: node.offers
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function suggestWabActionsFromProducts(products) {
|
|
61
|
-
const actions = [];
|
|
62
|
-
if (products.length) {
|
|
63
|
-
actions.push({
|
|
64
|
-
name: 'getProductFromSchema',
|
|
65
|
-
description: 'Structured products from schema.org JSON-LD',
|
|
66
|
-
source: 'schema.org'
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
if (products.some((p) => p.offers)) {
|
|
70
|
-
actions.push({
|
|
71
|
-
name: 'getOfferPrice',
|
|
72
|
-
description: 'Prices from schema.org Offer',
|
|
73
|
-
source: 'schema.org'
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
return actions;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
module.exports = {
|
|
80
|
-
extractJsonLdBlocks,
|
|
81
|
-
extractProductsFromHtml,
|
|
82
|
-
suggestWabActionsFromProducts
|
|
83
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Server-side / Node: extract schema.org Product nodes from HTML (JSON-LD blocks).
|
|
3
|
+
* No extra dependencies — regex-based script extraction (same semantics as browser WABSchema).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* const { extractProductsFromHtml, suggestWabActionsFromProducts } = require('./schema-discovery');
|
|
7
|
+
* const products = extractProductsFromHtml(htmlString);
|
|
8
|
+
* const hints = suggestWabActionsFromProducts(products);
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function extractJsonLdBlocks(html) {
|
|
12
|
+
if (!html || typeof html !== 'string') return [];
|
|
13
|
+
const re = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
14
|
+
const blocks = [];
|
|
15
|
+
let m;
|
|
16
|
+
while ((m = re.exec(html)) !== null) {
|
|
17
|
+
blocks.push(m[1].trim());
|
|
18
|
+
}
|
|
19
|
+
return blocks;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function flattenGraph(data) {
|
|
23
|
+
if (Array.isArray(data)) return data;
|
|
24
|
+
if (data && Array.isArray(data['@graph'])) return data['@graph'];
|
|
25
|
+
return [data];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} html
|
|
30
|
+
* @returns {Array<{ type: string, name?: string, sku?: string, offers?: unknown }>}
|
|
31
|
+
*/
|
|
32
|
+
function extractProductsFromHtml(html) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const blocks = extractJsonLdBlocks(html);
|
|
35
|
+
for (const text of blocks) {
|
|
36
|
+
let data;
|
|
37
|
+
try {
|
|
38
|
+
data = JSON.parse(text);
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const items = flattenGraph(data);
|
|
43
|
+
for (const node of items) {
|
|
44
|
+
if (!node || typeof node !== 'object') continue;
|
|
45
|
+
let types = node['@type'];
|
|
46
|
+
if (typeof types === 'string') types = [types];
|
|
47
|
+
if (!Array.isArray(types)) types = [];
|
|
48
|
+
if (!types.includes('Product')) continue;
|
|
49
|
+
out.push({
|
|
50
|
+
type: 'Product',
|
|
51
|
+
name: node.name,
|
|
52
|
+
sku: node.sku,
|
|
53
|
+
offers: node.offers
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function suggestWabActionsFromProducts(products) {
|
|
61
|
+
const actions = [];
|
|
62
|
+
if (products.length) {
|
|
63
|
+
actions.push({
|
|
64
|
+
name: 'getProductFromSchema',
|
|
65
|
+
description: 'Structured products from schema.org JSON-LD',
|
|
66
|
+
source: 'schema.org'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (products.some((p) => p.offers)) {
|
|
70
|
+
actions.push({
|
|
71
|
+
name: 'getOfferPrice',
|
|
72
|
+
description: 'Prices from schema.org Offer',
|
|
73
|
+
source: 'schema.org'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return actions;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
extractJsonLdBlocks,
|
|
81
|
+
extractProductsFromHtml,
|
|
82
|
+
suggestWabActionsFromProducts
|
|
83
|
+
};
|