@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,254 @@
|
|
|
1
|
+
function compactText(value) {
|
|
2
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function includesAny(text, terms) {
|
|
6
|
+
return terms.some((term) => text.includes(term));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getFieldLabel(field) {
|
|
10
|
+
return compactText(
|
|
11
|
+
field?.ariaLabelledByText
|
|
12
|
+
|| field?.ariaLabel
|
|
13
|
+
|| field?.labelForText
|
|
14
|
+
|| field?.placeholder
|
|
15
|
+
|| field?.name
|
|
16
|
+
|| field?.tag
|
|
17
|
+
|| 'unknown'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function deriveRiskLevel(label, field) {
|
|
22
|
+
const text = compactText(label).toLowerCase();
|
|
23
|
+
const tag = compactText(field?.tag).toLowerCase();
|
|
24
|
+
const type = compactText(field?.type).toLowerCase();
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
includesAny(text, ['证件', '身份证', '手机号', '电话', '邮箱', '账号', '账户', '密码', '上传', '提交', '确认', '验证码', '姓名'])
|
|
28
|
+
|| includesAny(tag, ['file'])
|
|
29
|
+
|| includesAny(type, ['file', 'password'])
|
|
30
|
+
) {
|
|
31
|
+
return 'sensitive';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
includesAny(text, ['城市', '部门', '日期', '时间', '时长', '天数', '学校', '专业', '学历', '年级', '地区', '岗位', '语言', '接受', '调配'])
|
|
36
|
+
|| tag === 'select'
|
|
37
|
+
|| type === 'select'
|
|
38
|
+
) {
|
|
39
|
+
return 'review';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (
|
|
43
|
+
includesAny(text, ['研究', '项目', '技能', '经历', '简介', '自我介绍', '主页', 'description', 'bio', 'homepage', 'project', 'research', 'skill'])
|
|
44
|
+
|| tag === 'textarea'
|
|
45
|
+
|| type === 'textarea'
|
|
46
|
+
) {
|
|
47
|
+
return 'safe';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return 'safe';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deriveCurrentState(field) {
|
|
54
|
+
if (field?.current_state) return field.current_state;
|
|
55
|
+
const type = compactText(field?.type).toLowerCase();
|
|
56
|
+
const tag = compactText(field?.tag).toLowerCase();
|
|
57
|
+
const isCheckable = type === 'checkbox' || type === 'radio' || tag === 'checkbox' || tag === 'radio';
|
|
58
|
+
if (field?.checked === true) return 'filled';
|
|
59
|
+
if (field?.checked === false && isCheckable) return 'missing';
|
|
60
|
+
if (isCheckable) return 'missing';
|
|
61
|
+
if (Array.isArray(field?.value)) return field.value.length > 0 ? 'filled' : 'missing';
|
|
62
|
+
if (field?.value === null || field?.value === undefined) return 'missing';
|
|
63
|
+
return compactText(field.value) ? 'filled' : 'missing';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function normalizeFormField(field) {
|
|
67
|
+
const label = compactText(field?.label) || getFieldLabel(field);
|
|
68
|
+
const normalized_label = compactText(label);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...field,
|
|
72
|
+
label,
|
|
73
|
+
normalized_label,
|
|
74
|
+
disabled: field?.disabled === true,
|
|
75
|
+
readOnly: field?.readOnly === true || field?.readonly === true,
|
|
76
|
+
current_state: deriveCurrentState(field),
|
|
77
|
+
risk_level: field?.risk_level ?? deriveRiskLevel(label, field),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function summarizeFormFields(fields) {
|
|
82
|
+
const normalizedFields = fields.map((field) => normalizeFormField(field));
|
|
83
|
+
const counts = normalizedFields.reduce((acc, field) => {
|
|
84
|
+
acc[field.risk_level] += 1;
|
|
85
|
+
return acc;
|
|
86
|
+
}, { safe: 0, review: 0, sensitive: 0 });
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
total: normalizedFields.length,
|
|
90
|
+
safe: counts.safe,
|
|
91
|
+
review: counts.review,
|
|
92
|
+
sensitive: counts.sensitive,
|
|
93
|
+
labels: normalizedFields.map((field) => field.label),
|
|
94
|
+
fields: normalizedFields,
|
|
95
|
+
lines: [
|
|
96
|
+
`Total fields: ${normalizedFields.length}`,
|
|
97
|
+
`Safe: ${counts.safe}`,
|
|
98
|
+
`Review: ${counts.review}`,
|
|
99
|
+
`Sensitive: ${counts.sensitive}`,
|
|
100
|
+
`Labels: ${normalizedFields.map((field) => field.label).join(', ') || 'none'}`,
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildFormVerification(fields) {
|
|
106
|
+
const normalizedFields = fields.map((field) => normalizeFormField(field));
|
|
107
|
+
const missingRequired = normalizedFields.filter((field) => field.required && field.current_state !== 'filled').length;
|
|
108
|
+
const riskyPending = normalizedFields.filter((field) => field.risk_level !== 'safe' && field.current_state !== 'filled').length;
|
|
109
|
+
const unresolved = normalizedFields.filter((field) => field.current_state === 'unresolved').length;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
completion_status: missingRequired > 0 || riskyPending > 0 || unresolved > 0 ? 'review_required' : 'ready',
|
|
113
|
+
summary: {
|
|
114
|
+
missing_required: missingRequired,
|
|
115
|
+
risky_pending: riskyPending,
|
|
116
|
+
unresolved,
|
|
117
|
+
},
|
|
118
|
+
fields: normalizedFields,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function finalizeFormSnapshot({ fields = [], sections = [], submit_controls = [] } = {}) {
|
|
123
|
+
const normalizedFields = fields.map((field) => normalizeFormField(field));
|
|
124
|
+
const summary = summarizeFormFields(normalizedFields);
|
|
125
|
+
const verification = buildFormVerification(normalizedFields);
|
|
126
|
+
const counts = normalizedFields.reduce((acc, field) => {
|
|
127
|
+
acc[field.normalized_label] = (acc[field.normalized_label] ?? 0) + 1;
|
|
128
|
+
return acc;
|
|
129
|
+
}, {});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
sections,
|
|
133
|
+
fields: normalizedFields,
|
|
134
|
+
submit_controls: submit_controls.map((control) => ({
|
|
135
|
+
...control,
|
|
136
|
+
label: compactText(control?.label) || 'submit',
|
|
137
|
+
risk_level: control?.risk_level ?? 'sensitive',
|
|
138
|
+
})),
|
|
139
|
+
ambiguous_labels: Object.entries(counts)
|
|
140
|
+
.filter(([, count]) => count > 1)
|
|
141
|
+
.map(([label]) => label),
|
|
142
|
+
summary,
|
|
143
|
+
completion_status: verification.completion_status,
|
|
144
|
+
verification: verification.summary,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function collectVisibleFormSnapshot(page) {
|
|
149
|
+
const rawSnapshot = await page.evaluate(() => {
|
|
150
|
+
function compactText(value) {
|
|
151
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getHintId(el) {
|
|
155
|
+
return el.getAttribute('data-grasp-id') || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getLabelForText(el) {
|
|
159
|
+
const id = el.getAttribute('id');
|
|
160
|
+
if (!id) return '';
|
|
161
|
+
const label = document.querySelector(`label[for="${id}"]`);
|
|
162
|
+
return label?.textContent?.trim() ?? '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getAriaLabelledByText(el) {
|
|
166
|
+
const labelledBy = el.getAttribute('aria-labelledby');
|
|
167
|
+
if (!labelledBy) return '';
|
|
168
|
+
return labelledBy.trim().split(/\s+/)
|
|
169
|
+
.map((id) => document.getElementById(id)?.textContent?.trim() ?? '')
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join(' ');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isVisible(el) {
|
|
175
|
+
const rect = el.getBoundingClientRect();
|
|
176
|
+
return rect.width > 0 && rect.height > 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getSectionName(el) {
|
|
180
|
+
const fieldset = el.closest('fieldset');
|
|
181
|
+
const legend = fieldset?.querySelector('legend');
|
|
182
|
+
const legendText = compactText(legend?.textContent);
|
|
183
|
+
if (legendText) return legendText;
|
|
184
|
+
|
|
185
|
+
const group = el.closest('section, form, [role="group"], .form-section, .resume-section');
|
|
186
|
+
const heading = group?.querySelector('h1, h2, h3, h4, h5, h6, .section-title, [data-section-title]');
|
|
187
|
+
const headingText = compactText(heading?.textContent);
|
|
188
|
+
return headingText || 'General';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function describeField(el) {
|
|
192
|
+
const tag = el.tagName.toLowerCase();
|
|
193
|
+
const type = el.getAttribute('type') || (tag === 'select' ? 'select' : tag === 'textarea' ? 'textarea' : tag);
|
|
194
|
+
return {
|
|
195
|
+
hint_id: getHintId(el),
|
|
196
|
+
tag,
|
|
197
|
+
type,
|
|
198
|
+
id: el.getAttribute('id')?.trim() ?? '',
|
|
199
|
+
name: el.getAttribute('name')?.trim() ?? '',
|
|
200
|
+
required: el.required || el.getAttribute('required') !== null,
|
|
201
|
+
disabled: el.disabled === true,
|
|
202
|
+
readOnly: 'readOnly' in el ? el.readOnly === true : false,
|
|
203
|
+
value: 'value' in el ? el.value : null,
|
|
204
|
+
checked: 'checked' in el ? el.checked : null,
|
|
205
|
+
ariaLabelledByText: getAriaLabelledByText(el),
|
|
206
|
+
ariaLabel: el.getAttribute('aria-label')?.trim() ?? '',
|
|
207
|
+
labelForText: getLabelForText(el),
|
|
208
|
+
placeholder: el.getAttribute('placeholder')?.trim() ?? '',
|
|
209
|
+
section_name: getSectionName(el),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const fieldElements = [...document.querySelectorAll('input, textarea, select')]
|
|
214
|
+
.filter((el) => {
|
|
215
|
+
const type = el.getAttribute('type') || '';
|
|
216
|
+
return type !== 'hidden' && isVisible(el);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const fields = fieldElements.map(describeField);
|
|
220
|
+
const sections = [...new Set(fields.map((field) => field.section_name))].map((name) => ({
|
|
221
|
+
name,
|
|
222
|
+
field_labels: fields
|
|
223
|
+
.filter((field) => field.section_name === name)
|
|
224
|
+
.map((field) => compactText(
|
|
225
|
+
field.ariaLabelledByText
|
|
226
|
+
|| field.ariaLabel
|
|
227
|
+
|| field.labelForText
|
|
228
|
+
|| field.placeholder
|
|
229
|
+
|| field.name
|
|
230
|
+
|| field.tag
|
|
231
|
+
|| 'unknown'
|
|
232
|
+
)),
|
|
233
|
+
}));
|
|
234
|
+
|
|
235
|
+
const submit_controls = [...document.querySelectorAll('button, input[type="submit"], input[type="button"], input[type="image"]')]
|
|
236
|
+
.filter((el) => isVisible(el))
|
|
237
|
+
.map((el) => ({
|
|
238
|
+
hint_id: getHintId(el),
|
|
239
|
+
tag: el.tagName.toLowerCase(),
|
|
240
|
+
type: el.getAttribute('type') || el.tagName.toLowerCase(),
|
|
241
|
+
label: compactText(
|
|
242
|
+
el.getAttribute('aria-label')
|
|
243
|
+
|| el.textContent
|
|
244
|
+
|| el.getAttribute('value')
|
|
245
|
+
|| 'submit'
|
|
246
|
+
),
|
|
247
|
+
risk_level: 'sensitive',
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
return { fields, sections, submit_controls };
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return finalizeFormSnapshot(rawSnapshot);
|
|
254
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { textResponse } from './responses.js';
|
|
2
|
+
import { buildAgentBoundary, buildAgentBoundaryLines } from './route-boundary.js';
|
|
3
|
+
import { buildAgentPrompt } from './prompt-assembly.js';
|
|
4
|
+
|
|
5
|
+
function normalizeLines(value) {
|
|
6
|
+
return Array.isArray(value) ? value : [value];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildGatewayResponse({
|
|
10
|
+
status,
|
|
11
|
+
page,
|
|
12
|
+
result = {},
|
|
13
|
+
continuation = {},
|
|
14
|
+
evidence = {},
|
|
15
|
+
runtime = {},
|
|
16
|
+
route = null,
|
|
17
|
+
error_code = null,
|
|
18
|
+
message,
|
|
19
|
+
}) {
|
|
20
|
+
const agentBoundary = buildAgentBoundary({
|
|
21
|
+
status,
|
|
22
|
+
page,
|
|
23
|
+
result,
|
|
24
|
+
continuation,
|
|
25
|
+
route,
|
|
26
|
+
});
|
|
27
|
+
const agentPrompt = buildAgentPrompt({
|
|
28
|
+
status,
|
|
29
|
+
page,
|
|
30
|
+
result,
|
|
31
|
+
continuation,
|
|
32
|
+
route,
|
|
33
|
+
agentBoundary,
|
|
34
|
+
});
|
|
35
|
+
const boundaryLines = buildAgentBoundaryLines(agentBoundary);
|
|
36
|
+
const lines = message
|
|
37
|
+
? [...normalizeLines(message), ...boundaryLines].filter(Boolean)
|
|
38
|
+
: [
|
|
39
|
+
`Status: ${status}`,
|
|
40
|
+
`Page: ${page?.title ?? 'unknown'}`,
|
|
41
|
+
`URL: ${page?.url ?? 'unknown'}`,
|
|
42
|
+
runtime?.instance?.display ? `Instance: ${runtime.instance.display}` : null,
|
|
43
|
+
runtime?.instance?.warning ? `Instance warning: ${runtime.instance.warning}` : null,
|
|
44
|
+
route?.selected_mode ? `Route: ${route.selected_mode}` : null,
|
|
45
|
+
...boundaryLines,
|
|
46
|
+
result.summary ? `Summary: ${result.summary}` : null,
|
|
47
|
+
continuation.suggested_next_action ? `Next: ${continuation.suggested_next_action}` : null,
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
|
|
50
|
+
return textResponse(lines, {
|
|
51
|
+
status,
|
|
52
|
+
page,
|
|
53
|
+
result,
|
|
54
|
+
continuation,
|
|
55
|
+
evidence,
|
|
56
|
+
runtime,
|
|
57
|
+
...(agentBoundary ? { agent_boundary: agentBoundary } : {}),
|
|
58
|
+
...(agentPrompt ? { agent_prompt: agentPrompt } : {}),
|
|
59
|
+
...(error_code ? { error_code } : {}),
|
|
60
|
+
...(route ? { route } : {}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
import { createServerState } from './state.js';
|
|
4
|
+
import { registerTools } from './tools.js';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version } = require('../../package.json');
|
|
9
|
+
|
|
10
|
+
export const SERVER_INFO = {
|
|
11
|
+
name: 'grasp',
|
|
12
|
+
version,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createGraspServer() {
|
|
16
|
+
const server = new McpServer(SERVER_INFO);
|
|
17
|
+
const state = createServerState();
|
|
18
|
+
|
|
19
|
+
registerTools(server, state);
|
|
20
|
+
|
|
21
|
+
return { server, state };
|
|
22
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { extractMainContent, summarizeExtractedText, toMarkdownDocument, waitUntilStable } from './content.js';
|
|
2
|
+
import { rankAffordances } from './affordances.js';
|
|
3
|
+
import { syncPageState } from './state.js';
|
|
4
|
+
|
|
5
|
+
export async function extractObservedContent({ page, deps = {}, include_markdown = false } = {}) {
|
|
6
|
+
const waitStable = deps.waitStable ?? waitUntilStable;
|
|
7
|
+
const extractContent = deps.extractContent ?? extractMainContent;
|
|
8
|
+
await waitStable(page, { stableChecks: 3, interval: 200, timeout: 5000 });
|
|
9
|
+
const content = await extractContent(page);
|
|
10
|
+
const result = {
|
|
11
|
+
summary: summarizeExtractedText(content.text),
|
|
12
|
+
main_text: content.text,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (include_markdown) {
|
|
16
|
+
result.markdown = toMarkdownDocument(content);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function observeSearchSnapshot({ page, state, query, frame, deps = {} }) {
|
|
23
|
+
const waitStable = deps.waitStable ?? waitUntilStable;
|
|
24
|
+
const extractContent = deps.extractContent ?? extractMainContent;
|
|
25
|
+
await waitStable(page, { stableChecks: 2, interval: 150, timeout: 2500 });
|
|
26
|
+
await syncPageState(page, state, { force: true });
|
|
27
|
+
const hints = state.hintMap.map((hint) => ({ ...hint }));
|
|
28
|
+
const ranking = rankAffordances({ hints });
|
|
29
|
+
const searchIds = new Set(ranking.search_input.map((hint) => hint.id));
|
|
30
|
+
const annotatedHints = hints.map((hint) => ({
|
|
31
|
+
...hint,
|
|
32
|
+
semantic: searchIds.has(hint.id)
|
|
33
|
+
? 'search_input'
|
|
34
|
+
: hint.type === 'button'
|
|
35
|
+
? 'submit_control'
|
|
36
|
+
: 'candidate',
|
|
37
|
+
}));
|
|
38
|
+
const content = await extractContent(page);
|
|
39
|
+
const domRevision = state.pageState?.domRevision ?? 0;
|
|
40
|
+
const submitCandidate = ranking.command_button?.[0] ?? null;
|
|
41
|
+
return {
|
|
42
|
+
query,
|
|
43
|
+
title: await page.title(),
|
|
44
|
+
url: page.url(),
|
|
45
|
+
hints: annotatedHints,
|
|
46
|
+
ranking,
|
|
47
|
+
content,
|
|
48
|
+
domRevision,
|
|
49
|
+
submitCandidate,
|
|
50
|
+
frameId: frame?.taskId ?? null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { summarizeExtractedText, toMarkdownDocument } from './content.js';
|
|
2
|
+
|
|
3
|
+
export function buildPageProjection({
|
|
4
|
+
engine,
|
|
5
|
+
surface,
|
|
6
|
+
title,
|
|
7
|
+
url,
|
|
8
|
+
mainText,
|
|
9
|
+
markdown,
|
|
10
|
+
includeMarkdown = false,
|
|
11
|
+
} = {}) {
|
|
12
|
+
const normalizedTitle = String(title ?? '').trim() || 'Untitled';
|
|
13
|
+
const normalizedUrl = String(url ?? '').trim() || 'unknown';
|
|
14
|
+
const normalizedMainText = String(mainText ?? '').trim();
|
|
15
|
+
const result = {
|
|
16
|
+
engine: String(engine ?? 'runtime'),
|
|
17
|
+
surface: String(surface ?? 'content'),
|
|
18
|
+
title: normalizedTitle,
|
|
19
|
+
url: normalizedUrl,
|
|
20
|
+
summary: summarizeExtractedText(normalizedMainText),
|
|
21
|
+
main_text: normalizedMainText,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (markdown !== undefined) {
|
|
25
|
+
result.markdown = markdown;
|
|
26
|
+
} else if (includeMarkdown) {
|
|
27
|
+
result.markdown = toMarkdownDocument({ title: normalizedTitle, text: normalizedMainText });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function createPageState() {
|
|
2
|
+
return {
|
|
3
|
+
lastUrl: null,
|
|
4
|
+
domRevision: 0,
|
|
5
|
+
lastSnapshotHash: null,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function applySnapshotToPageState(state, { url, snapshotHash }) {
|
|
10
|
+
const next = {
|
|
11
|
+
...state,
|
|
12
|
+
lastUrl: url,
|
|
13
|
+
lastSnapshotHash: snapshotHash,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const sameUrl = state.lastUrl === url;
|
|
17
|
+
|
|
18
|
+
if (!sameUrl) {
|
|
19
|
+
next.domRevision = 0;
|
|
20
|
+
} else if (state.lastSnapshotHash && state.lastSnapshotHash !== snapshotHash) {
|
|
21
|
+
next.domRevision = state.domRevision + 1;
|
|
22
|
+
} else {
|
|
23
|
+
next.domRevision = state.domRevision;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { ACTION_NOT_VERIFIED } from './error-codes.js';
|
|
2
|
+
|
|
3
|
+
const EXECUTION_CONTEXT_DESTROYED = 'Execution context was destroyed';
|
|
4
|
+
|
|
5
|
+
function isExecutionContextDestroyed(error) {
|
|
6
|
+
return typeof error?.message === 'string' && error.message.includes(EXECUTION_CONTEXT_DESTROYED);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function verifyTypeResult({
|
|
10
|
+
page,
|
|
11
|
+
expectedText,
|
|
12
|
+
allowPageChange = false,
|
|
13
|
+
prevUrl = null,
|
|
14
|
+
prevDomRevision = null,
|
|
15
|
+
newDomRevision = null,
|
|
16
|
+
}) {
|
|
17
|
+
const currentUrl = page.url();
|
|
18
|
+
const hasPrevDom = typeof prevDomRevision === 'number';
|
|
19
|
+
const hasNewDom = typeof newDomRevision === 'number';
|
|
20
|
+
const domRevisionValue = hasNewDom ? newDomRevision : hasPrevDom ? prevDomRevision : null;
|
|
21
|
+
const domChanged = hasPrevDom && hasNewDom && newDomRevision !== prevDomRevision;
|
|
22
|
+
const urlChanged = prevUrl != null && currentUrl !== prevUrl;
|
|
23
|
+
const navigationObserved = allowPageChange && (domChanged || urlChanged);
|
|
24
|
+
const baseEvidence = {
|
|
25
|
+
url: currentUrl,
|
|
26
|
+
domRevision: domRevisionValue,
|
|
27
|
+
navigationObserved,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const evidence = await page.evaluate(() => {
|
|
32
|
+
const active = document.activeElement;
|
|
33
|
+
const tag = active?.tagName?.toLowerCase() ?? '';
|
|
34
|
+
const value = active?.value ?? '';
|
|
35
|
+
const text = active?.innerText?.trim?.() ?? active?.textContent?.trim?.() ?? '';
|
|
36
|
+
const isFormField = ['input', 'textarea'].includes(tag) || active?.isContentEditable;
|
|
37
|
+
return { value, text, tag, isFormField };
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const wroteExpectedText = evidence.value === expectedText || evidence.text === expectedText;
|
|
41
|
+
if (wroteExpectedText && evidence.isFormField) {
|
|
42
|
+
return { ok: true, evidence };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (navigationObserved) {
|
|
46
|
+
return { ok: true, evidence: baseEvidence };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error_code: ACTION_NOT_VERIFIED,
|
|
52
|
+
retryable: true,
|
|
53
|
+
suggested_next_step: 'reverify',
|
|
54
|
+
evidence,
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (navigationObserved && isExecutionContextDestroyed(error)) {
|
|
58
|
+
return { ok: true, evidence: baseEvidence };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error_code: ACTION_NOT_VERIFIED,
|
|
64
|
+
retryable: true,
|
|
65
|
+
suggested_next_step: 'reverify',
|
|
66
|
+
evidence: { ...baseEvidence, error: error?.message ?? null },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function verifyGenericAction({ page, hintId, prevDomRevision, prevUrl, prevActiveId, newDomRevision }) {
|
|
72
|
+
const currentUrl = page.url();
|
|
73
|
+
const domRevisionValue = typeof newDomRevision === 'number' ? newDomRevision : null;
|
|
74
|
+
const baseEvidence = {
|
|
75
|
+
url: currentUrl,
|
|
76
|
+
domRevision: domRevisionValue,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let snapshot;
|
|
80
|
+
let evaluateError = null;
|
|
81
|
+
try {
|
|
82
|
+
snapshot = await page.evaluate((targetId) => {
|
|
83
|
+
const el = targetId ? document.querySelector(`[data-grasp-id="${targetId}"]`) : null;
|
|
84
|
+
const activeId = document.activeElement?.getAttribute('data-grasp-id') ?? null;
|
|
85
|
+
return {
|
|
86
|
+
elementVisible: !!el,
|
|
87
|
+
activeId,
|
|
88
|
+
};
|
|
89
|
+
}, hintId);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
evaluateError = error;
|
|
92
|
+
snapshot = {
|
|
93
|
+
elementVisible: false,
|
|
94
|
+
activeId: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const domChanged = typeof newDomRevision === 'number' && typeof prevDomRevision === 'number' && newDomRevision !== prevDomRevision;
|
|
99
|
+
const urlChanged = prevUrl != null && currentUrl !== prevUrl;
|
|
100
|
+
const activeChanged = snapshot.activeId !== prevActiveId;
|
|
101
|
+
const navigationObserved = domChanged || urlChanged;
|
|
102
|
+
|
|
103
|
+
if (navigationObserved || activeChanged) {
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
evidence: {
|
|
107
|
+
...baseEvidence,
|
|
108
|
+
elementVisible: snapshot.elementVisible,
|
|
109
|
+
activeId: snapshot.activeId,
|
|
110
|
+
navigationObserved,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error_code: ACTION_NOT_VERIFIED,
|
|
118
|
+
retryable: true,
|
|
119
|
+
suggested_next_step: 'reverify',
|
|
120
|
+
evidence: {
|
|
121
|
+
...baseEvidence,
|
|
122
|
+
elementVisible: snapshot.elementVisible,
|
|
123
|
+
activeId: snapshot.activeId,
|
|
124
|
+
navigationObserved,
|
|
125
|
+
error: evaluateError?.message ?? null,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|