@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,256 @@
|
|
|
1
|
+
import { deriveWorkspaceHintItems } from './workspace-tasks.js';
|
|
2
|
+
|
|
3
|
+
export function buildCheckpointHandoffSuggestion(pageState = {}, pageUrl = '') {
|
|
4
|
+
const checkpointKind = pageState.checkpointKind ?? 'unknown';
|
|
5
|
+
const role = pageState.currentRole ?? 'unknown';
|
|
6
|
+
const reason = checkpointKind === 'waiting_room'
|
|
7
|
+
? 'checkpoint_waiting_room'
|
|
8
|
+
: checkpointKind === 'challenge'
|
|
9
|
+
? 'checkpoint_challenge'
|
|
10
|
+
: checkpointKind === 'verification'
|
|
11
|
+
? 'checkpoint_verification'
|
|
12
|
+
: 'checkpoint_required';
|
|
13
|
+
|
|
14
|
+
const note = `Checkpoint detected (${checkpointKind}) at ${pageUrl || 'current page'}; human presence may be required before continuation can resume.`;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
reason,
|
|
18
|
+
note,
|
|
19
|
+
expected_url_contains: (() => {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(pageUrl);
|
|
22
|
+
return url.hostname;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
})(),
|
|
27
|
+
expected_page_role: role === 'checkpoint' ? null : role,
|
|
28
|
+
expected_selector: null,
|
|
29
|
+
continuation_goal: 'resume after checkpoint clearance',
|
|
30
|
+
expected_hint_label: null,
|
|
31
|
+
checkpoint_kind: checkpointKind,
|
|
32
|
+
checkpoint_signals: pageState.checkpointSignals ?? [],
|
|
33
|
+
suggested_next_action: pageState.suggestedNextAction ?? 'handoff_required',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildSessionTrustPreflight(targetUrl, pageState = {}, handoff = {}) {
|
|
38
|
+
let hostname = null;
|
|
39
|
+
let currentHostname = null;
|
|
40
|
+
try {
|
|
41
|
+
hostname = new URL(targetUrl).hostname.toLowerCase();
|
|
42
|
+
} catch {}
|
|
43
|
+
try {
|
|
44
|
+
currentHostname = pageState?.lastUrl ? new URL(pageState.lastUrl).hostname.toLowerCase() : null;
|
|
45
|
+
} catch {}
|
|
46
|
+
|
|
47
|
+
const sameTargetContext = !!(hostname && currentHostname && hostname === currentHostname);
|
|
48
|
+
|
|
49
|
+
const highRiskHost = hostname && (
|
|
50
|
+
hostname.includes('chatgpt.com') ||
|
|
51
|
+
hostname.includes('openai.com') ||
|
|
52
|
+
hostname.includes('github.com')
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const checkpointActive = sameTargetContext && (pageState.currentRole === 'checkpoint' || pageState.riskGateDetected === true);
|
|
56
|
+
const priorHandoffForSameHost = !!(handoff.expected_url_contains && hostname && handoff.expected_url_contains.includes(hostname));
|
|
57
|
+
|
|
58
|
+
let sessionTrust = 'medium';
|
|
59
|
+
let recommendedEntryStrategy = 'direct';
|
|
60
|
+
const trustSignals = [];
|
|
61
|
+
|
|
62
|
+
if (highRiskHost) trustSignals.push('high_risk_host');
|
|
63
|
+
if (priorHandoffForSameHost) trustSignals.push('prior_handoff_context_for_host');
|
|
64
|
+
if (sameTargetContext) {
|
|
65
|
+
trustSignals.push('same_target_context');
|
|
66
|
+
}
|
|
67
|
+
if (checkpointActive) trustSignals.push('active_checkpoint_detected');
|
|
68
|
+
|
|
69
|
+
if (highRiskHost && checkpointActive) {
|
|
70
|
+
sessionTrust = 'low';
|
|
71
|
+
recommendedEntryStrategy = 'handoff_or_preheat';
|
|
72
|
+
} else if (highRiskHost && priorHandoffForSameHost) {
|
|
73
|
+
sessionTrust = 'medium';
|
|
74
|
+
recommendedEntryStrategy = 'resume_existing_session';
|
|
75
|
+
} else if (highRiskHost) {
|
|
76
|
+
sessionTrust = 'low';
|
|
77
|
+
recommendedEntryStrategy = 'preheat_before_direct_entry';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
target_url: targetUrl,
|
|
82
|
+
hostname,
|
|
83
|
+
current_hostname: currentHostname,
|
|
84
|
+
same_target_context: sameTargetContext,
|
|
85
|
+
session_trust: sessionTrust,
|
|
86
|
+
recommended_entry_strategy: recommendedEntryStrategy,
|
|
87
|
+
trust_signals: trustSignals,
|
|
88
|
+
checkpoint_active: checkpointActive,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function assessResumeContinuation(page, state, anchors = {}) {
|
|
93
|
+
const {
|
|
94
|
+
expected_url_contains = null,
|
|
95
|
+
expected_page_role = null,
|
|
96
|
+
expected_selector = null,
|
|
97
|
+
continuation_goal = null,
|
|
98
|
+
expected_hint_label = null,
|
|
99
|
+
} = anchors;
|
|
100
|
+
|
|
101
|
+
const checks = [];
|
|
102
|
+
const currentUrl = page?.url?.() ?? '';
|
|
103
|
+
const currentRole = state?.pageState?.currentRole ?? 'unknown';
|
|
104
|
+
|
|
105
|
+
if (expected_url_contains) {
|
|
106
|
+
checks.push({ kind: 'url_contains', expected: expected_url_contains, ok: currentUrl.includes(expected_url_contains), actual: currentUrl });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (expected_page_role) {
|
|
110
|
+
checks.push({ kind: 'page_role', expected: expected_page_role, ok: currentRole === expected_page_role, actual: currentRole });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (expected_selector) {
|
|
114
|
+
let found = false;
|
|
115
|
+
try {
|
|
116
|
+
found = await page.evaluate((selector) => Boolean(document.querySelector(selector)), expected_selector);
|
|
117
|
+
} catch {}
|
|
118
|
+
checks.push({ kind: 'selector_present', expected: expected_selector, ok: found, actual: found });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (expected_hint_label) {
|
|
122
|
+
const hintMatch = (state?.hintMap ?? []).find((hint) =>
|
|
123
|
+
String(hint?.label ?? '').toLowerCase().includes(String(expected_hint_label).toLowerCase())
|
|
124
|
+
);
|
|
125
|
+
checks.push({ kind: 'hint_label_present', expected: expected_hint_label, ok: Boolean(hintMatch), actual: hintMatch?.label ?? null });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const requiredChecks = checks.length;
|
|
129
|
+
const passedChecks = checks.filter((check) => check.ok).length;
|
|
130
|
+
const taskContinuationOk = requiredChecks === 0 ? null : passedChecks === requiredChecks;
|
|
131
|
+
const continuationReady = taskContinuationOk === true && (!expected_hint_label || checks.some((check) => check.kind === 'hint_label_present' && check.ok));
|
|
132
|
+
const suggestedNextAction = continuationReady
|
|
133
|
+
? expected_hint_label
|
|
134
|
+
? `use_hint_matching:${expected_hint_label}`
|
|
135
|
+
: continuation_goal
|
|
136
|
+
? `continue_goal:${continuation_goal}`
|
|
137
|
+
: 'continue_task'
|
|
138
|
+
: taskContinuationOk === false
|
|
139
|
+
? 'do_not_continue'
|
|
140
|
+
: 'needs_confirmation';
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
required_checks: requiredChecks,
|
|
144
|
+
passed_checks: passedChecks,
|
|
145
|
+
task_continuation_ok: taskContinuationOk,
|
|
146
|
+
continuation_ready: continuationReady,
|
|
147
|
+
continuation_goal,
|
|
148
|
+
suggested_next_action: suggestedNextAction,
|
|
149
|
+
checks,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function assessGatewayContinuation(page, state) {
|
|
154
|
+
const handoffState = state?.handoff?.state ?? 'idle';
|
|
155
|
+
const pageState = state?.pageState ?? {};
|
|
156
|
+
const gatedByPage = pageState.currentRole === 'checkpoint' || pageState.riskGateDetected === true;
|
|
157
|
+
const anchors = {
|
|
158
|
+
expected_url_contains: state?.handoff?.expected_url_contains ?? null,
|
|
159
|
+
expected_page_role: state?.handoff?.expected_page_role ?? null,
|
|
160
|
+
expected_selector: state?.handoff?.expected_selector ?? null,
|
|
161
|
+
continuation_goal: state?.handoff?.continuation_goal ?? null,
|
|
162
|
+
expected_hint_label: state?.handoff?.expected_hint_label ?? null,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (handoffState === 'handoff_required' || handoffState === 'handoff_in_progress' || handoffState === 'awaiting_reacquisition') {
|
|
166
|
+
return {
|
|
167
|
+
status: 'handoff_required',
|
|
168
|
+
continuation: {
|
|
169
|
+
can_continue: false,
|
|
170
|
+
suggested_next_action: 'request_handoff',
|
|
171
|
+
handoff_state: handoffState,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (gatedByPage) {
|
|
177
|
+
return {
|
|
178
|
+
status: 'gated',
|
|
179
|
+
continuation: {
|
|
180
|
+
can_continue: false,
|
|
181
|
+
suggested_next_action: 'request_handoff',
|
|
182
|
+
handoff_state: handoffState,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const continuation = await assessResumeContinuation(page, state, anchors);
|
|
188
|
+
const workspaceHintItems = deriveWorkspaceHintItems(state?.hintMap ?? []);
|
|
189
|
+
const workspaceLike = pageState.currentRole === 'workspace'
|
|
190
|
+
|| pageState.currentRole === 'navigation-heavy'
|
|
191
|
+
|| pageState.workspaceSurface === 'list'
|
|
192
|
+
|| (pageState.workspaceSignals ?? []).includes('workspace_navigation')
|
|
193
|
+
|| workspaceHintItems.length > 0;
|
|
194
|
+
const suggestedDirectAction = workspaceLike
|
|
195
|
+
? 'workspace_inspect'
|
|
196
|
+
: pageState.currentRole === 'form' || pageState.currentRole === 'auth'
|
|
197
|
+
? 'form_inspect'
|
|
198
|
+
: continuation.suggested_next_action === 'needs_confirmation'
|
|
199
|
+
? 'extract'
|
|
200
|
+
: continuation.suggested_next_action;
|
|
201
|
+
|
|
202
|
+
if (handoffState === 'resumed_verified' || handoffState === 'resumed_unverified') {
|
|
203
|
+
if (continuation.task_continuation_ok === false) {
|
|
204
|
+
return {
|
|
205
|
+
status: 'failed',
|
|
206
|
+
continuation: {
|
|
207
|
+
...continuation,
|
|
208
|
+
can_continue: false,
|
|
209
|
+
handoff_state: handoffState,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (continuation.continuation_ready) {
|
|
215
|
+
return {
|
|
216
|
+
status: 'resumed',
|
|
217
|
+
continuation: {
|
|
218
|
+
...continuation,
|
|
219
|
+
suggested_next_action: suggestedDirectAction,
|
|
220
|
+
can_continue: true,
|
|
221
|
+
handoff_state: handoffState,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
status: 'failed',
|
|
228
|
+
continuation: {
|
|
229
|
+
...continuation,
|
|
230
|
+
can_continue: false,
|
|
231
|
+
handoff_state: handoffState,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (continuation.task_continuation_ok === false) {
|
|
237
|
+
return {
|
|
238
|
+
status: 'failed',
|
|
239
|
+
continuation: {
|
|
240
|
+
...continuation,
|
|
241
|
+
can_continue: false,
|
|
242
|
+
handoff_state: handoffState,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
status: 'direct',
|
|
249
|
+
continuation: {
|
|
250
|
+
...continuation,
|
|
251
|
+
suggested_next_action: suggestedDirectAction,
|
|
252
|
+
can_continue: true,
|
|
253
|
+
handoff_state: handoffState,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function normalizeHostname(url) {
|
|
2
|
+
try {
|
|
3
|
+
return new URL(String(url ?? '')).hostname.toLowerCase();
|
|
4
|
+
} catch {
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isRuntimeHost(hostname) {
|
|
10
|
+
return hostname === 'bosszhipin.com'
|
|
11
|
+
|| hostname === 'zhipin.com'
|
|
12
|
+
|| hostname.endsWith('.bosszhipin.com')
|
|
13
|
+
|| hostname.endsWith('.zhipin.com')
|
|
14
|
+
|| hostname === 'mp.weixin.qq.com'
|
|
15
|
+
|| hostname.endsWith('.mp.weixin.qq.com')
|
|
16
|
+
|| hostname === 'xiaohongshu.com'
|
|
17
|
+
|| hostname === 'www.xiaohongshu.com'
|
|
18
|
+
|| hostname.endsWith('.xiaohongshu.com')
|
|
19
|
+
|| hostname === 'xhslink.com'
|
|
20
|
+
|| hostname.endsWith('.xhslink.com');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function selectEngine({ tool, url } = {}) {
|
|
24
|
+
const hostname = normalizeHostname(url);
|
|
25
|
+
return {
|
|
26
|
+
tool: tool ?? 'extract',
|
|
27
|
+
engine: isRuntimeHost(hostname) ? 'runtime' : 'data',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
function normalizeUrl(url) {
|
|
2
|
+
try {
|
|
3
|
+
const parsed = new URL(url);
|
|
4
|
+
if (parsed.pathname !== '/' && parsed.pathname.endsWith('/')) {
|
|
5
|
+
parsed.pathname = parsed.pathname.slice(0, -1);
|
|
6
|
+
}
|
|
7
|
+
parsed.hash = '';
|
|
8
|
+
return parsed.toString();
|
|
9
|
+
} catch {
|
|
10
|
+
return url ?? null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function readPageAvailability(page) {
|
|
15
|
+
if (!page) {
|
|
16
|
+
return { available: false, finalUrl: null };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (page.isClosed?.() === true) {
|
|
20
|
+
return { available: false, finalUrl: null };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const finalUrl = page.url?.() ?? null;
|
|
24
|
+
if (!finalUrl) {
|
|
25
|
+
return { available: false, finalUrl: null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
if (typeof page.evaluate === 'function') {
|
|
30
|
+
await page.evaluate(() => document.location.href);
|
|
31
|
+
}
|
|
32
|
+
return { available: true, finalUrl };
|
|
33
|
+
} catch {
|
|
34
|
+
return { available: false, finalUrl };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createEntryOrchestrator({
|
|
39
|
+
directGoto,
|
|
40
|
+
trustedContextOpen,
|
|
41
|
+
} = {}) {
|
|
42
|
+
const runStrategy = async (strategy, targetUrl, state) => {
|
|
43
|
+
if (strategy === 'direct_goto') {
|
|
44
|
+
return directGoto(targetUrl, { state });
|
|
45
|
+
}
|
|
46
|
+
if (strategy === 'trusted_context_open') {
|
|
47
|
+
return trustedContextOpen(targetUrl, { state });
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Unknown entry strategy: ${strategy}`);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
async run({ targetUrl, strategies = [], state = null }) {
|
|
54
|
+
const attempts = [];
|
|
55
|
+
let lastAttempt = {
|
|
56
|
+
page: null,
|
|
57
|
+
finalUrl: null,
|
|
58
|
+
strategy: strategies[0] ?? null,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const strategy of strategies) {
|
|
62
|
+
lastAttempt = {
|
|
63
|
+
page: null,
|
|
64
|
+
finalUrl: null,
|
|
65
|
+
strategy,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const page = await runStrategy(strategy, targetUrl, state);
|
|
70
|
+
const { available, finalUrl } = await readPageAvailability(page);
|
|
71
|
+
const verified = normalizeUrl(finalUrl) === normalizeUrl(targetUrl);
|
|
72
|
+
|
|
73
|
+
lastAttempt = {
|
|
74
|
+
page,
|
|
75
|
+
finalUrl,
|
|
76
|
+
strategy,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
attempts.push({
|
|
80
|
+
strategy,
|
|
81
|
+
final_url: finalUrl,
|
|
82
|
+
page_available: available,
|
|
83
|
+
verified,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (verified) {
|
|
87
|
+
return {
|
|
88
|
+
page,
|
|
89
|
+
entry_method: strategy,
|
|
90
|
+
final_url: finalUrl,
|
|
91
|
+
verified: true,
|
|
92
|
+
evidence: { attempts },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
attempts.push({
|
|
97
|
+
strategy,
|
|
98
|
+
final_url: null,
|
|
99
|
+
page_available: false,
|
|
100
|
+
verified: false,
|
|
101
|
+
error: error.message,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
page: lastAttempt.page,
|
|
108
|
+
entry_method: lastAttempt.strategy,
|
|
109
|
+
final_url: lastAttempt.finalUrl,
|
|
110
|
+
verified: false,
|
|
111
|
+
evidence: { attempts },
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const ACTION_NOT_VERIFIED = 'ACTION_NOT_VERIFIED';
|
|
2
|
+
export const TYPE_FAILED = 'TYPE_FAILED';
|
|
3
|
+
export const NO_EFFECT = 'NO_EFFECT';
|
|
4
|
+
export const LOADING_PENDING = 'LOADING_PENDING';
|
|
5
|
+
export const EXECUTION_FAILED = 'EXECUTION_FAILED';
|
|
6
|
+
export const ROUTE_BLOCKED = 'ROUTE_BLOCKED';
|
|
7
|
+
export const BOUNDARY_MISMATCH = 'BOUNDARY_MISMATCH';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const PRETEXT_MODULE_URL = new URL('../../node_modules/@chenglou/pretext/dist/layout.js', import.meta.url).href;
|
|
2
|
+
|
|
3
|
+
function normalizeText(value, fallback = '') {
|
|
4
|
+
return String(value ?? fallback).trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function fallbackLayout(text, maxWidth, lineHeight) {
|
|
8
|
+
const normalized = normalizeText(text);
|
|
9
|
+
if (!normalized) {
|
|
10
|
+
return { height: lineHeight, lineCount: 1 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const charsPerLine = Math.max(10, Math.floor(maxWidth / 9));
|
|
14
|
+
const lineCount = Math.max(1, Math.ceil(normalized.length / charsPerLine));
|
|
15
|
+
return {
|
|
16
|
+
height: lineCount * lineHeight,
|
|
17
|
+
lineCount,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function defaultCreateScratchPage(page) {
|
|
22
|
+
const context = page?.context?.();
|
|
23
|
+
if (!context || typeof context.newPage !== 'function') {
|
|
24
|
+
throw new Error('Pretext measurement requires a browser page context.');
|
|
25
|
+
}
|
|
26
|
+
return context.newPage();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function measureWithPretext(page, blocks, deps = {}) {
|
|
30
|
+
const createScratchPage = deps.createScratchPage ?? defaultCreateScratchPage;
|
|
31
|
+
const scratchPage = await createScratchPage(page);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await scratchPage.setContent('<!doctype html><html><body></body></html>', { waitUntil: 'load' });
|
|
35
|
+
await scratchPage.addScriptTag({
|
|
36
|
+
type: 'module',
|
|
37
|
+
content: `
|
|
38
|
+
import { prepare, layout } from ${JSON.stringify(PRETEXT_MODULE_URL)};
|
|
39
|
+
window.__graspPretextMeasure = async (blocks) => {
|
|
40
|
+
return blocks.map((block) => {
|
|
41
|
+
const prepared = prepare(block.text, block.font, { whiteSpace: block.whiteSpace ?? 'normal' });
|
|
42
|
+
const result = layout(prepared, block.maxWidth, block.lineHeight);
|
|
43
|
+
return {
|
|
44
|
+
key: block.key,
|
|
45
|
+
height: result.height,
|
|
46
|
+
lineCount: result.lineCount,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
`,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return scratchPage.evaluate(async (input) => {
|
|
54
|
+
return window.__graspPretextMeasure(input);
|
|
55
|
+
}, blocks);
|
|
56
|
+
} finally {
|
|
57
|
+
await scratchPage.close?.().catch?.(() => {});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function buildExplainShareCard(page, projection, options = {}, deps = {}) {
|
|
62
|
+
const width = Number(options.width ?? 640);
|
|
63
|
+
const title = normalizeText(projection?.title, 'Untitled');
|
|
64
|
+
const summary = normalizeText(projection?.summary, projection?.main_text);
|
|
65
|
+
const bodyExcerpt = normalizeText(projection?.main_text).slice(0, 420);
|
|
66
|
+
const blocks = [
|
|
67
|
+
{ key: 'title', text: title, font: '600 28px Arial', maxWidth: width, lineHeight: 34 },
|
|
68
|
+
{ key: 'summary', text: summary, font: '16px Arial', maxWidth: width, lineHeight: 24 },
|
|
69
|
+
{ key: 'body', text: bodyExcerpt, font: '14px Arial', maxWidth: width, lineHeight: 22, whiteSpace: 'pre-wrap' },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
let engine = 'fallback';
|
|
73
|
+
let measured = blocks.map((block) => ({
|
|
74
|
+
key: block.key,
|
|
75
|
+
...fallbackLayout(block.text, block.maxWidth, block.lineHeight),
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const nextMeasured = await measureWithPretext(page, blocks, deps);
|
|
80
|
+
if (Array.isArray(nextMeasured) && nextMeasured.length === blocks.length) {
|
|
81
|
+
measured = nextMeasured;
|
|
82
|
+
engine = 'pretext';
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// fall back to deterministic approximation
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const byKey = Object.fromEntries(measured.map((item) => [item.key, item]));
|
|
89
|
+
const estimatedHeight = (byKey.title?.height ?? 0)
|
|
90
|
+
+ (byKey.summary?.height ?? 0)
|
|
91
|
+
+ (byKey.body?.height ?? 0)
|
|
92
|
+
+ 220;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
engine,
|
|
96
|
+
width,
|
|
97
|
+
estimated_height: estimatedHeight,
|
|
98
|
+
title_lines: byKey.title?.lineCount ?? 1,
|
|
99
|
+
summary_lines: byKey.summary?.lineCount ?? 1,
|
|
100
|
+
body_lines: byKey.body?.lineCount ?? 1,
|
|
101
|
+
body_excerpt: bodyExcerpt,
|
|
102
|
+
card_markdown: [
|
|
103
|
+
`# ${title}`,
|
|
104
|
+
'',
|
|
105
|
+
`Source: ${normalizeText(projection?.url, 'unknown')}`,
|
|
106
|
+
'',
|
|
107
|
+
`Summary: ${summary}`,
|
|
108
|
+
'',
|
|
109
|
+
`Layout engine: ${engine}`,
|
|
110
|
+
`Estimated height: ${estimatedHeight}px`,
|
|
111
|
+
].join('\n'),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { bossFastPathAdapter } from './boss-fast-path.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ADAPTER_DIR = path.join(os.homedir(), '.grasp', 'site-adapters');
|
|
10
|
+
|
|
11
|
+
function normalizeEntry(value) {
|
|
12
|
+
return String(value ?? '').trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseSkillEntry(source) {
|
|
16
|
+
const text = String(source ?? '');
|
|
17
|
+
const frontmatterMatch = text.match(/^---\s*([\s\S]*?)\s*---/);
|
|
18
|
+
const candidates = [
|
|
19
|
+
frontmatterMatch?.[1] ?? '',
|
|
20
|
+
text,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
const entryMatch = candidate.match(/^(?:entry|adapter)\s*:\s*(.+)$/m);
|
|
25
|
+
if (entryMatch) {
|
|
26
|
+
return normalizeEntry(entryMatch[1]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeAdapter(candidate) {
|
|
34
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const matches = typeof candidate.matches === 'function'
|
|
39
|
+
? candidate.matches.bind(candidate)
|
|
40
|
+
: typeof candidate.match === 'function'
|
|
41
|
+
? candidate.match.bind(candidate)
|
|
42
|
+
: null;
|
|
43
|
+
const read = typeof candidate.read === 'function'
|
|
44
|
+
? candidate.read.bind(candidate)
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
if (!matches || !read) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
id: normalizeEntry(candidate.id) || 'external-adapter',
|
|
53
|
+
matches,
|
|
54
|
+
read,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function importAdapterModule(modulePath, loader = (target) => import(target)) {
|
|
59
|
+
const mod = await loader(pathToFileURL(modulePath).href);
|
|
60
|
+
return normalizeAdapter(mod.default ?? mod.adapter ?? mod);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadAdaptersFromDir(dirPath, deps = {}) {
|
|
64
|
+
const {
|
|
65
|
+
readdirImpl = readdir,
|
|
66
|
+
readFileImpl = readFile,
|
|
67
|
+
importModule = (target) => import(target),
|
|
68
|
+
} = deps;
|
|
69
|
+
|
|
70
|
+
if (!dirPath || !existsSync(dirPath)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const entries = await readdirImpl(dirPath, { withFileTypes: true });
|
|
75
|
+
const adapters = [];
|
|
76
|
+
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (!entry.isFile()) continue;
|
|
79
|
+
|
|
80
|
+
const filePath = path.join(dirPath, entry.name);
|
|
81
|
+
if (entry.name.endsWith('.js')) {
|
|
82
|
+
const adapter = await importAdapterModule(filePath, importModule);
|
|
83
|
+
if (adapter) adapters.push(adapter);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (entry.name.endsWith('.skill')) {
|
|
88
|
+
const skillSource = await readFileImpl(filePath, 'utf8');
|
|
89
|
+
const relativeEntry = parseSkillEntry(skillSource);
|
|
90
|
+
if (!relativeEntry) continue;
|
|
91
|
+
const targetPath = path.resolve(path.dirname(filePath), relativeEntry);
|
|
92
|
+
const adapter = await importAdapterModule(targetPath, importModule);
|
|
93
|
+
if (adapter) adapters.push(adapter);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return adapters;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function resolveFastPathAdapters(deps = {}) {
|
|
101
|
+
const adapterDirs = Array.isArray(deps.adapterDirs)
|
|
102
|
+
? deps.adapterDirs
|
|
103
|
+
: [process.env.GRASP_SITE_ADAPTER_DIR || DEFAULT_ADAPTER_DIR].filter(Boolean);
|
|
104
|
+
const externalAdapters = [];
|
|
105
|
+
|
|
106
|
+
for (const dirPath of adapterDirs) {
|
|
107
|
+
const loaded = await loadAdaptersFromDir(dirPath, deps);
|
|
108
|
+
externalAdapters.push(...loaded);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [bossFastPathAdapter, ...externalAdapters];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function readFastPath(page, deps = {}) {
|
|
115
|
+
const adapters = await resolveFastPathAdapters(deps);
|
|
116
|
+
const currentUrl = page.url();
|
|
117
|
+
|
|
118
|
+
for (const adapter of adapters) {
|
|
119
|
+
if (!adapter.matches(currentUrl)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await adapter.read(page, { url: currentUrl });
|
|
124
|
+
if (result) {
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function readBossFastPath(page) {
|
|
133
|
+
return readFastPath(page, { adapterDirs: [] });
|
|
134
|
+
}
|