@usercase/mcp 0.2.11-next.2 → 0.2.11-next.3
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/mcp/hub.js +10 -1
- package/package.json +1 -1
- package/src/browser/hand.js +3 -1
- package/src/browser/study.js +47 -1
- package/src/run/visit.js +2 -2
package/mcp/hub.js
CHANGED
|
@@ -450,7 +450,12 @@ class Hub {
|
|
|
450
450
|
external,
|
|
451
451
|
study: research
|
|
452
452
|
? {
|
|
453
|
-
|
|
453
|
+
// 패널 옮기기는 막혔던 자리에서 비키게 끈다. 옮기고 나면 그 항목은 목록에서 내린다
|
|
454
|
+
press: async (k) => {
|
|
455
|
+
if (k !== "move") return study.press(page, k);
|
|
456
|
+
await study.movePanel(page, rec.panelInWay);
|
|
457
|
+
rec.panelInWay = null;
|
|
458
|
+
},
|
|
454
459
|
// 과제 질문·시간 초과 창이 페이지를 덮고 있나 — 그 사이 조작은 페이지에 닿지 않는다
|
|
455
460
|
blocking: async () => {
|
|
456
461
|
const w = study.isSurveyUrl(page.url()) ? null : await study.readStudy(page);
|
|
@@ -505,6 +510,9 @@ class Hub {
|
|
|
505
510
|
if (look.ended) return "This visit has ended.";
|
|
506
511
|
if (look.card) return renderCard(look.card);
|
|
507
512
|
// 과제 건너뛰기는 위젯 단추다. 화면 목록 끝에 붙인다 — 누르면 Visit 이 위젯을 누른다
|
|
513
|
+
// 우리 패널이 누르려던 것을 덮었다 — 사람은 패널을 끌어 비킨다(11/04 실험실 covered_by_panel 3건: 캘린더 Week·Month)
|
|
514
|
+
if (st && rec.panelInWay && !look.view.dialog)
|
|
515
|
+
look.view.items.push({ i: look.view.items.length, label: "Drag the study panel aside (it is covering the page)", where: "study panel, by its top bar", study: "move" });
|
|
508
516
|
if (st && st.skip && !look.view.dialog) look.view.items.push({ i: look.view.items.length, label: `${st.skip} (in the study panel — gives up on this task)`, where: "study panel, corner of the screen", study: "skip" });
|
|
509
517
|
// 아직 건너뛸 수 없는 과제 — 예전엔 "패널 기다리기" 를 줘서 막히면 몇 분씩 아무것도 안 했다(라이브 11번 × 약 5분).
|
|
510
518
|
// 사람은 막히면 계속 찾아본다. 그러다 실패 시간이 지나면 SDK 가 묻는 창을 띄우고, 다음 화면에서 그 창이 먼저 보인다
|
|
@@ -780,6 +788,7 @@ class Hub {
|
|
|
780
788
|
if (TRACE) trace(rec, { ev: "act", action: decision.action, study: r && r.study, actMs: Date.now() - a0, waitTurnMs: v.lastWaitMs || 0, sdk: await sdkSnap(rec.page) });
|
|
781
789
|
// 패널의 건너뛰기를 눌렀다 — 그 과제를 건너뛴 것으로 남긴다
|
|
782
790
|
if (r && r.study === "skip" && !r.skipped && rec.study) rec.study.skipped.push(rec.study.lastTask);
|
|
791
|
+
if (r && r.panelAt && rec.study) rec.panelInWay = r.panelAt;
|
|
783
792
|
if (r.interrupted && r.study) {
|
|
784
793
|
rec.cutOff = true;
|
|
785
794
|
return ["Before you could do that, the study asked you something.", "", await this.screen(rec)].join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usercase/mcp",
|
|
3
|
-
"version": "0.2.11-next.
|
|
3
|
+
"version": "0.2.11-next.3",
|
|
4
4
|
"description": "Virtual users that browse your website and tell you what they thought — an MCP server for Claude Code",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://usercase.io",
|
package/src/browser/hand.js
CHANGED
|
@@ -133,7 +133,8 @@ async function coveredBy(page, el) {
|
|
|
133
133
|
)
|
|
134
134
|
.catch(() => null);
|
|
135
135
|
if (!hit) return null;
|
|
136
|
-
|
|
136
|
+
// 패널은 끌어 옮길 수 있다(core Widget 알약 드래그). 막힌 자리를 같이 넘겨 hub 가 "패널 옮기기" 를 화면 목록에 붙인다
|
|
137
|
+
if (hit.own === "panel") return { say: "the study panel is on top of it — drag the panel aside (it is in the list), or scroll so it is not under the panel", panelAt: { x: cx, y: cy } };
|
|
137
138
|
if (hit.own) return { say: "the survey card is on top of it" };
|
|
138
139
|
if (hit.text) return { say: `something else on the screen is on top of it ("${hit.text}") — you cannot reach it until that is out of the way` };
|
|
139
140
|
return { invisible: true, x: cx, y: cy };
|
|
@@ -387,6 +388,7 @@ async function clickLikePerson(page, state, el) {
|
|
|
387
388
|
if (startY !== null) await page.evaluate((y) => window.scrollTo({ top: y, behavior: "instant" }), startY).catch(() => {});
|
|
388
389
|
const e = new Error(cover.say);
|
|
389
390
|
e.human = cover.say;
|
|
391
|
+
if (cover.panelAt) e.panelAt = cover.panelAt;
|
|
390
392
|
throw e;
|
|
391
393
|
}
|
|
392
394
|
// 화면 가장자리 밖으로 잘려 나간 것. 굴려도 옆으로는 안 온다 — 사람은 "잘려 있다" 를 본다.
|
package/src/browser/study.js
CHANGED
|
@@ -180,6 +180,52 @@ const PRESS = {
|
|
|
180
180
|
submit: "#eb-plugin #eb-survey-action-btn",
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* 리서치 패널을 끌어 비킨다 — 알약(#eb-pill)을 잡고 끌면 패널이 같이 움직인다(core components/widget/Widget.ts setupDrag).
|
|
185
|
+
* 막혔던 자리(away)의 반대쪽 가장자리로. 그래도 덮으면 위아래도 바꾼다. 고객이 끌기를 껐으면(draggable 0) 안 움직인다
|
|
186
|
+
*/
|
|
187
|
+
async function movePanel(page, away) {
|
|
188
|
+
const vp = page.viewportSize() || { width: 1280, height: 860 };
|
|
189
|
+
const pill = page.locator("#eb-plugin #eb-pill").first();
|
|
190
|
+
const covers = (pt) =>
|
|
191
|
+
pt
|
|
192
|
+
? page
|
|
193
|
+
.evaluate(([x, y]) => {
|
|
194
|
+
const h = document.elementFromPoint(x, y);
|
|
195
|
+
return !!(h && h.closest("#eb-plugin"));
|
|
196
|
+
}, [pt.x, pt.y])
|
|
197
|
+
.catch(() => false)
|
|
198
|
+
: Promise.resolve(false);
|
|
199
|
+
const drag = async (tx, ty) => {
|
|
200
|
+
const b = await pill.boundingBox();
|
|
201
|
+
if (!b) throw new Error("the study panel is not on the screen");
|
|
202
|
+
const sx = b.x + b.width / 2;
|
|
203
|
+
const sy = b.y + b.height / 2;
|
|
204
|
+
await page.mouse.move(sx, sy, { steps: 6 });
|
|
205
|
+
await sleep(120 + Math.random() * 200);
|
|
206
|
+
await page.mouse.down();
|
|
207
|
+
await page.mouse.move(sx + (tx - sx) * 0.5 + (Math.random() - 0.5) * 30, sy + (ty - sy) * 0.5 + (Math.random() - 0.5) * 30, { steps: 10 });
|
|
208
|
+
await page.mouse.move(tx, ty, { steps: 8 });
|
|
209
|
+
await sleep(80 + Math.random() * 150);
|
|
210
|
+
await page.mouse.up();
|
|
211
|
+
await sleep(300);
|
|
212
|
+
const a = await pill.boundingBox();
|
|
213
|
+
return a && Math.hypot(a.x - b.x, a.y - b.y) > 20;
|
|
214
|
+
};
|
|
215
|
+
const b = await pill.boundingBox();
|
|
216
|
+
if (!b) throw new Error("the study panel is not on the screen");
|
|
217
|
+
const half = b.width / 2;
|
|
218
|
+
const ax = away ? away.x : b.x + half;
|
|
219
|
+
const ay = away ? away.y : b.y;
|
|
220
|
+
// 옆으로 — 막힌 자리의 반대쪽 끝
|
|
221
|
+
const tx = ax < vp.width / 2 ? vp.width - half - 24 : half + 24;
|
|
222
|
+
if (!(await drag(tx, b.y + b.height / 2))) throw new Error("the panel did not move — it stays where it is");
|
|
223
|
+
if (!(await covers(away))) return;
|
|
224
|
+
// 패널이 길어 아직 덮는다 — 위아래도 반대쪽으로
|
|
225
|
+
const ty = ay < vp.height / 2 ? vp.height - b.height - 24 : 24 + b.height / 2;
|
|
226
|
+
await drag(tx, ty);
|
|
227
|
+
}
|
|
228
|
+
|
|
183
229
|
/** 위젯 단추를 누른다 */
|
|
184
230
|
async function press(page, key) {
|
|
185
231
|
const sel = PRESS[key];
|
|
@@ -211,4 +257,4 @@ async function answerQuestion(page, q, answer) {
|
|
|
211
257
|
return null;
|
|
212
258
|
}
|
|
213
259
|
|
|
214
|
-
module.exports = { readStudy, waitState, join, press, answerQuestion, isSurveyUrl };
|
|
260
|
+
module.exports = { readStudy, waitState, join, press, movePanel, answerQuestion, isSurveyUrl };
|
package/src/run/visit.js
CHANGED
|
@@ -727,7 +727,7 @@ class Visit {
|
|
|
727
727
|
// 리서치 위젯의 단추(과제 건너뛰기). 페이지 조작이 아니라 위젯을 누른다 — 화면 목록에 hub 가 붙여 둔 것이다
|
|
728
728
|
if (decision.action === "click" && item && item.study && this.study) {
|
|
729
729
|
record.study = item.study;
|
|
730
|
-
await this.study.press(item.study).catch(() => (record.skipped = "it did not respond"));
|
|
730
|
+
await this.study.press(item.study).catch((e) => (record.skipped = item.study === "move" && e && e.message ? e.message : "it did not respond"));
|
|
731
731
|
await sleep(1500);
|
|
732
732
|
this.steps.push(record);
|
|
733
733
|
this.history.push(`click "${item.label}" — ${decision.why}${record.skipped ? ` (${record.skipped})` : ""}`);
|
|
@@ -892,7 +892,7 @@ class Visit {
|
|
|
892
892
|
let ours = false;
|
|
893
893
|
const clicked = await hand
|
|
894
894
|
.clickLikePerson(page, this.handState, el)
|
|
895
|
-
.catch((e) => ((record.skipped = e.human || humanClickError(e)), (ours = /eb-insight|eb-plugin/.test(String(e.message))), { missed: false }));
|
|
895
|
+
.catch((e) => ((record.skipped = e.human || humanClickError(e)), (record.panelAt = e.panelAt || undefined), (ours = /eb-insight|eb-plugin/.test(String(e.message))), { missed: false }));
|
|
896
896
|
// 사이트의 무언가가 덮어 못 눌렀다. 우리 카드·위젯이 덮은 것은 사이트 탓이 아니다
|
|
897
897
|
if (record.skipped && /on top of it/.test(record.skipped) && !/survey card|study panel/.test(record.skipped) && !ours)
|
|
898
898
|
await this.shoot(record, "covered", await boxOf(page, el));
|