@yuzc-001/grasp 0.6.6
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 +21 -0
- package/README.md +327 -0
- package/README.zh-CN.md +324 -0
- package/examples/README.md +31 -0
- package/examples/claude-desktop.json +8 -0
- package/examples/codex-config.toml +4 -0
- package/grasp.skill +0 -0
- package/index.js +87 -0
- package/package.json +48 -0
- package/scripts/grasp_openclaw_ctl.sh +122 -0
- package/scripts/run-search-benchmark.mjs +287 -0
- package/scripts/update-star-history.mjs +274 -0
- package/skill/SKILL.md +61 -0
- package/skill/references/tools.md +306 -0
- package/src/cli/auto-configure.js +116 -0
- package/src/cli/cmd-connect.js +148 -0
- package/src/cli/cmd-explain.js +42 -0
- package/src/cli/cmd-logs.js +55 -0
- package/src/cli/cmd-status.js +119 -0
- package/src/cli/config.js +27 -0
- package/src/cli/detect-chrome.js +58 -0
- package/src/grasp/handoff/events.js +67 -0
- package/src/grasp/handoff/persist.js +48 -0
- package/src/grasp/handoff/state.js +28 -0
- package/src/grasp/page/capture.js +34 -0
- package/src/grasp/page/state.js +273 -0
- package/src/grasp/verify/evidence.js +40 -0
- package/src/grasp/verify/pipeline.js +52 -0
- package/src/layer1-bridge/chrome.js +416 -0
- package/src/layer1-bridge/webmcp.js +143 -0
- package/src/layer2-perception/hints.js +284 -0
- package/src/layer3-action/actions.js +400 -0
- package/src/runtime/browser-instance.js +65 -0
- package/src/runtime/truth/model.js +94 -0
- package/src/runtime/truth/snapshot.js +51 -0
- package/src/server/affordances.js +47 -0
- package/src/server/audit.js +122 -0
- package/src/server/boss-fast-path.js +164 -0
- package/src/server/boundary-guard.js +53 -0
- package/src/server/content.js +97 -0
- package/src/server/continuity.js +256 -0
- package/src/server/engine-selection.js +29 -0
- package/src/server/entry-orchestrator.js +115 -0
- package/src/server/error-codes.js +7 -0
- package/src/server/explain-share-card.js +113 -0
- package/src/server/fast-path-router.js +134 -0
- package/src/server/form-runtime.js +602 -0
- package/src/server/form-tasks.js +254 -0
- package/src/server/gateway-response.js +62 -0
- package/src/server/index.js +22 -0
- package/src/server/observe.js +52 -0
- package/src/server/page-projection.js +31 -0
- package/src/server/page-state.js +27 -0
- package/src/server/postconditions.js +128 -0
- package/src/server/prompt-assembly.js +148 -0
- package/src/server/responses.js +44 -0
- package/src/server/route-boundary.js +174 -0
- package/src/server/route-policy.js +168 -0
- package/src/server/runtime-confirmation.js +87 -0
- package/src/server/runtime-status.js +7 -0
- package/src/server/share-artifacts.js +284 -0
- package/src/server/state.js +132 -0
- package/src/server/structured-extraction.js +131 -0
- package/src/server/surface-prompts.js +166 -0
- package/src/server/task-frame.js +11 -0
- package/src/server/tasks/search-task.js +321 -0
- package/src/server/tools.actions.js +1361 -0
- package/src/server/tools.form.js +526 -0
- package/src/server/tools.gateway.js +757 -0
- package/src/server/tools.handoff.js +210 -0
- package/src/server/tools.js +20 -0
- package/src/server/tools.legacy.js +983 -0
- package/src/server/tools.strategy.js +250 -0
- package/src/server/tools.task-surface.js +66 -0
- package/src/server/tools.workspace.js +873 -0
- package/src/server/workspace-runtime.js +1138 -0
- package/src/server/workspace-tasks.js +735 -0
- package/start-chrome.bat +84 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function createHandoffState() {
|
|
2
|
+
return {
|
|
3
|
+
state: 'idle', // idle | handoff_required | handoff_in_progress | awaiting_reacquisition | resumed_unverified | resumed_verified
|
|
4
|
+
reason: null,
|
|
5
|
+
note: null,
|
|
6
|
+
requestedAt: null,
|
|
7
|
+
updatedAt: Date.now(),
|
|
8
|
+
verifiedAt: null,
|
|
9
|
+
evidence: null,
|
|
10
|
+
expected_url_contains: null,
|
|
11
|
+
expected_page_role: null,
|
|
12
|
+
expected_selector: null,
|
|
13
|
+
continuation_goal: null,
|
|
14
|
+
expected_hint_label: null,
|
|
15
|
+
taskId: null,
|
|
16
|
+
siteKey: null,
|
|
17
|
+
sessionKey: null,
|
|
18
|
+
lastUrl: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mergeHandoffState(base, patch = {}) {
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
...patch,
|
|
26
|
+
updatedAt: patch.updatedAt ?? Date.now(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
async function sleep(ms) {
|
|
2
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isExecutionContextTransient(error) {
|
|
6
|
+
const message = error?.message ?? '';
|
|
7
|
+
return message.includes('Execution context was destroyed') || message.includes('Cannot find context with specified id');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function capturePageSnapshot(page, { attempts = 3, delayMs = 120 } = {}) {
|
|
11
|
+
let lastError = null;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
14
|
+
try {
|
|
15
|
+
return await page.evaluate(() => {
|
|
16
|
+
const bodyText = document.body?.innerText?.replace(/\s+/g, ' ').trim().slice(0, 1200) ?? '';
|
|
17
|
+
const nodes = document.querySelectorAll('button,a,input,textarea,select,[role],[contenteditable]').length;
|
|
18
|
+
const forms = document.querySelectorAll('form,input,textarea,select').length;
|
|
19
|
+
const navs = document.querySelectorAll('nav,header,[role="navigation"],aside a').length;
|
|
20
|
+
const headings = Array.from(document.querySelectorAll('h1,h2,h3')).map((el) => el.textContent?.trim()).filter(Boolean).slice(0, 8);
|
|
21
|
+
const title = document.title || null;
|
|
22
|
+
return { bodyText, nodes, forms, navs, headings, title };
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
lastError = error;
|
|
26
|
+
if (!isExecutionContextTransient(error) || i === attempts - 1) {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
await sleep(delayMs * (i + 1));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
throw lastError;
|
|
34
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
export function createPageGraspState() {
|
|
2
|
+
return {
|
|
3
|
+
lastUrl: null,
|
|
4
|
+
domRevision: 0,
|
|
5
|
+
lastSnapshotHash: null,
|
|
6
|
+
pageIdentity: null,
|
|
7
|
+
currentRole: 'unknown', // unknown | content | form | auth | docs | search | workspace | navigation-heavy | checkpoint
|
|
8
|
+
workspaceSurface: null, // null | list | detail | thread | composer | loading_shell
|
|
9
|
+
workspaceSignals: [],
|
|
10
|
+
graspConfidence: 'unknown', // unknown | low | medium | high
|
|
11
|
+
reacquired: false,
|
|
12
|
+
riskGateDetected: false,
|
|
13
|
+
checkpointSignals: [],
|
|
14
|
+
checkpointKind: null,
|
|
15
|
+
suggestedNextAction: null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function detectCheckpointSignals({ url, title = '', bodyText = '', headings = [], nodes = 0 }) {
|
|
20
|
+
const text = bodyText.toLowerCase();
|
|
21
|
+
const titleText = String(title).toLowerCase();
|
|
22
|
+
const headingText = headings.join(' ').toLowerCase();
|
|
23
|
+
const signals = [];
|
|
24
|
+
const solvedCloudflareChallenge = text.includes('you bypassed the cloudflare challenge');
|
|
25
|
+
|
|
26
|
+
if (titleText.includes('just a moment') || text.includes('just a moment')) {
|
|
27
|
+
signals.push('title_or_text_just_a_moment');
|
|
28
|
+
}
|
|
29
|
+
if (text.includes('checking your browser')) signals.push('checking_your_browser');
|
|
30
|
+
if (text.includes('verify you are human')) signals.push('verify_you_are_human');
|
|
31
|
+
if (text.includes('security check')) signals.push('security_check');
|
|
32
|
+
if (!solvedCloudflareChallenge && (text.includes('cf-challenge') || text.includes('cloudflare'))) signals.push('cloudflare_challenge');
|
|
33
|
+
if (headingText.includes('just a moment')) signals.push('heading_just_a_moment');
|
|
34
|
+
if (nodes <= 0 && text.length <= 24) signals.push('low_interaction_sparse_page');
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
const host = parsed.hostname.toLowerCase();
|
|
39
|
+
const search = parsed.search.toLowerCase();
|
|
40
|
+
const fullUrl = parsed.toString().toLowerCase();
|
|
41
|
+
|
|
42
|
+
if (search.includes('__cf_chl') || fullUrl.includes('cf_chl')) {
|
|
43
|
+
signals.push('cloudflare_challenge_url');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ((host.includes('chatgpt.com') || host.includes('openai.com')) && signals.length > 0) {
|
|
47
|
+
signals.push('high_risk_target_with_gate_signals');
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
return [...new Set(signals)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function classifyCheckpointKind(signals = []) {
|
|
55
|
+
if (signals.includes('cloudflare_challenge') || signals.includes('cloudflare_challenge_url') || signals.includes('checking_your_browser')) {
|
|
56
|
+
return 'challenge';
|
|
57
|
+
}
|
|
58
|
+
if (signals.includes('title_or_text_just_a_moment')) {
|
|
59
|
+
return 'waiting_room';
|
|
60
|
+
}
|
|
61
|
+
if (signals.includes('security_check') || signals.includes('verify_you_are_human')) {
|
|
62
|
+
return 'verification';
|
|
63
|
+
}
|
|
64
|
+
if (signals.length > 0) {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function classifySuggestedNextAction({ riskGateDetected, checkpointKind, nodes = 0 }) {
|
|
71
|
+
if (!riskGateDetected) return null;
|
|
72
|
+
if (checkpointKind === 'waiting_room' && nodes <= 0) return 'wait_then_recheck';
|
|
73
|
+
return 'handoff_required';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getUrlPath(url) {
|
|
77
|
+
try {
|
|
78
|
+
return new URL(url).pathname.toLowerCase();
|
|
79
|
+
} catch {
|
|
80
|
+
return url.toLowerCase();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getUrlPathSegments(url) {
|
|
85
|
+
return getUrlPath(url).split('/').filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasWorkspacePathEvidence(url) {
|
|
89
|
+
const segments = getUrlPathSegments(url);
|
|
90
|
+
const workspaceSegments = ['chat', 'thread', 'conversation', 'inbox', 'message', 'messages', 'msg'];
|
|
91
|
+
return segments.some((segment) => workspaceSegments.includes(segment));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detectWorkspaceSignals({ url, title = '', bodyText = '', headings = [], navs = 0 }) {
|
|
95
|
+
const text = bodyText.toLowerCase();
|
|
96
|
+
const signals = [];
|
|
97
|
+
|
|
98
|
+
if (hasWorkspacePathEvidence(url)) {
|
|
99
|
+
signals.push('workspace_path');
|
|
100
|
+
}
|
|
101
|
+
if (text.includes('按enter键发送')) {
|
|
102
|
+
signals.push('workspace_composer_text');
|
|
103
|
+
}
|
|
104
|
+
if (text.includes('发消息')) {
|
|
105
|
+
signals.push('workspace_composer_text');
|
|
106
|
+
}
|
|
107
|
+
if (text.includes('发送消息')) {
|
|
108
|
+
signals.push('workspace_composer_text');
|
|
109
|
+
}
|
|
110
|
+
if (text.includes('输入消息')) {
|
|
111
|
+
signals.push('workspace_composer_text');
|
|
112
|
+
}
|
|
113
|
+
if (text.includes('消息') || text.includes('聊天') || text.includes('对话')) {
|
|
114
|
+
signals.push('workspace_thread_text');
|
|
115
|
+
}
|
|
116
|
+
if (navs >= 6) {
|
|
117
|
+
signals.push('workspace_navigation');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [...new Set(signals)];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function scoreWorkspaceSurface({ url, bodyText = '' }) {
|
|
124
|
+
const text = bodyText.toLowerCase();
|
|
125
|
+
let threadScore = hasWorkspacePathEvidence(url) ? 2 : 0;
|
|
126
|
+
let composerScore = 0;
|
|
127
|
+
let loadingScore = 0;
|
|
128
|
+
|
|
129
|
+
if (text.includes('按enter键发送')) {
|
|
130
|
+
composerScore += 3;
|
|
131
|
+
}
|
|
132
|
+
if (text.includes('发消息')) {
|
|
133
|
+
composerScore += 3;
|
|
134
|
+
}
|
|
135
|
+
if (text.includes('发送消息')) {
|
|
136
|
+
composerScore += 3;
|
|
137
|
+
}
|
|
138
|
+
if (text.includes('输入消息')) {
|
|
139
|
+
composerScore += 3;
|
|
140
|
+
}
|
|
141
|
+
if (text.includes('消息')) {
|
|
142
|
+
threadScore += 1;
|
|
143
|
+
}
|
|
144
|
+
if (text.includes('聊天') || text.includes('对话')) {
|
|
145
|
+
threadScore += 1;
|
|
146
|
+
}
|
|
147
|
+
if (text.includes('加载中,请稍候')) {
|
|
148
|
+
loadingScore += 2;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { threadScore, composerScore, loadingScore };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function classifyWorkspaceSurface(scores = {}) {
|
|
155
|
+
const { threadScore = 0, composerScore = 0, loadingScore = 0 } = scores;
|
|
156
|
+
|
|
157
|
+
if (loadingScore > 0) {
|
|
158
|
+
return 'loading_shell';
|
|
159
|
+
}
|
|
160
|
+
if (composerScore >= 3 && composerScore > threadScore) {
|
|
161
|
+
return 'composer';
|
|
162
|
+
}
|
|
163
|
+
if (threadScore >= 2) {
|
|
164
|
+
return 'thread';
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function classifyWorkspaceRole({ url, bodyText = '', headings = [] }) {
|
|
170
|
+
if (!hasWorkspacePathEvidence(url)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const scores = scoreWorkspaceSurface({ url, bodyText, headings });
|
|
174
|
+
return Math.max(scores.threadScore, scores.composerScore) >= 2 ? scores : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function classifyPageRole({ url, title = '', bodyText = '', nodes = 0, forms = 0, navs = 0, headings = [] }) {
|
|
178
|
+
const text = bodyText.toLowerCase();
|
|
179
|
+
const path = getUrlPath(url);
|
|
180
|
+
|
|
181
|
+
const searchHints = ['search results', 'no results', 'filter results'];
|
|
182
|
+
const formHints = ['submit', 'required', 'email', 'message'];
|
|
183
|
+
const headingText = headings.join(' ').toLowerCase();
|
|
184
|
+
const checkpointSignals = detectCheckpointSignals({ url, title, bodyText, headings, nodes });
|
|
185
|
+
const workspaceScores = classifyWorkspaceRole({ url, bodyText, headings });
|
|
186
|
+
|
|
187
|
+
if (checkpointSignals.length > 0) {
|
|
188
|
+
return 'checkpoint';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (workspaceScores) {
|
|
192
|
+
return 'workspace';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const authCopyPresent = ['sign in', 'log in', 'login'].some((hint) => text.includes(hint) || headingText.includes(hint));
|
|
196
|
+
const credentialPairPresent = text.includes('password') && text.includes('username');
|
|
197
|
+
if (authCopyPresent || credentialPairPresent || /\/login\b|\/signin\b/.test(path)) {
|
|
198
|
+
return 'auth';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const docsByPath = /\/docs\b|\/guide\b|\/reference\b|\/manual\b/.test(path);
|
|
202
|
+
const docsByHeadings = /installation|getting started|api reference|documentation/.test(headingText);
|
|
203
|
+
const docsByLayout = text.includes('on this page') || text.includes("what's next");
|
|
204
|
+
if (docsByPath || docsByHeadings || docsByLayout) {
|
|
205
|
+
return 'docs';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (searchHints.some((hint) => text.includes(hint))) {
|
|
209
|
+
return 'search';
|
|
210
|
+
}
|
|
211
|
+
const hasFormText = formHints.some((hint) => text.includes(hint));
|
|
212
|
+
const hasStrongFormSignals = forms >= 1 && hasFormText;
|
|
213
|
+
const hasDenseStandaloneForm = forms >= 3 && navs < 6;
|
|
214
|
+
if (hasStrongFormSignals || hasDenseStandaloneForm) {
|
|
215
|
+
return 'form';
|
|
216
|
+
}
|
|
217
|
+
if (navs >= 6) {
|
|
218
|
+
return 'navigation-heavy';
|
|
219
|
+
}
|
|
220
|
+
return 'content';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function classifyConfidence({ nodes = 0, bodyText = '', urlChanged, domRevisionChanged }) {
|
|
224
|
+
if (!bodyText) return 'low';
|
|
225
|
+
if (nodes <= 0) return 'low';
|
|
226
|
+
if (urlChanged || domRevisionChanged) return 'medium';
|
|
227
|
+
if (nodes >= 1 && bodyText.length >= 40) return 'high';
|
|
228
|
+
return 'medium';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function applySnapshotToPageGraspState(
|
|
232
|
+
state,
|
|
233
|
+
{ url, snapshotHash, title = '', bodyText = '', nodes = 0, forms = 0, navs = 0, headings = [] }
|
|
234
|
+
) {
|
|
235
|
+
const next = {
|
|
236
|
+
...state,
|
|
237
|
+
lastUrl: url,
|
|
238
|
+
lastSnapshotHash: snapshotHash,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const sameUrl = state.lastUrl === url;
|
|
242
|
+
const urlChanged = !sameUrl;
|
|
243
|
+
const domRevisionChanged = !!(sameUrl && state.lastSnapshotHash && state.lastSnapshotHash !== snapshotHash);
|
|
244
|
+
|
|
245
|
+
if (!sameUrl) {
|
|
246
|
+
next.domRevision = 0;
|
|
247
|
+
} else if (domRevisionChanged) {
|
|
248
|
+
next.domRevision = state.domRevision + 1;
|
|
249
|
+
} else {
|
|
250
|
+
next.domRevision = state.domRevision;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
next.reacquired = urlChanged || domRevisionChanged;
|
|
254
|
+
next.pageIdentity = `${url}#${next.domRevision}`;
|
|
255
|
+
next.checkpointSignals = detectCheckpointSignals({ url, title, bodyText, headings, nodes });
|
|
256
|
+
next.riskGateDetected = next.checkpointSignals.length > 0;
|
|
257
|
+
next.checkpointKind = classifyCheckpointKind(next.checkpointSignals);
|
|
258
|
+
next.suggestedNextAction = classifySuggestedNextAction({
|
|
259
|
+
riskGateDetected: next.riskGateDetected,
|
|
260
|
+
checkpointKind: next.checkpointKind,
|
|
261
|
+
nodes,
|
|
262
|
+
});
|
|
263
|
+
next.workspaceSignals = detectWorkspaceSignals({ url, title, bodyText, headings, navs });
|
|
264
|
+
next.currentRole = classifyPageRole({ url, title, bodyText, nodes, forms, navs, headings });
|
|
265
|
+
next.workspaceSurface = next.currentRole === 'workspace'
|
|
266
|
+
? classifyWorkspaceSurface(scoreWorkspaceSurface({ url, bodyText }))
|
|
267
|
+
: next.currentRole === 'navigation-heavy'
|
|
268
|
+
? 'list'
|
|
269
|
+
: null;
|
|
270
|
+
next.graspConfidence = classifyConfidence({ bodyText, nodes, urlChanged, domRevisionChanged });
|
|
271
|
+
|
|
272
|
+
return next;
|
|
273
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function buildEvidence(base = {}) {
|
|
2
|
+
return {
|
|
3
|
+
hint_id: base.hint_id ?? null,
|
|
4
|
+
action: base.action ?? null,
|
|
5
|
+
url: base.url ?? null,
|
|
6
|
+
page_title: base.page_title ?? null,
|
|
7
|
+
dom_revision: base.dom_revision ?? null,
|
|
8
|
+
active_hint: base.active_hint ?? null,
|
|
9
|
+
summary_excerpt: base.summary_excerpt ?? null,
|
|
10
|
+
page_role: base.page_role ?? null,
|
|
11
|
+
grasp_confidence: base.grasp_confidence ?? null,
|
|
12
|
+
reacquired: base.reacquired ?? null,
|
|
13
|
+
details: base.details ?? null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function capturePageEvidence(page, state, overrides = {}) {
|
|
18
|
+
let title = null;
|
|
19
|
+
let summaryExcerpt = null;
|
|
20
|
+
try {
|
|
21
|
+
title = await page.title();
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
summaryExcerpt = await page.evaluate(() =>
|
|
26
|
+
document.body?.innerText?.replace(/\s+/g, ' ').trim().slice(0, 240) ?? ''
|
|
27
|
+
);
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
return buildEvidence({
|
|
31
|
+
url: page?.url?.() ?? null,
|
|
32
|
+
page_title: title,
|
|
33
|
+
dom_revision: state?.pageState?.domRevision ?? null,
|
|
34
|
+
page_role: state?.pageState?.currentRole ?? null,
|
|
35
|
+
grasp_confidence: state?.pageState?.graspConfidence ?? null,
|
|
36
|
+
reacquired: state?.pageState?.reacquired ?? null,
|
|
37
|
+
...overrides,
|
|
38
|
+
summary_excerpt: overrides.summary_excerpt ?? summaryExcerpt,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { buildEvidence, capturePageEvidence } from './evidence.js';
|
|
2
|
+
|
|
3
|
+
export async function runVerifiedAction({
|
|
4
|
+
action,
|
|
5
|
+
page,
|
|
6
|
+
state,
|
|
7
|
+
execute,
|
|
8
|
+
verify,
|
|
9
|
+
onSuccess,
|
|
10
|
+
onFailure,
|
|
11
|
+
baseEvidence = {},
|
|
12
|
+
}) {
|
|
13
|
+
const executionResult = await execute();
|
|
14
|
+
const verification = await verify(executionResult);
|
|
15
|
+
|
|
16
|
+
if (!verification?.ok) {
|
|
17
|
+
const failureEvidence = await capturePageEvidence(page, state, {
|
|
18
|
+
action,
|
|
19
|
+
...baseEvidence,
|
|
20
|
+
details: verification?.evidence ?? null,
|
|
21
|
+
});
|
|
22
|
+
const failure = {
|
|
23
|
+
ok: false,
|
|
24
|
+
error_code: verification?.error_code,
|
|
25
|
+
retryable: verification?.retryable,
|
|
26
|
+
suggested_next_step: verification?.suggested_next_step,
|
|
27
|
+
evidence: failureEvidence,
|
|
28
|
+
executionResult,
|
|
29
|
+
};
|
|
30
|
+
if (onFailure) return onFailure(failure);
|
|
31
|
+
return failure;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const capturedSuccess = await capturePageEvidence(page, state, {
|
|
35
|
+
action,
|
|
36
|
+
...baseEvidence,
|
|
37
|
+
url: verification?.evidence?.url ?? page?.url?.() ?? null,
|
|
38
|
+
dom_revision: verification?.evidence?.domRevision ?? state?.pageState?.domRevision ?? null,
|
|
39
|
+
active_hint: verification?.evidence?.activeId ?? null,
|
|
40
|
+
details: verification?.evidence ?? null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const successEvidence = buildEvidence(capturedSuccess);
|
|
44
|
+
const success = {
|
|
45
|
+
ok: true,
|
|
46
|
+
evidence: successEvidence,
|
|
47
|
+
executionResult,
|
|
48
|
+
verification,
|
|
49
|
+
};
|
|
50
|
+
if (onSuccess) return onSuccess(success);
|
|
51
|
+
return success;
|
|
52
|
+
}
|