@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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +327 -0
  3. package/README.zh-CN.md +324 -0
  4. package/examples/README.md +31 -0
  5. package/examples/claude-desktop.json +8 -0
  6. package/examples/codex-config.toml +4 -0
  7. package/grasp.skill +0 -0
  8. package/index.js +87 -0
  9. package/package.json +48 -0
  10. package/scripts/grasp_openclaw_ctl.sh +122 -0
  11. package/scripts/run-search-benchmark.mjs +287 -0
  12. package/scripts/update-star-history.mjs +274 -0
  13. package/skill/SKILL.md +61 -0
  14. package/skill/references/tools.md +306 -0
  15. package/src/cli/auto-configure.js +116 -0
  16. package/src/cli/cmd-connect.js +148 -0
  17. package/src/cli/cmd-explain.js +42 -0
  18. package/src/cli/cmd-logs.js +55 -0
  19. package/src/cli/cmd-status.js +119 -0
  20. package/src/cli/config.js +27 -0
  21. package/src/cli/detect-chrome.js +58 -0
  22. package/src/grasp/handoff/events.js +67 -0
  23. package/src/grasp/handoff/persist.js +48 -0
  24. package/src/grasp/handoff/state.js +28 -0
  25. package/src/grasp/page/capture.js +34 -0
  26. package/src/grasp/page/state.js +273 -0
  27. package/src/grasp/verify/evidence.js +40 -0
  28. package/src/grasp/verify/pipeline.js +52 -0
  29. package/src/layer1-bridge/chrome.js +416 -0
  30. package/src/layer1-bridge/webmcp.js +143 -0
  31. package/src/layer2-perception/hints.js +284 -0
  32. package/src/layer3-action/actions.js +400 -0
  33. package/src/runtime/browser-instance.js +65 -0
  34. package/src/runtime/truth/model.js +94 -0
  35. package/src/runtime/truth/snapshot.js +51 -0
  36. package/src/server/affordances.js +47 -0
  37. package/src/server/audit.js +122 -0
  38. package/src/server/boss-fast-path.js +164 -0
  39. package/src/server/boundary-guard.js +53 -0
  40. package/src/server/content.js +97 -0
  41. package/src/server/continuity.js +256 -0
  42. package/src/server/engine-selection.js +29 -0
  43. package/src/server/entry-orchestrator.js +115 -0
  44. package/src/server/error-codes.js +7 -0
  45. package/src/server/explain-share-card.js +113 -0
  46. package/src/server/fast-path-router.js +134 -0
  47. package/src/server/form-runtime.js +602 -0
  48. package/src/server/form-tasks.js +254 -0
  49. package/src/server/gateway-response.js +62 -0
  50. package/src/server/index.js +22 -0
  51. package/src/server/observe.js +52 -0
  52. package/src/server/page-projection.js +31 -0
  53. package/src/server/page-state.js +27 -0
  54. package/src/server/postconditions.js +128 -0
  55. package/src/server/prompt-assembly.js +148 -0
  56. package/src/server/responses.js +44 -0
  57. package/src/server/route-boundary.js +174 -0
  58. package/src/server/route-policy.js +168 -0
  59. package/src/server/runtime-confirmation.js +87 -0
  60. package/src/server/runtime-status.js +7 -0
  61. package/src/server/share-artifacts.js +284 -0
  62. package/src/server/state.js +132 -0
  63. package/src/server/structured-extraction.js +131 -0
  64. package/src/server/surface-prompts.js +166 -0
  65. package/src/server/task-frame.js +11 -0
  66. package/src/server/tasks/search-task.js +321 -0
  67. package/src/server/tools.actions.js +1361 -0
  68. package/src/server/tools.form.js +526 -0
  69. package/src/server/tools.gateway.js +757 -0
  70. package/src/server/tools.handoff.js +210 -0
  71. package/src/server/tools.js +20 -0
  72. package/src/server/tools.legacy.js +983 -0
  73. package/src/server/tools.strategy.js +250 -0
  74. package/src/server/tools.task-surface.js +66 -0
  75. package/src/server/tools.workspace.js +873 -0
  76. package/src/server/workspace-runtime.js +1138 -0
  77. package/src/server/workspace-tasks.js +735 -0
  78. package/start-chrome.bat +84 -0
@@ -0,0 +1,602 @@
1
+ function compactText(value) {
2
+ return String(value ?? '').replace(/\s+/g, ' ').trim();
3
+ }
4
+
5
+ function normalizeText(value) {
6
+ return compactText(value).toLowerCase();
7
+ }
8
+
9
+ function buildUnresolved(reason, requestedField, matches = []) {
10
+ return {
11
+ reason,
12
+ requested_field: compactText(requestedField),
13
+ matches: matches.map((field) => ({
14
+ label: field.label,
15
+ hint_id: field.hint_id ?? null,
16
+ })),
17
+ };
18
+ }
19
+
20
+ function getSnapshotFields(snapshot) {
21
+ return Array.isArray(snapshot?.fields) ? snapshot.fields : [];
22
+ }
23
+
24
+ function isTextLikeField(field) {
25
+ const tag = normalizeText(field?.tag);
26
+ const type = normalizeText(field?.type);
27
+
28
+ if (tag === 'textarea' || type === 'textarea') return true;
29
+ if (type === '') return tag === 'input';
30
+
31
+ return !['checkbox', 'radio', 'select', 'date', 'datetime-local', 'month', 'week', 'time', 'file', 'submit', 'button'].includes(type);
32
+ }
33
+
34
+ function isSelectLikeField(field) {
35
+ const tag = normalizeText(field?.tag);
36
+ const type = normalizeText(field?.type);
37
+ return tag === 'select' || ['select', 'checkbox', 'radio'].includes(type);
38
+ }
39
+
40
+ function isDateLikeField(field) {
41
+ const type = normalizeText(field?.type);
42
+ return ['date', 'datetime-local', 'month', 'week', 'time'].includes(type);
43
+ }
44
+
45
+ function isNeverSupportedField(field) {
46
+ const tag = normalizeText(field?.tag);
47
+ const type = normalizeText(field?.type);
48
+ return tag === 'button' || ['file', 'submit', 'button', 'reset'].includes(type);
49
+ }
50
+
51
+ function isFieldEditable(field) {
52
+ return field?.disabled !== true && field?.readOnly !== true && field?.readonly !== true;
53
+ }
54
+
55
+ function fieldIdentity(field) {
56
+ return {
57
+ hint_id: compactText(field?.hint_id),
58
+ id: compactText(field?.id),
59
+ name: compactText(field?.name),
60
+ label: normalizeText(field?.normalized_label ?? field?.label),
61
+ type: normalizeText(field?.type),
62
+ tag: normalizeText(field?.tag),
63
+ };
64
+ }
65
+
66
+ function findFieldInSnapshot(snapshot, field) {
67
+ const identity = fieldIdentity(field);
68
+ const fields = getSnapshotFields(snapshot);
69
+
70
+ if (identity.hint_id) {
71
+ const match = fields.find((candidate) => compactText(candidate?.hint_id) === identity.hint_id);
72
+ if (match) return match;
73
+ }
74
+
75
+ if (identity.id) {
76
+ const match = fields.find((candidate) => compactText(candidate?.id) === identity.id);
77
+ if (match) return match;
78
+ }
79
+
80
+ if (identity.name) {
81
+ const match = fields.find((candidate) => (
82
+ compactText(candidate?.name) === identity.name
83
+ && normalizeText(candidate?.type) === identity.type
84
+ && normalizeText(candidate?.tag) === identity.tag
85
+ ));
86
+ if (match) return match;
87
+ }
88
+
89
+ const labelMatches = fields.filter((candidate) => (
90
+ normalizeText(candidate?.normalized_label ?? candidate?.label) === identity.label
91
+ ));
92
+
93
+ if (labelMatches.length === 1) {
94
+ return labelMatches[0];
95
+ }
96
+
97
+ return labelMatches.find((candidate) => (
98
+ normalizeText(candidate?.type) === identity.type
99
+ && normalizeText(candidate?.tag) === identity.tag
100
+ )) ?? null;
101
+ }
102
+
103
+ function fieldStateKey(field) {
104
+ return JSON.stringify({
105
+ value: Array.isArray(field?.value) ? field.value : String(field?.value ?? ''),
106
+ checked: field?.checked ?? null,
107
+ current_state: field?.current_state ?? null,
108
+ });
109
+ }
110
+
111
+ function verifyWriteMutation(requestedField, field, snapshot) {
112
+ const refreshedField = findFieldInSnapshot(snapshot, field);
113
+
114
+ if (!refreshedField) {
115
+ return {
116
+ ok: false,
117
+ unresolved: buildUnresolved('no_live_target', requestedField, [field]),
118
+ snapshot,
119
+ };
120
+ }
121
+
122
+ if (fieldStateKey(refreshedField) === fieldStateKey(field)) {
123
+ return {
124
+ ok: false,
125
+ unresolved: buildUnresolved('no_effect', requestedField, [refreshedField]),
126
+ snapshot,
127
+ };
128
+ }
129
+
130
+ return {
131
+ ok: true,
132
+ field: refreshedField,
133
+ snapshot,
134
+ };
135
+ }
136
+
137
+ function buildVerification(fields, blockers) {
138
+ const summary = fields.reduce((acc, field) => {
139
+ if (field.current_state !== 'filled' && field.required) acc.missing_required += 1;
140
+ if (field.current_state !== 'filled' && field.risk_level !== 'safe') acc.risky_pending += 1;
141
+ if (field.current_state === 'unresolved') acc.unresolved += 1;
142
+ return acc;
143
+ }, { missing_required: 0, risky_pending: 0, unresolved: 0 });
144
+
145
+ return {
146
+ blockers,
147
+ summary,
148
+ };
149
+ }
150
+
151
+ function toUnresolvedFromError(requestedField, error, matches = []) {
152
+ const reason = error?.message === 'unsupported_widget'
153
+ ? 'unsupported_widget'
154
+ : error?.message === 'no_live_target'
155
+ ? 'no_live_target'
156
+ : error?.message === 'field_not_editable'
157
+ ? 'field_not_editable'
158
+ : error?.message === 'no_effect'
159
+ ? 'no_effect'
160
+ : null;
161
+
162
+ if (!reason) {
163
+ throw error;
164
+ }
165
+
166
+ return {
167
+ ok: false,
168
+ field: null,
169
+ unresolved: buildUnresolved(reason, requestedField, matches),
170
+ snapshot: null,
171
+ };
172
+ }
173
+
174
+ function normalizeWriterOutcome(requestedField, outcome, matches = [], field = null) {
175
+ if (outcome && outcome.ok === false) {
176
+ return {
177
+ ok: false,
178
+ field: null,
179
+ unresolved: outcome.unresolved ?? buildUnresolved('no_live_target', requestedField, matches),
180
+ snapshot: outcome.snapshot ?? null,
181
+ };
182
+ }
183
+
184
+ return {
185
+ ok: true,
186
+ field,
187
+ evidence: outcome?.evidence,
188
+ snapshot: outcome?.snapshot ?? null,
189
+ };
190
+ }
191
+
192
+ async function runWrite(runtime, requestedField, value, config) {
193
+ const initialSnapshot = runtime?.snapshot ?? runtime;
194
+ const resolution = await resolveFieldTarget(initialSnapshot, requestedField);
195
+ if (!resolution.field) {
196
+ return {
197
+ ok: false,
198
+ field: null,
199
+ unresolved: resolution.unresolved,
200
+ snapshot: initialSnapshot,
201
+ };
202
+ }
203
+
204
+ const field = resolution.field;
205
+ if (!config.supports(field)) {
206
+ return {
207
+ ok: false,
208
+ field,
209
+ unresolved: buildUnresolved('unsupported_widget', requestedField, [field]),
210
+ snapshot: initialSnapshot,
211
+ };
212
+ }
213
+
214
+ if (!isFieldEditable(field)) {
215
+ return {
216
+ ok: false,
217
+ field,
218
+ unresolved: buildUnresolved('field_not_editable', requestedField, [field]),
219
+ snapshot: initialSnapshot,
220
+ };
221
+ }
222
+
223
+ let method = config.fallbackMethod;
224
+ let outcome;
225
+ try {
226
+ if (field.hint_id && typeof config.executeHint === 'function') {
227
+ outcome = await config.executeHint(field, value);
228
+ method = config.hintMethod;
229
+ } else if (typeof config.executeFallback === 'function') {
230
+ outcome = await config.executeFallback(field, value);
231
+ } else {
232
+ return {
233
+ ok: false,
234
+ field,
235
+ unresolved: buildUnresolved('no_live_target', requestedField, [field]),
236
+ snapshot: initialSnapshot,
237
+ };
238
+ }
239
+ } catch (error) {
240
+ return toUnresolvedFromError(requestedField, error, [field]);
241
+ }
242
+
243
+ const normalizedOutcome = normalizeWriterOutcome(requestedField, outcome, [field], field);
244
+ if (!normalizedOutcome.ok) {
245
+ return {
246
+ ok: false,
247
+ field,
248
+ unresolved: normalizedOutcome.unresolved,
249
+ snapshot: normalizedOutcome.snapshot ?? initialSnapshot,
250
+ };
251
+ }
252
+
253
+ const snapshot = normalizedOutcome.snapshot
254
+ ?? (typeof runtime?.refreshSnapshot === 'function'
255
+ ? await runtime.refreshSnapshot()
256
+ : initialSnapshot);
257
+ const verified = verifyWriteMutation(requestedField, field, snapshot);
258
+
259
+ if (!verified.ok) {
260
+ return {
261
+ ok: false,
262
+ field,
263
+ unresolved: verified.unresolved,
264
+ snapshot: verified.snapshot,
265
+ };
266
+ }
267
+
268
+ return {
269
+ ok: true,
270
+ field: verified.field,
271
+ evidence: normalizedOutcome.evidence ?? createWriteEvidence({ field: field.label, method }),
272
+ snapshot: verified.snapshot,
273
+ };
274
+ }
275
+
276
+ export async function resolveFieldTarget(snapshot, requestedField) {
277
+ const normalized = normalizeText(requestedField);
278
+ const matches = getSnapshotFields(snapshot).filter((field) => (
279
+ normalizeText(field?.normalized_label ?? field?.label) === normalized
280
+ ));
281
+ const supportedMatches = matches.filter((field) => !isNeverSupportedField(field));
282
+ const hintBacked = supportedMatches.filter((field) => compactText(field?.hint_id));
283
+
284
+ if (hintBacked.length === 1) {
285
+ return { field: hintBacked[0], ambiguous: false, matches };
286
+ }
287
+
288
+ if (supportedMatches.length === 1) {
289
+ return { field: supportedMatches[0], ambiguous: false, matches };
290
+ }
291
+
292
+ if (hintBacked.length > 1 || supportedMatches.length > 1) {
293
+ return {
294
+ field: null,
295
+ ambiguous: true,
296
+ matches,
297
+ unresolved: buildUnresolved('ambiguous_label', requestedField, hintBacked.length > 0 ? hintBacked : supportedMatches),
298
+ };
299
+ }
300
+
301
+ if (matches.length > 0) {
302
+ return {
303
+ field: null,
304
+ ambiguous: false,
305
+ matches,
306
+ unresolved: buildUnresolved('unsupported_widget', requestedField, matches),
307
+ };
308
+ }
309
+
310
+ return {
311
+ field: null,
312
+ ambiguous: false,
313
+ matches: [],
314
+ unresolved: buildUnresolved('no_live_target', requestedField),
315
+ };
316
+ }
317
+
318
+ export function createWriteEvidence({ field, method }) {
319
+ return {
320
+ field,
321
+ method,
322
+ autosave_possible: true,
323
+ write_side_effect: 'draft_mutation_possible',
324
+ };
325
+ }
326
+
327
+ export async function fillSafeFields(runtime, values) {
328
+ const written = [];
329
+ const skipped = [];
330
+ const unresolved = [];
331
+ const evidence = [];
332
+ let snapshot = runtime?.snapshot ?? runtime;
333
+ const writer = runtime?.writeTextField;
334
+
335
+ for (const [requestedField, value] of Object.entries(values ?? {})) {
336
+ const resolution = await resolveFieldTarget(snapshot, requestedField);
337
+ if (!resolution.field) {
338
+ unresolved.push({
339
+ field: requestedField,
340
+ reason: resolution.unresolved.reason,
341
+ });
342
+ continue;
343
+ }
344
+
345
+ const field = resolution.field;
346
+ if (field.risk_level !== 'safe') {
347
+ skipped.push({
348
+ field: field.label,
349
+ reason: 'risk_not_safe',
350
+ risk_level: field.risk_level,
351
+ });
352
+ continue;
353
+ }
354
+
355
+ if (!isTextLikeField(field)) {
356
+ unresolved.push({
357
+ field: field.label,
358
+ reason: 'unsupported_widget',
359
+ });
360
+ continue;
361
+ }
362
+
363
+ if (typeof writer !== 'function') {
364
+ unresolved.push({
365
+ field: field.label,
366
+ reason: 'no_live_target',
367
+ });
368
+ continue;
369
+ }
370
+
371
+ const outcome = await writer(field, value);
372
+ if (outcome && outcome.ok === false) {
373
+ unresolved.push({
374
+ field: field.label,
375
+ reason: outcome.unresolved?.reason ?? 'no_live_target',
376
+ });
377
+ continue;
378
+ }
379
+ written.push({ field: field.label, value });
380
+ evidence.push(outcome?.evidence ?? createWriteEvidence({
381
+ field: field.label,
382
+ method: field.hint_id ? 'type_hint' : 'write_field',
383
+ }));
384
+ if (outcome?.snapshot) {
385
+ snapshot = outcome.snapshot;
386
+ }
387
+ }
388
+
389
+ return {
390
+ written,
391
+ skipped,
392
+ unresolved,
393
+ evidence,
394
+ snapshot,
395
+ };
396
+ }
397
+
398
+ async function applyReviewedWrite(runtime, requestedField, value, writer, method, supports) {
399
+ const snapshot = runtime?.snapshot ?? runtime;
400
+ const resolution = await resolveFieldTarget(snapshot, requestedField);
401
+ if (!resolution.field) {
402
+ return {
403
+ status: 'unresolved',
404
+ unresolved: resolution.unresolved,
405
+ snapshot,
406
+ };
407
+ }
408
+
409
+ const field = resolution.field;
410
+ if (field.risk_level === 'sensitive') {
411
+ return {
412
+ status: 'blocked',
413
+ field: field.label,
414
+ reason: 'risk_sensitive',
415
+ snapshot,
416
+ };
417
+ }
418
+
419
+ if (field.risk_level !== 'review') {
420
+ return {
421
+ status: 'blocked',
422
+ field: field.label,
423
+ reason: 'risk_not_review',
424
+ snapshot,
425
+ };
426
+ }
427
+
428
+ if (typeof supports === 'function' && !supports(field)) {
429
+ return {
430
+ status: 'unresolved',
431
+ field: field.label,
432
+ unresolved: buildUnresolved('unsupported_widget', requestedField, [field]),
433
+ snapshot,
434
+ };
435
+ }
436
+
437
+ if (!isFieldEditable(field)) {
438
+ return {
439
+ status: 'unresolved',
440
+ field: field.label,
441
+ unresolved: buildUnresolved('field_not_editable', requestedField, [field]),
442
+ snapshot,
443
+ };
444
+ }
445
+
446
+ if (typeof writer !== 'function') {
447
+ return {
448
+ status: 'unresolved',
449
+ field: field.label,
450
+ unresolved: buildUnresolved('no_live_target', requestedField, [field]),
451
+ snapshot,
452
+ };
453
+ }
454
+
455
+ let outcome;
456
+ try {
457
+ outcome = await writer(field, value);
458
+ } catch (error) {
459
+ return toUnresolvedFromError(requestedField, error, [field]);
460
+ }
461
+
462
+ if (outcome && outcome.ok === false) {
463
+ return {
464
+ status: 'unresolved',
465
+ field: field.label,
466
+ unresolved: outcome.unresolved ?? buildUnresolved('no_live_target', requestedField, [field]),
467
+ snapshot: outcome.snapshot ?? snapshot,
468
+ };
469
+ }
470
+
471
+ const verified = verifyWriteMutation(requestedField, field, outcome?.snapshot ?? snapshot);
472
+
473
+ if (!verified.ok) {
474
+ return {
475
+ status: 'unresolved',
476
+ field: field.label,
477
+ unresolved: verified.unresolved,
478
+ snapshot: verified.snapshot,
479
+ };
480
+ }
481
+
482
+ return {
483
+ status: 'written',
484
+ field: field.label,
485
+ value,
486
+ evidence: outcome?.evidence ?? createWriteEvidence({ field: field.label, method }),
487
+ snapshot: verified.snapshot,
488
+ };
489
+ }
490
+
491
+ export async function applyReviewedControl(runtime, requestedField, value) {
492
+ return applyReviewedWrite(runtime, requestedField, value, runtime?.setControlValue, 'set_control_hint', isSelectLikeField);
493
+ }
494
+
495
+ export async function applyReviewedDate(runtime, requestedField, value) {
496
+ return applyReviewedWrite(runtime, requestedField, value, runtime?.setDateValue, 'set_date_hint', isDateLikeField);
497
+ }
498
+
499
+ export async function previewSubmit(runtime, snapshot, options = {}) {
500
+ const mode = options.mode ?? 'preview';
501
+ const fields = snapshot?.fields ?? [];
502
+ const blockers = fields
503
+ .filter((field) => field.current_state !== 'filled' && (field.required || field.risk_level !== 'safe' || field.current_state === 'unresolved'))
504
+ .map((field) => ({
505
+ field: field.label,
506
+ reason: field.current_state === 'unresolved'
507
+ ? 'unresolved'
508
+ : field.required
509
+ ? 'required_missing'
510
+ : `risk_${field.risk_level}`,
511
+ }));
512
+ const verification = buildVerification(fields, blockers);
513
+ const base = {
514
+ autosave_possible: true,
515
+ submit_controls: snapshot?.submit_controls ?? [],
516
+ verification,
517
+ };
518
+
519
+ if (mode !== 'confirm') {
520
+ return {
521
+ mode: 'preview',
522
+ blocked: blockers.length > 0,
523
+ blockers,
524
+ ...base,
525
+ };
526
+ }
527
+
528
+ if (blockers.length > 0) {
529
+ return {
530
+ mode: 'confirm',
531
+ blocked: true,
532
+ blockers,
533
+ ...base,
534
+ };
535
+ }
536
+
537
+ if (options.confirmation !== 'SUBMIT') {
538
+ return {
539
+ mode: 'confirm',
540
+ blocked: true,
541
+ reason: 'confirmation_required',
542
+ blockers: [],
543
+ ...base,
544
+ };
545
+ }
546
+
547
+ const control = snapshot?.submit_controls?.[0];
548
+ if (!control || typeof runtime?.clickSubmit !== 'function') {
549
+ return {
550
+ mode: 'confirm',
551
+ blocked: true,
552
+ reason: 'no_submit_control',
553
+ blockers: [],
554
+ ...base,
555
+ };
556
+ }
557
+
558
+ await runtime.clickSubmit(control);
559
+ return {
560
+ mode: 'confirm',
561
+ blocked: false,
562
+ submitted: true,
563
+ blockers: [],
564
+ ...base,
565
+ evidence: {
566
+ field: control.label,
567
+ method: 'submit_click',
568
+ autosave_possible: false,
569
+ write_side_effect: 'submit_attempted',
570
+ },
571
+ };
572
+ }
573
+
574
+ export async function writeTextField(runtime, requestedField, value) {
575
+ return runWrite(runtime, requestedField, value, {
576
+ supports: isTextLikeField,
577
+ executeHint: runtime?.typeByHintId,
578
+ executeFallback: runtime?.writeByField,
579
+ hintMethod: 'type_hint',
580
+ fallbackMethod: 'write_field',
581
+ });
582
+ }
583
+
584
+ export async function setControlValue(runtime, requestedField, value) {
585
+ return runWrite(runtime, requestedField, value, {
586
+ supports: isSelectLikeField,
587
+ executeHint: runtime?.setControlByHintId,
588
+ executeFallback: runtime?.setControlByField,
589
+ hintMethod: 'set_control_hint',
590
+ fallbackMethod: 'set_control_field',
591
+ });
592
+ }
593
+
594
+ export async function setDateValue(runtime, requestedField, value) {
595
+ return runWrite(runtime, requestedField, value, {
596
+ supports: isDateLikeField,
597
+ executeHint: runtime?.setDateByHintId,
598
+ executeFallback: runtime?.setDateByField,
599
+ hintMethod: 'set_date_hint',
600
+ fallbackMethod: 'set_date_field',
601
+ });
602
+ }