assistme 0.8.10 → 0.8.11
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/dist/{chunk-HY3FFXSQ.js → chunk-BGMIIZLF.js} +2 -2
- package/dist/{chunk-T3DBLWUW.js → chunk-RJBLIGFJ.js} +65 -4
- package/dist/{chunk-QGH5MFJA.js → chunk-RUP2IQG3.js} +19 -3
- package/dist/{config-2HH7PO34.js → config-V2EJC3EH.js} +1 -1
- package/dist/index.js +10 -12
- package/dist/job-runner-OT3HZQEF.js +7 -0
- package/dist/workers/entry.js +77 -14
- package/package.json +1 -1
- package/src/agent/event-hooks.ts +15 -2
- package/src/agent/job-runner.ts +14 -3
- package/src/agent/system-prompt.ts +22 -0
- package/src/browser/controller.ts +73 -3
- package/src/db/types.ts +1 -0
- package/src/mcp/agent-tools-server.ts +31 -8
- package/src/orchestrator.ts +7 -6
- package/src/utils/config.ts +2 -2
- package/src/utils/schemas.ts +2 -0
- package/tests/agent/event-hooks.test.ts +1 -0
- package/tests/agent/processor.test.ts +11 -0
- package/tests/utils/config.test.ts +1 -1
- package/dist/job-runner-IBVUDW6A.js +0 -7
|
@@ -30,7 +30,7 @@ var CONFIG_DEFAULTS = {
|
|
|
30
30
|
supabaseAnonKey: SUPABASE_ANON_KEY_DEFAULT,
|
|
31
31
|
sessionName: "Default",
|
|
32
32
|
model: "claude-sonnet-4-20250514",
|
|
33
|
-
taskTimeoutMinutes:
|
|
33
|
+
taskTimeoutMinutes: 0
|
|
34
34
|
};
|
|
35
35
|
var config = new Conf({
|
|
36
36
|
projectName: "assistme",
|
|
@@ -48,7 +48,7 @@ function getConfig() {
|
|
|
48
48
|
workspacePath: resolve(workspacePath),
|
|
49
49
|
sessionName: config.get("sessionName") || "Default",
|
|
50
50
|
model: config.get("model") || "claude-sonnet-4-20250514",
|
|
51
|
-
taskTimeoutMinutes: config.get("taskTimeoutMinutes")
|
|
51
|
+
taskTimeoutMinutes: config.get("taskTimeoutMinutes") ?? 0
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
function setConfig(key, value) {
|
|
@@ -17,13 +17,13 @@ import {
|
|
|
17
17
|
readAuthStore,
|
|
18
18
|
safeParse,
|
|
19
19
|
writeAuthStore
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-RUP2IQG3.js";
|
|
21
21
|
import {
|
|
22
22
|
AppError,
|
|
23
23
|
errorMessage,
|
|
24
24
|
getConfig,
|
|
25
25
|
getDataDir
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-BGMIIZLF.js";
|
|
27
27
|
|
|
28
28
|
// src/db/auth.ts
|
|
29
29
|
async function loginWithToken(mcpToken) {
|
|
@@ -53,7 +53,7 @@ async function logout() {
|
|
|
53
53
|
|
|
54
54
|
// src/db/session.ts
|
|
55
55
|
async function createSession(sessionName, workspacePath, version) {
|
|
56
|
-
const { getConfig: getConfig2 } = await import("./config-
|
|
56
|
+
const { getConfig: getConfig2 } = await import("./config-V2EJC3EH.js");
|
|
57
57
|
const data = await callMcpHandler("session.create", {
|
|
58
58
|
session_name: sessionName,
|
|
59
59
|
workspace_path: workspacePath,
|
|
@@ -386,6 +386,8 @@ var BrowserController = class {
|
|
|
386
386
|
// ── Navigation ──────────────────────────────────────────────────
|
|
387
387
|
async navigate(url) {
|
|
388
388
|
this.ensureConnected();
|
|
389
|
+
this.refCache.clear();
|
|
390
|
+
this.frameContexts.clear();
|
|
389
391
|
await this.send("Page.navigate", { url });
|
|
390
392
|
await this.waitForLoad();
|
|
391
393
|
const info = await this.getPageInfo();
|
|
@@ -394,6 +396,8 @@ URL: ${info.url}`;
|
|
|
394
396
|
}
|
|
395
397
|
async goBack() {
|
|
396
398
|
this.ensureConnected();
|
|
399
|
+
this.refCache.clear();
|
|
400
|
+
this.frameContexts.clear();
|
|
397
401
|
try {
|
|
398
402
|
const history = await this.send("Page.getNavigationHistory");
|
|
399
403
|
const idx = history.currentIndex ?? 0;
|
|
@@ -414,6 +418,8 @@ URL: ${info.url}`;
|
|
|
414
418
|
}
|
|
415
419
|
async reload() {
|
|
416
420
|
this.ensureConnected();
|
|
421
|
+
this.refCache.clear();
|
|
422
|
+
this.frameContexts.clear();
|
|
417
423
|
await this.send("Page.reload");
|
|
418
424
|
await this.waitForLoad();
|
|
419
425
|
return "Page reloaded.";
|
|
@@ -1332,6 +1338,47 @@ Refs:
|
|
|
1332
1338
|
return null;
|
|
1333
1339
|
}
|
|
1334
1340
|
}
|
|
1341
|
+
// ── Overlay Dismissal ────────────────────────────────────────────
|
|
1342
|
+
/**
|
|
1343
|
+
* Attempt to dismiss popups/overlays blocking interaction.
|
|
1344
|
+
* Tries Escape key first, then common close button patterns.
|
|
1345
|
+
* Called automatically when clickRef detects an element is covered.
|
|
1346
|
+
*/
|
|
1347
|
+
async tryDismissOverlay() {
|
|
1348
|
+
try {
|
|
1349
|
+
await this.pressKey("Escape");
|
|
1350
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1351
|
+
await this.send("Runtime.evaluate", {
|
|
1352
|
+
expression: `
|
|
1353
|
+
(function() {
|
|
1354
|
+
var selectors = [
|
|
1355
|
+
'button[aria-label="Close"]', 'button[aria-label="close"]',
|
|
1356
|
+
'button[aria-label="Dismiss"]', 'button[aria-label="dismiss"]',
|
|
1357
|
+
'[role="dialog"] button[aria-label*="close" i]',
|
|
1358
|
+
'[role="dialog"] button[aria-label*="dismiss" i]',
|
|
1359
|
+
'[data-dismiss]', '[data-close]',
|
|
1360
|
+
'.modal-close', '[class*="close-button"]',
|
|
1361
|
+
'[class*="CloseButton"]', '[class*="dismiss"]',
|
|
1362
|
+
];
|
|
1363
|
+
for (var i = 0; i < selectors.length; i++) {
|
|
1364
|
+
try {
|
|
1365
|
+
var btn = document.querySelector(selectors[i]);
|
|
1366
|
+
if (btn && btn.offsetParent !== null) {
|
|
1367
|
+
btn.click();
|
|
1368
|
+
return 'clicked';
|
|
1369
|
+
}
|
|
1370
|
+
} catch(e) {}
|
|
1371
|
+
}
|
|
1372
|
+
return 'none';
|
|
1373
|
+
})()
|
|
1374
|
+
`,
|
|
1375
|
+
returnByValue: true
|
|
1376
|
+
});
|
|
1377
|
+
return true;
|
|
1378
|
+
} catch {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1335
1382
|
// ── Ref-based Interactions (CDP Input Events) ─────────────────────
|
|
1336
1383
|
/**
|
|
1337
1384
|
* Click an element by ref using CDP Input.dispatchMouseEvent.
|
|
@@ -1342,6 +1389,8 @@ Refs:
|
|
|
1342
1389
|
* Includes auto-wait: retries up to 3 times (with 500ms intervals) if the
|
|
1343
1390
|
* element is not yet actionable (e.g., covered by a loading overlay, still
|
|
1344
1391
|
* animating into view). This matches Playwright's auto-waiting behavior.
|
|
1392
|
+
* When an element is covered by an overlay, auto-dismiss is attempted
|
|
1393
|
+
* (Escape key + common close buttons) before retrying.
|
|
1345
1394
|
*/
|
|
1346
1395
|
async clickRef(refId) {
|
|
1347
1396
|
this.ensureConnected();
|
|
@@ -1359,6 +1408,11 @@ Refs:
|
|
|
1359
1408
|
}
|
|
1360
1409
|
if (resolved.error) {
|
|
1361
1410
|
lastError = resolved.error;
|
|
1411
|
+
if (resolved.error.includes("Element is covered by") && attempt < maxRetries - 1) {
|
|
1412
|
+
await this.tryDismissOverlay();
|
|
1413
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1362
1416
|
if (attempt < maxRetries - 1) {
|
|
1363
1417
|
await new Promise((r) => setTimeout(r, 500));
|
|
1364
1418
|
continue;
|
|
@@ -1542,7 +1596,14 @@ Refs:
|
|
|
1542
1596
|
result,
|
|
1543
1597
|
success
|
|
1544
1598
|
});
|
|
1545
|
-
if (!success)
|
|
1599
|
+
if (!success) {
|
|
1600
|
+
results.push({
|
|
1601
|
+
action: "hint",
|
|
1602
|
+
result: "Refs may be stale after failure. Use browser_snapshot to get fresh refs before retrying.",
|
|
1603
|
+
success: true
|
|
1604
|
+
});
|
|
1605
|
+
break;
|
|
1606
|
+
}
|
|
1546
1607
|
if (spec.action !== "wait") {
|
|
1547
1608
|
await new Promise((r) => setTimeout(r, 200));
|
|
1548
1609
|
}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
errorMessage,
|
|
3
3
|
getConfig,
|
|
4
4
|
getDataDir
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-BGMIIZLF.js";
|
|
6
6
|
|
|
7
7
|
// src/db/auth-store.ts
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -240,6 +240,7 @@ var JobRowSchema = z.object({
|
|
|
240
240
|
job_id: z.string(),
|
|
241
241
|
job_name: z.string(),
|
|
242
242
|
job_description: z.string().optional().default(""),
|
|
243
|
+
job_prompt: z.string().optional().nullable(),
|
|
243
244
|
skill_id: z.string().optional().nullable(),
|
|
244
245
|
skill_name: z.string().optional().nullable(),
|
|
245
246
|
skill_description: z.string().optional().default(""),
|
|
@@ -250,6 +251,7 @@ var JobListRowSchema = z.object({
|
|
|
250
251
|
id: z.string(),
|
|
251
252
|
name: z.string(),
|
|
252
253
|
description: z.string().optional().default(""),
|
|
254
|
+
prompt: z.string().optional().nullable(),
|
|
253
255
|
skill_count: z.number().optional().default(0)
|
|
254
256
|
});
|
|
255
257
|
var JobRunRowSchema = z.object({
|
|
@@ -373,6 +375,7 @@ var JobRunner = class {
|
|
|
373
375
|
jobId: first.job_id,
|
|
374
376
|
jobName: first.job_name,
|
|
375
377
|
jobDescription: first.job_description,
|
|
378
|
+
jobPrompt: first.job_prompt ?? null,
|
|
376
379
|
skills: rows.filter((row) => row.skill_id).map((row) => ({
|
|
377
380
|
skillId: row.skill_id,
|
|
378
381
|
skillName: row.skill_name || "",
|
|
@@ -469,7 +472,12 @@ var JobRunner = class {
|
|
|
469
472
|
prompt += `**Name:** ${job.jobName}
|
|
470
473
|
`;
|
|
471
474
|
prompt += `**Description:** ${job.jobDescription}
|
|
472
|
-
|
|
475
|
+
`;
|
|
476
|
+
if (job.jobPrompt) {
|
|
477
|
+
prompt += `**Current Execution Prompt:** ${job.jobPrompt}
|
|
478
|
+
`;
|
|
479
|
+
}
|
|
480
|
+
prompt += `
|
|
473
481
|
`;
|
|
474
482
|
if (job.skills.length > 0) {
|
|
475
483
|
prompt += `**Current Skills:**
|
|
@@ -510,9 +518,10 @@ var JobRunner = class {
|
|
|
510
518
|
* chain them based on what it discovers at runtime.
|
|
511
519
|
*/
|
|
512
520
|
buildJobPrompt(job, runId) {
|
|
521
|
+
const effectiveDescription = job.jobPrompt || job.jobDescription;
|
|
513
522
|
let prompt = `## Job: ${job.jobName}
|
|
514
523
|
`;
|
|
515
|
-
prompt += `*${
|
|
524
|
+
prompt += `*${effectiveDescription}*
|
|
516
525
|
|
|
517
526
|
`;
|
|
518
527
|
prompt += `**Run ID:** ${runId}
|
|
@@ -536,12 +545,17 @@ var JobRunner = class {
|
|
|
536
545
|
}
|
|
537
546
|
prompt += `
|
|
538
547
|
### How to Work
|
|
548
|
+
`;
|
|
549
|
+
prompt += `- **Plan first**: Before jumping into actions, briefly plan your approach \u2014 which sites/tools to use, in what order, and how to verify results.
|
|
539
550
|
`;
|
|
540
551
|
prompt += `- **Be agentic**: Decide what to do based on what you discover. `;
|
|
541
552
|
prompt += `If checking Slack reveals a request that requires GitHub work, go do the GitHub work immediately \u2014 don't just note it for later.
|
|
542
553
|
`;
|
|
543
554
|
prompt += `- **Chain dynamically**: One skill's output should inform your next action. `;
|
|
544
555
|
prompt += `For example, if you find an assigned GitHub issue, use your coding skills to implement it.
|
|
556
|
+
`;
|
|
557
|
+
prompt += `- **Use separate tabs for different sites**: When working across multiple websites, open each in its own tab (browser_new_tab). `;
|
|
558
|
+
prompt += `This preserves page state and avoids re-navigation. Use browser_switch_tab to move between them.
|
|
545
559
|
`;
|
|
546
560
|
prompt += `- **Skip what's irrelevant**: If a capability doesn't apply right now, skip it.
|
|
547
561
|
`;
|
|
@@ -550,6 +564,8 @@ var JobRunner = class {
|
|
|
550
564
|
`;
|
|
551
565
|
prompt += `- **Respond and act**: If you find messages or issues that need replies, reply to them. `;
|
|
552
566
|
prompt += `If you find code tasks, implement them.
|
|
567
|
+
`;
|
|
568
|
+
prompt += `- **Handle auth walls**: If a site requires login, use browser_request_user_action immediately \u2014 do not waste attempts trying to bypass auth walls.
|
|
553
569
|
|
|
554
570
|
`;
|
|
555
571
|
prompt += `When finished, provide a summary of what you accomplished and any items that need the user's attention.
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
setSessionBusy,
|
|
28
28
|
toggleScheduledTask,
|
|
29
29
|
updateHeartbeat
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-RJBLIGFJ.js";
|
|
31
31
|
import {
|
|
32
32
|
HEARTBEAT_INTERVAL_MS,
|
|
33
33
|
HEARTBEAT_LOG_MAX_ENTRIES,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
setLogConversationId,
|
|
40
40
|
setLogHook,
|
|
41
41
|
setLogLevel
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-RUP2IQG3.js";
|
|
43
43
|
import {
|
|
44
44
|
clearConfig,
|
|
45
45
|
errorMessage,
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
getConfigPath,
|
|
48
48
|
getDataDir,
|
|
49
49
|
setConfig
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-BGMIIZLF.js";
|
|
51
51
|
|
|
52
52
|
// src/index.ts
|
|
53
53
|
import { Command } from "commander";
|
|
@@ -1500,12 +1500,10 @@ var Orchestrator = class {
|
|
|
1500
1500
|
await this.dispatchAndWait(`[JobRun: ${jobRun.job_name}] ${prompt}`);
|
|
1501
1501
|
await runner.completeRun(jobRun.id, "completed", "Job executed via web trigger");
|
|
1502
1502
|
} catch (err) {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
`Execution error: ${err instanceof Error ? err.message : err}`
|
|
1508
|
-
).catch((e) => log.error(`Failed to mark run as failed: ${e}`));
|
|
1503
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1504
|
+
log.error(`Job run failed: ${errMsg}`);
|
|
1505
|
+
const summary = errMsg.includes("cancelled") ? `Cancelled by user. Check workspace for any partial results.` : `Execution error: ${errMsg}`;
|
|
1506
|
+
await runner.completeRun(jobRun.id, "failed", summary).catch((e) => log.error(`Failed to mark run as failed: ${e}`));
|
|
1509
1507
|
}
|
|
1510
1508
|
}
|
|
1511
1509
|
// ── Busy State ──────────────────────────────────────────────────
|
|
@@ -2237,7 +2235,7 @@ function registerJobCommands(program2) {
|
|
|
2237
2235
|
jobCmd.command("list").description("List your defined jobs").action(async () => {
|
|
2238
2236
|
try {
|
|
2239
2237
|
await getCurrentUserId();
|
|
2240
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
2238
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-OT3HZQEF.js");
|
|
2241
2239
|
const runner = new JobRunner2();
|
|
2242
2240
|
const jobs = await runner.listJobs();
|
|
2243
2241
|
if (jobs.length === 0) {
|
|
@@ -2261,7 +2259,7 @@ function registerJobCommands(program2) {
|
|
|
2261
2259
|
jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
|
|
2262
2260
|
try {
|
|
2263
2261
|
await getCurrentUserId();
|
|
2264
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
2262
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-OT3HZQEF.js");
|
|
2265
2263
|
const runner = new JobRunner2();
|
|
2266
2264
|
const runs = await runner.getRunHistory(name, parseInt(opts.limit || "5"));
|
|
2267
2265
|
if (runs.length === 0) {
|
|
@@ -2300,7 +2298,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`));
|
|
|
2300
2298
|
process.exit(1);
|
|
2301
2299
|
}
|
|
2302
2300
|
await getCurrentUserId();
|
|
2303
|
-
const { JobRunner: JobRunner2 } = await import("./job-runner-
|
|
2301
|
+
const { JobRunner: JobRunner2 } = await import("./job-runner-OT3HZQEF.js");
|
|
2304
2302
|
const runner = new JobRunner2();
|
|
2305
2303
|
const job = await runner.loadJob(name);
|
|
2306
2304
|
if (!job) {
|
package/dist/workers/entry.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resetEventSequence,
|
|
21
21
|
setActionRequest,
|
|
22
22
|
upsertAgentSkill
|
|
23
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-RJBLIGFJ.js";
|
|
24
24
|
import {
|
|
25
25
|
EDSGER_PRODUCT_SLUG,
|
|
26
26
|
JobRunner,
|
|
@@ -47,14 +47,14 @@ import {
|
|
|
47
47
|
safeParse,
|
|
48
48
|
setCorrelationId,
|
|
49
49
|
setLogTransport
|
|
50
|
-
} from "../chunk-
|
|
50
|
+
} from "../chunk-RUP2IQG3.js";
|
|
51
51
|
import {
|
|
52
52
|
AppError,
|
|
53
53
|
assertWithinAssistMeRoot,
|
|
54
54
|
errorMessage,
|
|
55
55
|
getAssistMeRoot,
|
|
56
56
|
getConfig
|
|
57
|
-
} from "../chunk-
|
|
57
|
+
} from "../chunk-BGMIIZLF.js";
|
|
58
58
|
|
|
59
59
|
// src/workers/conversation.ts
|
|
60
60
|
import { config as loadEnv } from "dotenv";
|
|
@@ -1708,7 +1708,9 @@ function createAgentToolsServer(deps) {
|
|
|
1708
1708
|
{
|
|
1709
1709
|
name: z2.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
|
|
1710
1710
|
description: z2.string().describe("One-line description of what this skill does"),
|
|
1711
|
-
instructions: z2.string().describe(
|
|
1711
|
+
instructions: z2.string().describe(
|
|
1712
|
+
"Markdown step-by-step instructions. IMPORTANT: When a skill produces document output (reports, summaries, etc.), instruct it to generate .docx/.xlsx/.pptx files (not .md), so end users can open and edit them."
|
|
1713
|
+
),
|
|
1712
1714
|
emoji: z2.string().optional().describe("Single emoji representing this skill")
|
|
1713
1715
|
},
|
|
1714
1716
|
async (args) => {
|
|
@@ -1984,14 +1986,38 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
|
|
|
1984
1986
|
|
|
1985
1987
|
`;
|
|
1986
1988
|
}
|
|
1987
|
-
response += `**Your task:** Analyze this job description and
|
|
1989
|
+
response += `**Your task:** Analyze this job description and do TWO things:
|
|
1990
|
+
`;
|
|
1991
|
+
response += `- **A. Rewrite the description into a professional, structured prompt** that clearly defines the job's objectives, scope, and expected outcomes. This will be used as the actual execution prompt when running the job. Store it as the \`prompt\` parameter when calling \`skill_link_job\`.
|
|
1992
|
+
`;
|
|
1993
|
+
response += `- **B. Decompose the job into 4-10 automatable skills.**
|
|
1988
1994
|
|
|
1989
1995
|
`;
|
|
1990
|
-
response += `**
|
|
1996
|
+
response += `**Example \u2014 Task A (professional prompt rewrite):**
|
|
1997
|
+
`;
|
|
1998
|
+
response += `> User description: "\u6211\u662F\u7535\u5546\u8FD0\u8425\uFF0C\u6BCF\u5929\u770B\u7ADE\u54C1\u4EF7\u683C\u3001\u5199\u6587\u6848\u3001\u56DE\u590D\u8BC4\u8BBA"
|
|
1999
|
+
`;
|
|
2000
|
+
response += `> Professional prompt:
|
|
1991
2001
|
`;
|
|
1992
|
-
response +=
|
|
2002
|
+
response += `> \u4F5C\u4E3A\u7535\u5546\u8FD0\u8425\u4E13\u5458\uFF0C\u7CFB\u7EDF\u6027\u5730\u6267\u884C\u4EE5\u4E0B\u6838\u5FC3\u65E5\u5E38\u804C\u8D23\uFF1A
|
|
1993
2003
|
`;
|
|
1994
|
-
response +=
|
|
2004
|
+
response += `> 1. \u7ADE\u54C1\u4EF7\u683C\u76D1\u63A7 \u2014 \u91C7\u96C6\u4E3B\u8981\u7ADE\u54C1\u5728\u5404\u7535\u5546\u5E73\u53F0\u7684\u5B9E\u65F6\u4EF7\u683C\uFF0C\u8BC6\u522B\u4EF7\u683C\u53D8\u52A8\u8D8B\u52BF\uFF0C\u751F\u6210\u5BF9\u6BD4\u62A5\u544A\uFF08.docx\uFF09
|
|
2005
|
+
`;
|
|
2006
|
+
response += `> 2. \u5546\u54C1\u6587\u6848\u64B0\u5199 \u2014 \u6839\u636E\u4EA7\u54C1\u5356\u70B9\u3001\u76EE\u6807\u53D7\u4F17\u548C\u5E73\u53F0\u8C03\u6027\uFF0C\u64B0\u5199\u7B26\u5408 SEO \u89C4\u8303\u7684\u5546\u54C1\u6807\u9898\u4E0E\u63CF\u8FF0
|
|
2007
|
+
`;
|
|
2008
|
+
response += `> 3. \u5BA2\u6237\u8BC4\u8BBA\u7BA1\u7406 \u2014 \u68C0\u67E5\u65B0\u589E\u5DEE\u8BC4\u4E0E\u54A8\u8BE2\uFF0C\u6309\u4F18\u5148\u7EA7\u5206\u7C7B\uFF0C\u8D77\u8349\u4E13\u4E1A\u56DE\u590D
|
|
2009
|
+
`;
|
|
2010
|
+
response += `> \u5B8C\u6210\u540E\u6C47\u603B\u5F53\u65E5\u5DE5\u4F5C\u6458\u8981\uFF0C\u6807\u6CE8\u9700\u8981\u4EBA\u5DE5\u5173\u6CE8\u7684\u4E8B\u9879\u3002
|
|
2011
|
+
|
|
2012
|
+
`;
|
|
2013
|
+
response += `The prompt should be in the same language as the user's description. Keep it under 2000 characters.
|
|
2014
|
+
|
|
2015
|
+
`;
|
|
2016
|
+
response += `**Workflow (follow these steps in order):**
|
|
2017
|
+
`;
|
|
2018
|
+
response += `1. Draft the professional prompt (task A) AND a list of proposed skills with name, emoji, one-line description each (task B).
|
|
2019
|
+
`;
|
|
2020
|
+
response += `2. Call \`ask_user\` with both the rewritten prompt and the skill list as "question", and these options:
|
|
1995
2021
|
`;
|
|
1996
2022
|
response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]
|
|
1997
2023
|
`;
|
|
@@ -2014,7 +2040,7 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
|
|
|
2014
2040
|
response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
|
|
2015
2041
|
|
|
2016
2042
|
`;
|
|
2017
|
-
response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}"
|
|
2043
|
+
response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}", the list of created skill names, AND the professional prompt you drafted to link them and mark the job as analyzed.
|
|
2018
2044
|
|
|
2019
2045
|
`;
|
|
2020
2046
|
response += `**Guidelines for skill instructions:**
|
|
@@ -2028,6 +2054,8 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
|
|
|
2028
2054
|
response += `- Use placeholders like {query}, {date} for variable inputs
|
|
2029
2055
|
`;
|
|
2030
2056
|
response += `- Each skill should be a single, well-defined workflow (10-25 steps)
|
|
2057
|
+
`;
|
|
2058
|
+
response += `- When a skill produces document output (reports, summaries, data exports), instruct it to generate .docx/.xlsx/.pptx files \u2014 NOT .md \u2014 so end users can open and edit them
|
|
2031
2059
|
`;
|
|
2032
2060
|
return { content: [{ type: "text", text: response }] };
|
|
2033
2061
|
}
|
|
@@ -2038,11 +2066,14 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
|
|
|
2038
2066
|
{
|
|
2039
2067
|
job_name: z2.string().describe("Name of the job to link skills to"),
|
|
2040
2068
|
job_description: z2.string().describe("Job description (used if job doesn't exist yet)"),
|
|
2041
|
-
skill_names: z2.array(z2.string()).describe("Names of skills to link to this job")
|
|
2069
|
+
skill_names: z2.array(z2.string()).describe("Names of skills to link to this job"),
|
|
2070
|
+
prompt: z2.string().optional().describe(
|
|
2071
|
+
"Professional, structured rewrite of the job description (max 2000 chars). This becomes the actual execution prompt when running the job. Should clearly define objectives, scope, and expected outcomes."
|
|
2072
|
+
)
|
|
2042
2073
|
},
|
|
2043
2074
|
async (args) => {
|
|
2044
2075
|
try {
|
|
2045
|
-
await saveJobToDb(args.job_name, args.job_description, args.skill_names);
|
|
2076
|
+
await saveJobToDb(args.job_name, args.job_description, args.skill_names, args.prompt);
|
|
2046
2077
|
log.success(
|
|
2047
2078
|
`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`
|
|
2048
2079
|
);
|
|
@@ -2851,12 +2882,13 @@ ${message}`
|
|
|
2851
2882
|
]
|
|
2852
2883
|
});
|
|
2853
2884
|
}
|
|
2854
|
-
async function saveJobToDb(jobName, jobDescription, createdSkillNames) {
|
|
2885
|
+
async function saveJobToDb(jobName, jobDescription, createdSkillNames, prompt) {
|
|
2855
2886
|
try {
|
|
2856
2887
|
const data = await callMcpHandler("job.save_with_skills", {
|
|
2857
2888
|
job_name: jobName,
|
|
2858
2889
|
job_description: jobDescription,
|
|
2859
|
-
skill_names: createdSkillNames
|
|
2890
|
+
skill_names: createdSkillNames,
|
|
2891
|
+
prompt: prompt || null
|
|
2860
2892
|
});
|
|
2861
2893
|
log.debug(
|
|
2862
2894
|
`Job "${jobName}" saved via edge function (id: ${data}), ${createdSkillNames.length} skill(s) linked`
|
|
@@ -2921,12 +2953,21 @@ function createEventHooks(taskId, toolCallRecords, toolFailures = []) {
|
|
|
2921
2953
|
error: errorStr.slice(0, 500),
|
|
2922
2954
|
timestamp: Date.now()
|
|
2923
2955
|
});
|
|
2956
|
+
const sameToolFailures = toolFailures.filter((f) => f.toolName === displayName).length;
|
|
2957
|
+
const isBrowser = displayName.startsWith("browser_");
|
|
2958
|
+
const browserFailures = isBrowser ? toolFailures.filter((f) => f.toolName.startsWith("browser_")).length : 0;
|
|
2924
2959
|
await emitEvent(taskId, "tool_failure", {
|
|
2925
2960
|
name: displayName,
|
|
2926
2961
|
error: errorStr.slice(0, 500),
|
|
2927
|
-
failure_count:
|
|
2962
|
+
failure_count: sameToolFailures,
|
|
2963
|
+
browser_failure_count: browserFailures
|
|
2928
2964
|
});
|
|
2929
2965
|
log.warn(`Tool failure tracked: ${displayName} (total: ${toolFailures.length})`);
|
|
2966
|
+
if (isBrowser && browserFailures >= 3 && browserFailures % 3 === 0) {
|
|
2967
|
+
await emitEvent(taskId, "tool_failure_hint", {
|
|
2968
|
+
message: `${browserFailures} browser tool failures detected. Consider: (1) re-snapshot to get fresh refs, (2) use separate tabs for different sites, (3) navigate directly to target URLs to bypass popups, (4) use browser_request_user_action for login walls.`
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2930
2971
|
return {};
|
|
2931
2972
|
};
|
|
2932
2973
|
return {
|
|
@@ -3124,6 +3165,13 @@ Workflow for web tasks (e.g. "\u67E5\u4E00\u4E0B kindle \u6700\u65B0\u6B3E\u4EF7
|
|
|
3124
3165
|
6. Repeat 4-5 as needed (re-snapshot after navigation or major page changes)
|
|
3125
3166
|
7. Summarize findings
|
|
3126
3167
|
|
|
3168
|
+
Workflow for MULTI-SITE tasks (e.g. "\u5728 Seek \u627E\u5DE5\u4F5C\uFF0C\u7136\u540E\u53BB Immigration NZ \u9A8C\u8BC1\u96C7\u4E3B"):
|
|
3169
|
+
- ALWAYS use separate tabs for different websites. Do NOT navigate back and forth in one tab \u2014 you lose page state.
|
|
3170
|
+
- Open each site in its own tab: browser_new_tab \u2192 browser_navigate
|
|
3171
|
+
- Use browser_switch_tab to switch between sites
|
|
3172
|
+
- This preserves scroll position, form data, and search results on each site
|
|
3173
|
+
- Example: Tab 0 = Seek (job search), Tab 1 = Immigration NZ (employer check), Tab 2 = LinkedIn (research)
|
|
3174
|
+
|
|
3127
3175
|
Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
|
|
3128
3176
|
1. browser_connect + browser_navigate \u2192 go to the form page
|
|
3129
3177
|
2. browser_snapshot \u2192 see all form fields with ref numbers
|
|
@@ -3140,6 +3188,21 @@ Workflow for form filling (e.g. "\u6CE8\u518C\u4E00\u4E2A Gmail \u8D26\u53F7"):
|
|
|
3140
3188
|
- Browser approach too slow \u2192 consider building a script/CLI solution
|
|
3141
3189
|
- One data source fails \u2192 try an alternative source
|
|
3142
3190
|
- If stuck after 2 failed attempts at the same step, try a fundamentally different approach
|
|
3191
|
+
- **Popup/overlay blocks interaction** \u2192 The system auto-tries Escape + close buttons. If that fails, navigate directly to the target URL (e.g. append search params to skip the homepage popup). Do NOT repeatedly try to click through the same overlay.
|
|
3192
|
+
- **Login/auth wall** \u2192 Use browser_request_user_action to ask the user to log in. Use memory_store to remember which sites require login so you can warn the user upfront next time.
|
|
3193
|
+
- **Stale refs after navigation** \u2192 Refs are automatically cleared on navigation. Always re-snapshot after browser_navigate, goBack, or reload. If browser_act returns a "hint" about stale refs, re-snapshot immediately.
|
|
3194
|
+
|
|
3195
|
+
10. DOCUMENT OUTPUT FORMAT:
|
|
3196
|
+
- When creating documents, reports, or deliverables for the user, ALWAYS prefer Microsoft Office formats (.docx for text documents, .xlsx for spreadsheets, .pptx for presentations) over Markdown (.md).
|
|
3197
|
+
- Regular users need to open, read, and edit these files \u2014 Office formats are universally accessible.
|
|
3198
|
+
- Use the Bash tool to install and run document-generation libraries as needed:
|
|
3199
|
+
* Python: python-docx (Word), openpyxl (Excel), python-pptx (PowerPoint)
|
|
3200
|
+
* Node.js: docx (Word), exceljs (Excel), pptx (PowerPoint)
|
|
3201
|
+
- Only use Markdown when:
|
|
3202
|
+
* The output is internal (e.g. README.md in a program directory, skill instructions)
|
|
3203
|
+
* The user explicitly requests Markdown
|
|
3204
|
+
* The content is a quick inline response in the conversation (not a standalone file)
|
|
3205
|
+
- When writing .docx files, include proper formatting: headings, paragraphs, tables, bullet lists \u2014 not just plain text dumped into a Word file.
|
|
3143
3206
|
|
|
3144
3207
|
Guidelines:
|
|
3145
3208
|
- SELF-VERIFY before finishing: re-read modified files, take a final screenshot after browser actions, or re-check output to confirm correctness. Never assume success without confirming the end state.
|
package/package.json
CHANGED
package/src/agent/event-hooks.ts
CHANGED
|
@@ -34,7 +34,6 @@ export function stripMcpPrefix(toolName: string): string {
|
|
|
34
34
|
return match ? match[1] : toolName;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
37
|
/**
|
|
39
38
|
* Create PreToolUse and PostToolUse hooks that emit events to Supabase.
|
|
40
39
|
* These hooks let the web UI display tool activity in real-time.
|
|
@@ -113,14 +112,28 @@ export function createEventHooks(
|
|
|
113
112
|
timestamp: Date.now(),
|
|
114
113
|
});
|
|
115
114
|
|
|
115
|
+
const sameToolFailures = toolFailures.filter((f) => f.toolName === displayName).length;
|
|
116
|
+
const isBrowser = displayName.startsWith("browser_");
|
|
117
|
+
const browserFailures = isBrowser
|
|
118
|
+
? toolFailures.filter((f) => f.toolName.startsWith("browser_")).length
|
|
119
|
+
: 0;
|
|
120
|
+
|
|
116
121
|
await emitEvent(taskId, "tool_failure", {
|
|
117
122
|
name: displayName,
|
|
118
123
|
error: errorStr.slice(0, 500),
|
|
119
|
-
failure_count:
|
|
124
|
+
failure_count: sameToolFailures,
|
|
125
|
+
browser_failure_count: browserFailures,
|
|
120
126
|
});
|
|
121
127
|
|
|
122
128
|
log.warn(`Tool failure tracked: ${displayName} (total: ${toolFailures.length})`);
|
|
123
129
|
|
|
130
|
+
// Emit strategy hint when browser failures accumulate
|
|
131
|
+
if (isBrowser && browserFailures >= 3 && browserFailures % 3 === 0) {
|
|
132
|
+
await emitEvent(taskId, "tool_failure_hint", {
|
|
133
|
+
message: `${browserFailures} browser tool failures detected. Consider: (1) re-snapshot to get fresh refs, (2) use separate tabs for different sites, (3) navigate directly to target URLs to bypass popups, (4) use browser_request_user_action for login walls.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
124
137
|
return {};
|
|
125
138
|
};
|
|
126
139
|
|
package/src/agent/job-runner.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface JobInfo {
|
|
|
18
18
|
jobId: string;
|
|
19
19
|
jobName: string;
|
|
20
20
|
jobDescription: string;
|
|
21
|
+
jobPrompt: string | null;
|
|
21
22
|
skills: JobSkillInfo[];
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -57,6 +58,7 @@ export class JobRunner {
|
|
|
57
58
|
jobId: first.job_id,
|
|
58
59
|
jobName: first.job_name,
|
|
59
60
|
jobDescription: first.job_description,
|
|
61
|
+
jobPrompt: first.job_prompt ?? null,
|
|
60
62
|
skills: rows
|
|
61
63
|
.filter((row) => row!.skill_id)
|
|
62
64
|
.map((row) => ({
|
|
@@ -172,7 +174,11 @@ export class JobRunner {
|
|
|
172
174
|
let prompt = `Analyze this job definition and determine what skills are needed to execute it effectively.\n\n`;
|
|
173
175
|
prompt += `## Job Definition\n`;
|
|
174
176
|
prompt += `**Name:** ${job.jobName}\n`;
|
|
175
|
-
prompt += `**Description:** ${job.jobDescription}\n
|
|
177
|
+
prompt += `**Description:** ${job.jobDescription}\n`;
|
|
178
|
+
if (job.jobPrompt) {
|
|
179
|
+
prompt += `**Current Execution Prompt:** ${job.jobPrompt}\n`;
|
|
180
|
+
}
|
|
181
|
+
prompt += `\n`;
|
|
176
182
|
|
|
177
183
|
if (job.skills.length > 0) {
|
|
178
184
|
prompt += `**Current Skills:**\n`;
|
|
@@ -204,8 +210,9 @@ export class JobRunner {
|
|
|
204
210
|
* chain them based on what it discovers at runtime.
|
|
205
211
|
*/
|
|
206
212
|
buildJobPrompt(job: JobInfo, runId: string): string {
|
|
213
|
+
const effectiveDescription = job.jobPrompt || job.jobDescription;
|
|
207
214
|
let prompt = `## Job: ${job.jobName}\n`;
|
|
208
|
-
prompt += `*${
|
|
215
|
+
prompt += `*${effectiveDescription}*\n\n`;
|
|
209
216
|
prompt += `**Run ID:** ${runId}\n\n`;
|
|
210
217
|
|
|
211
218
|
prompt += `You are now acting as "${job.jobName}". `;
|
|
@@ -222,15 +229,19 @@ export class JobRunner {
|
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
prompt += `\n### How to Work\n`;
|
|
232
|
+
prompt += `- **Plan first**: Before jumping into actions, briefly plan your approach — which sites/tools to use, in what order, and how to verify results.\n`;
|
|
225
233
|
prompt += `- **Be agentic**: Decide what to do based on what you discover. `;
|
|
226
234
|
prompt += `If checking Slack reveals a request that requires GitHub work, go do the GitHub work immediately — don't just note it for later.\n`;
|
|
227
235
|
prompt += `- **Chain dynamically**: One skill's output should inform your next action. `;
|
|
228
236
|
prompt += `For example, if you find an assigned GitHub issue, use your coding skills to implement it.\n`;
|
|
237
|
+
prompt += `- **Use separate tabs for different sites**: When working across multiple websites, open each in its own tab (browser_new_tab). `;
|
|
238
|
+
prompt += `This preserves page state and avoids re-navigation. Use browser_switch_tab to move between them.\n`;
|
|
229
239
|
prompt += `- **Skip what's irrelevant**: If a capability doesn't apply right now, skip it.\n`;
|
|
230
240
|
prompt += `- **Use tools directly too**: You also have browser, file, and shell tools. `;
|
|
231
241
|
prompt += `Use them directly when a skill isn't needed — skills are guides, not mandatory steps.\n`;
|
|
232
242
|
prompt += `- **Respond and act**: If you find messages or issues that need replies, reply to them. `;
|
|
233
|
-
prompt += `If you find code tasks, implement them.\n
|
|
243
|
+
prompt += `If you find code tasks, implement them.\n`;
|
|
244
|
+
prompt += `- **Handle auth walls**: If a site requires login, use browser_request_user_action immediately — do not waste attempts trying to bypass auth walls.\n\n`;
|
|
234
245
|
|
|
235
246
|
prompt += `When finished, provide a summary of what you accomplished and any items that need the user's attention.\n`;
|
|
236
247
|
|
|
@@ -147,6 +147,13 @@ Workflow for web tasks (e.g. "查一下 kindle 最新款价格"):
|
|
|
147
147
|
6. Repeat 4-5 as needed (re-snapshot after navigation or major page changes)
|
|
148
148
|
7. Summarize findings
|
|
149
149
|
|
|
150
|
+
Workflow for MULTI-SITE tasks (e.g. "在 Seek 找工作,然后去 Immigration NZ 验证雇主"):
|
|
151
|
+
- ALWAYS use separate tabs for different websites. Do NOT navigate back and forth in one tab — you lose page state.
|
|
152
|
+
- Open each site in its own tab: browser_new_tab → browser_navigate
|
|
153
|
+
- Use browser_switch_tab to switch between sites
|
|
154
|
+
- This preserves scroll position, form data, and search results on each site
|
|
155
|
+
- Example: Tab 0 = Seek (job search), Tab 1 = Immigration NZ (employer check), Tab 2 = LinkedIn (research)
|
|
156
|
+
|
|
150
157
|
Workflow for form filling (e.g. "注册一个 Gmail 账号"):
|
|
151
158
|
1. browser_connect + browser_navigate → go to the form page
|
|
152
159
|
2. browser_snapshot → see all form fields with ref numbers
|
|
@@ -163,6 +170,21 @@ Workflow for form filling (e.g. "注册一个 Gmail 账号"):
|
|
|
163
170
|
- Browser approach too slow → consider building a script/CLI solution
|
|
164
171
|
- One data source fails → try an alternative source
|
|
165
172
|
- If stuck after 2 failed attempts at the same step, try a fundamentally different approach
|
|
173
|
+
- **Popup/overlay blocks interaction** → The system auto-tries Escape + close buttons. If that fails, navigate directly to the target URL (e.g. append search params to skip the homepage popup). Do NOT repeatedly try to click through the same overlay.
|
|
174
|
+
- **Login/auth wall** → Use browser_request_user_action to ask the user to log in. Use memory_store to remember which sites require login so you can warn the user upfront next time.
|
|
175
|
+
- **Stale refs after navigation** → Refs are automatically cleared on navigation. Always re-snapshot after browser_navigate, goBack, or reload. If browser_act returns a "hint" about stale refs, re-snapshot immediately.
|
|
176
|
+
|
|
177
|
+
10. DOCUMENT OUTPUT FORMAT:
|
|
178
|
+
- When creating documents, reports, or deliverables for the user, ALWAYS prefer Microsoft Office formats (.docx for text documents, .xlsx for spreadsheets, .pptx for presentations) over Markdown (.md).
|
|
179
|
+
- Regular users need to open, read, and edit these files — Office formats are universally accessible.
|
|
180
|
+
- Use the Bash tool to install and run document-generation libraries as needed:
|
|
181
|
+
* Python: python-docx (Word), openpyxl (Excel), python-pptx (PowerPoint)
|
|
182
|
+
* Node.js: docx (Word), exceljs (Excel), pptx (PowerPoint)
|
|
183
|
+
- Only use Markdown when:
|
|
184
|
+
* The output is internal (e.g. README.md in a program directory, skill instructions)
|
|
185
|
+
* The user explicitly requests Markdown
|
|
186
|
+
* The content is a quick inline response in the conversation (not a standalone file)
|
|
187
|
+
- When writing .docx files, include proper formatting: headings, paragraphs, tables, bullet lists — not just plain text dumped into a Word file.
|
|
166
188
|
|
|
167
189
|
Guidelines:
|
|
168
190
|
- SELF-VERIFY before finishing: re-read modified files, take a final screenshot after browser actions, or re-check output to confirm correctness. Never assume success without confirming the end state.
|
|
@@ -199,6 +199,9 @@ export class BrowserController {
|
|
|
199
199
|
|
|
200
200
|
async navigate(url: string): Promise<string> {
|
|
201
201
|
this.ensureConnected();
|
|
202
|
+
// Clear refs from previous page — they become stale after navigation
|
|
203
|
+
this.refCache.clear();
|
|
204
|
+
this.frameContexts.clear();
|
|
202
205
|
await this.send("Page.navigate", { url });
|
|
203
206
|
// Wait for load
|
|
204
207
|
await this.waitForLoad();
|
|
@@ -208,6 +211,8 @@ export class BrowserController {
|
|
|
208
211
|
|
|
209
212
|
async goBack(): Promise<string> {
|
|
210
213
|
this.ensureConnected();
|
|
214
|
+
this.refCache.clear();
|
|
215
|
+
this.frameContexts.clear();
|
|
211
216
|
try {
|
|
212
217
|
// Get navigation history and go to the previous entry
|
|
213
218
|
const history = (await this.send("Page.getNavigationHistory")) as {
|
|
@@ -235,6 +240,8 @@ export class BrowserController {
|
|
|
235
240
|
|
|
236
241
|
async reload(): Promise<string> {
|
|
237
242
|
this.ensureConnected();
|
|
243
|
+
this.refCache.clear();
|
|
244
|
+
this.frameContexts.clear();
|
|
238
245
|
await this.send("Page.reload");
|
|
239
246
|
await this.waitForLoad();
|
|
240
247
|
return "Page reloaded.";
|
|
@@ -1278,6 +1285,52 @@ export class BrowserController {
|
|
|
1278
1285
|
}
|
|
1279
1286
|
}
|
|
1280
1287
|
|
|
1288
|
+
// ── Overlay Dismissal ────────────────────────────────────────────
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Attempt to dismiss popups/overlays blocking interaction.
|
|
1292
|
+
* Tries Escape key first, then common close button patterns.
|
|
1293
|
+
* Called automatically when clickRef detects an element is covered.
|
|
1294
|
+
*/
|
|
1295
|
+
private async tryDismissOverlay(): Promise<boolean> {
|
|
1296
|
+
try {
|
|
1297
|
+
// Strategy 1: Press Escape to dismiss modals/popups
|
|
1298
|
+
await this.pressKey("Escape");
|
|
1299
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1300
|
+
|
|
1301
|
+
// Strategy 2: Click common close/dismiss buttons
|
|
1302
|
+
await this.send("Runtime.evaluate", {
|
|
1303
|
+
expression: `
|
|
1304
|
+
(function() {
|
|
1305
|
+
var selectors = [
|
|
1306
|
+
'button[aria-label="Close"]', 'button[aria-label="close"]',
|
|
1307
|
+
'button[aria-label="Dismiss"]', 'button[aria-label="dismiss"]',
|
|
1308
|
+
'[role="dialog"] button[aria-label*="close" i]',
|
|
1309
|
+
'[role="dialog"] button[aria-label*="dismiss" i]',
|
|
1310
|
+
'[data-dismiss]', '[data-close]',
|
|
1311
|
+
'.modal-close', '[class*="close-button"]',
|
|
1312
|
+
'[class*="CloseButton"]', '[class*="dismiss"]',
|
|
1313
|
+
];
|
|
1314
|
+
for (var i = 0; i < selectors.length; i++) {
|
|
1315
|
+
try {
|
|
1316
|
+
var btn = document.querySelector(selectors[i]);
|
|
1317
|
+
if (btn && btn.offsetParent !== null) {
|
|
1318
|
+
btn.click();
|
|
1319
|
+
return 'clicked';
|
|
1320
|
+
}
|
|
1321
|
+
} catch(e) {}
|
|
1322
|
+
}
|
|
1323
|
+
return 'none';
|
|
1324
|
+
})()
|
|
1325
|
+
`,
|
|
1326
|
+
returnByValue: true,
|
|
1327
|
+
});
|
|
1328
|
+
return true;
|
|
1329
|
+
} catch {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1281
1334
|
// ── Ref-based Interactions (CDP Input Events) ─────────────────────
|
|
1282
1335
|
|
|
1283
1336
|
/**
|
|
@@ -1289,6 +1342,8 @@ export class BrowserController {
|
|
|
1289
1342
|
* Includes auto-wait: retries up to 3 times (with 500ms intervals) if the
|
|
1290
1343
|
* element is not yet actionable (e.g., covered by a loading overlay, still
|
|
1291
1344
|
* animating into view). This matches Playwright's auto-waiting behavior.
|
|
1345
|
+
* When an element is covered by an overlay, auto-dismiss is attempted
|
|
1346
|
+
* (Escape key + common close buttons) before retrying.
|
|
1292
1347
|
*/
|
|
1293
1348
|
async clickRef(refId: number): Promise<RefActionResult> {
|
|
1294
1349
|
this.ensureConnected();
|
|
@@ -1311,7 +1366,13 @@ export class BrowserController {
|
|
|
1311
1366
|
|
|
1312
1367
|
if (resolved.error) {
|
|
1313
1368
|
lastError = resolved.error;
|
|
1314
|
-
// If element is covered
|
|
1369
|
+
// If element is covered by an overlay, try auto-dismissing it before retry
|
|
1370
|
+
if (resolved.error.includes("Element is covered by") && attempt < maxRetries - 1) {
|
|
1371
|
+
await this.tryDismissOverlay();
|
|
1372
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
// If element is hidden or other issue, wait and retry (it might be animating)
|
|
1315
1376
|
if (attempt < maxRetries - 1) {
|
|
1316
1377
|
await new Promise((r) => setTimeout(r, 500));
|
|
1317
1378
|
continue;
|
|
@@ -1528,8 +1589,17 @@ export class BrowserController {
|
|
|
1528
1589
|
success,
|
|
1529
1590
|
});
|
|
1530
1591
|
|
|
1531
|
-
// If an action failed, stop the batch (remaining refs may be stale)
|
|
1532
|
-
|
|
1592
|
+
// If an action failed, stop the batch (remaining refs may be stale).
|
|
1593
|
+
// Append re-snapshot hint to help the agent recover.
|
|
1594
|
+
if (!success) {
|
|
1595
|
+
results.push({
|
|
1596
|
+
action: "hint",
|
|
1597
|
+
result:
|
|
1598
|
+
"Refs may be stale after failure. Use browser_snapshot to get fresh refs before retrying.",
|
|
1599
|
+
success: true,
|
|
1600
|
+
});
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1533
1603
|
|
|
1534
1604
|
// Brief pause between actions for DOM to settle
|
|
1535
1605
|
if (spec.action !== "wait") {
|
package/src/db/types.ts
CHANGED
|
@@ -92,7 +92,11 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
92
92
|
{
|
|
93
93
|
name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
|
|
94
94
|
description: z.string().describe("One-line description of what this skill does"),
|
|
95
|
-
instructions: z.string().describe(
|
|
95
|
+
instructions: z.string().describe(
|
|
96
|
+
"Markdown step-by-step instructions. " +
|
|
97
|
+
"IMPORTANT: When a skill produces document output (reports, summaries, etc.), " +
|
|
98
|
+
"instruct it to generate .docx/.xlsx/.pptx files (not .md), so end users can open and edit them."
|
|
99
|
+
),
|
|
96
100
|
emoji: z.string().optional().describe("Single emoji representing this skill"),
|
|
97
101
|
},
|
|
98
102
|
async (args) => {
|
|
@@ -400,10 +404,21 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
400
404
|
response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}\n\n`;
|
|
401
405
|
}
|
|
402
406
|
|
|
403
|
-
response += `**Your task:** Analyze this job description and
|
|
404
|
-
response +=
|
|
405
|
-
response +=
|
|
406
|
-
response +=
|
|
407
|
+
response += `**Your task:** Analyze this job description and do TWO things:\n`;
|
|
408
|
+
response += `- **A. Rewrite the description into a professional, structured prompt** that clearly defines the job's objectives, scope, and expected outcomes. This will be used as the actual execution prompt when running the job. Store it as the \`prompt\` parameter when calling \`skill_link_job\`.\n`;
|
|
409
|
+
response += `- **B. Decompose the job into 4-10 automatable skills.**\n\n`;
|
|
410
|
+
response += `**Example — Task A (professional prompt rewrite):**\n`;
|
|
411
|
+
response += `> User description: "我是电商运营,每天看竞品价格、写文案、回复评论"\n`;
|
|
412
|
+
response += `> Professional prompt:\n`;
|
|
413
|
+
response += `> 作为电商运营专员,系统性地执行以下核心日常职责:\n`;
|
|
414
|
+
response += `> 1. 竞品价格监控 — 采集主要竞品在各电商平台的实时价格,识别价格变动趋势,生成对比报告(.docx)\n`;
|
|
415
|
+
response += `> 2. 商品文案撰写 — 根据产品卖点、目标受众和平台调性,撰写符合 SEO 规范的商品标题与描述\n`;
|
|
416
|
+
response += `> 3. 客户评论管理 — 检查新增差评与咨询,按优先级分类,起草专业回复\n`;
|
|
417
|
+
response += `> 完成后汇总当日工作摘要,标注需要人工关注的事项。\n\n`;
|
|
418
|
+
response += `The prompt should be in the same language as the user's description. Keep it under 2000 characters.\n\n`;
|
|
419
|
+
response += `**Workflow (follow these steps in order):**\n`;
|
|
420
|
+
response += `1. Draft the professional prompt (task A) AND a list of proposed skills with name, emoji, one-line description each (task B).\n`;
|
|
421
|
+
response += `2. Call \`ask_user\` with both the rewritten prompt and the skill list as "question", and these options:\n`;
|
|
407
422
|
response += ` - options: [{label: "Approve All", action_key: "approve_all", description: "Create all proposed skills"}, {label: "Cancel", action_key: "cancel", description: "Do not create any skills"}]\n`;
|
|
408
423
|
response += `3. WAIT for the response. If action_key is "approve_all", create all skills using \`skill_create\`. If "cancel", stop.\n`;
|
|
409
424
|
response += `4. Do NOT ask for confirmation in text. Do NOT create skills without calling ask_user first.\n\n`;
|
|
@@ -413,13 +428,14 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
413
428
|
response += `- instructions: detailed step-by-step markdown instructions the agent can follow\n`;
|
|
414
429
|
response += `- emoji: a single emoji representing the skill\n\n`;
|
|
415
430
|
response += `skill_create automatically adds the skill to the user's collection — no need to call skill_add.\n\n`;
|
|
416
|
-
response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}"
|
|
431
|
+
response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}", the list of created skill names, AND the professional prompt you drafted to link them and mark the job as analyzed.\n\n`;
|
|
417
432
|
response += `**Guidelines for skill instructions:**\n`;
|
|
418
433
|
response += `- Write clear, actionable markdown steps\n`;
|
|
419
434
|
response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks\n`;
|
|
420
435
|
response += `- Include error handling steps\n`;
|
|
421
436
|
response += `- Use placeholders like {query}, {date} for variable inputs\n`;
|
|
422
437
|
response += `- Each skill should be a single, well-defined workflow (10-25 steps)\n`;
|
|
438
|
+
response += `- When a skill produces document output (reports, summaries, data exports), instruct it to generate .docx/.xlsx/.pptx files — NOT .md — so end users can open and edit them\n`;
|
|
423
439
|
|
|
424
440
|
return { content: [{ type: "text", text: response }] };
|
|
425
441
|
}
|
|
@@ -431,10 +447,15 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
431
447
|
job_name: z.string().describe("Name of the job to link skills to"),
|
|
432
448
|
job_description: z.string().describe("Job description (used if job doesn't exist yet)"),
|
|
433
449
|
skill_names: z.array(z.string()).describe("Names of skills to link to this job"),
|
|
450
|
+
prompt: z.string().optional().describe(
|
|
451
|
+
"Professional, structured rewrite of the job description (max 2000 chars). " +
|
|
452
|
+
"This becomes the actual execution prompt when running the job. " +
|
|
453
|
+
"Should clearly define objectives, scope, and expected outcomes."
|
|
454
|
+
),
|
|
434
455
|
},
|
|
435
456
|
async (args) => {
|
|
436
457
|
try {
|
|
437
|
-
await saveJobToDb(args.job_name, args.job_description, args.skill_names);
|
|
458
|
+
await saveJobToDb(args.job_name, args.job_description, args.skill_names, args.prompt);
|
|
438
459
|
log.success(
|
|
439
460
|
`Job "${args.job_name}": linked ${args.skill_names.length} skills and marked as analyzed`
|
|
440
461
|
);
|
|
@@ -1393,13 +1414,15 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
|
|
|
1393
1414
|
async function saveJobToDb(
|
|
1394
1415
|
jobName: string,
|
|
1395
1416
|
jobDescription: string,
|
|
1396
|
-
createdSkillNames: string[]
|
|
1417
|
+
createdSkillNames: string[],
|
|
1418
|
+
prompt?: string
|
|
1397
1419
|
): Promise<void> {
|
|
1398
1420
|
try {
|
|
1399
1421
|
const data = await callMcpHandler("job.save_with_skills", {
|
|
1400
1422
|
job_name: jobName,
|
|
1401
1423
|
job_description: jobDescription,
|
|
1402
1424
|
skill_names: createdSkillNames,
|
|
1425
|
+
prompt: prompt || null,
|
|
1403
1426
|
});
|
|
1404
1427
|
|
|
1405
1428
|
log.debug(
|
package/src/orchestrator.ts
CHANGED
|
@@ -226,13 +226,14 @@ export class Orchestrator {
|
|
|
226
226
|
await this.dispatchAndWait(`[JobRun: ${jobRun.job_name}] ${prompt}`);
|
|
227
227
|
await runner.completeRun(jobRun.id, "completed", "Job executed via web trigger");
|
|
228
228
|
} catch (err) {
|
|
229
|
-
|
|
229
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
230
|
+
log.error(`Job run failed: ${errMsg}`);
|
|
231
|
+
// Preserve partial progress info in the summary
|
|
232
|
+
const summary = errMsg.includes("cancelled")
|
|
233
|
+
? `Cancelled by user. Check workspace for any partial results.`
|
|
234
|
+
: `Execution error: ${errMsg}`;
|
|
230
235
|
await runner
|
|
231
|
-
.completeRun(
|
|
232
|
-
jobRun.id,
|
|
233
|
-
"failed",
|
|
234
|
-
`Execution error: ${err instanceof Error ? err.message : err}`
|
|
235
|
-
)
|
|
236
|
+
.completeRun(jobRun.id, "failed", summary)
|
|
236
237
|
.catch((e) => log.error(`Failed to mark run as failed: ${e}`));
|
|
237
238
|
}
|
|
238
239
|
}
|
package/src/utils/config.ts
CHANGED
|
@@ -29,7 +29,7 @@ const CONFIG_DEFAULTS: Partial<AssistMeConfig> = {
|
|
|
29
29
|
supabaseAnonKey: SUPABASE_ANON_KEY_DEFAULT,
|
|
30
30
|
sessionName: "Default",
|
|
31
31
|
model: "claude-sonnet-4-20250514",
|
|
32
|
-
taskTimeoutMinutes:
|
|
32
|
+
taskTimeoutMinutes: 0,
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const config = new Conf<Partial<AssistMeConfig>>({
|
|
@@ -51,7 +51,7 @@ export function getConfig(): AssistMeConfig {
|
|
|
51
51
|
workspacePath: resolve(workspacePath),
|
|
52
52
|
sessionName: config.get("sessionName") || "Default",
|
|
53
53
|
model: config.get("model") || "claude-sonnet-4-20250514",
|
|
54
|
-
taskTimeoutMinutes: config.get("taskTimeoutMinutes")
|
|
54
|
+
taskTimeoutMinutes: config.get("taskTimeoutMinutes") ?? 0,
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
package/src/utils/schemas.ts
CHANGED
|
@@ -53,6 +53,7 @@ export const JobRowSchema = z.object({
|
|
|
53
53
|
job_id: z.string(),
|
|
54
54
|
job_name: z.string(),
|
|
55
55
|
job_description: z.string().optional().default(""),
|
|
56
|
+
job_prompt: z.string().optional().nullable(),
|
|
56
57
|
skill_id: z.string().optional().nullable(),
|
|
57
58
|
skill_name: z.string().optional().nullable(),
|
|
58
59
|
skill_description: z.string().optional().default(""),
|
|
@@ -64,6 +65,7 @@ export const JobListRowSchema = z.object({
|
|
|
64
65
|
id: z.string(),
|
|
65
66
|
name: z.string(),
|
|
66
67
|
description: z.string().optional().default(""),
|
|
68
|
+
prompt: z.string().optional().nullable(),
|
|
67
69
|
skill_count: z.number().optional().default(0),
|
|
68
70
|
});
|
|
69
71
|
|
|
@@ -39,6 +39,7 @@ vi.mock("../../src/utils/config.js", () => ({
|
|
|
39
39
|
workspacePath: "/tmp/test-workspace",
|
|
40
40
|
maxTurns: 10,
|
|
41
41
|
anthropicApiKey: "sk-ant-test",
|
|
42
|
+
taskTimeoutMinutes: 0,
|
|
42
43
|
}),
|
|
43
44
|
getAssistMeRoot: () => "/tmp/test-workspace/assistme",
|
|
44
45
|
}));
|
|
@@ -78,6 +79,7 @@ vi.mock("../../src/agent/memory.js", () => ({
|
|
|
78
79
|
MemoryManager: class MockMemoryManager {
|
|
79
80
|
buildMemoryPrompt = vi.fn().mockResolvedValue("");
|
|
80
81
|
remember = mockRemember;
|
|
82
|
+
compressIfNeeded = vi.fn().mockResolvedValue(undefined);
|
|
81
83
|
},
|
|
82
84
|
}));
|
|
83
85
|
|
|
@@ -85,6 +87,7 @@ vi.mock("../../src/agent/skills.js", () => ({
|
|
|
85
87
|
SkillManager: class MockSkillManager {
|
|
86
88
|
setUserId = vi.fn();
|
|
87
89
|
loadFromDb = vi.fn().mockResolvedValue(undefined);
|
|
90
|
+
ensureLoaded = vi.fn().mockResolvedValue(undefined);
|
|
88
91
|
buildSkillPrompt = vi.fn().mockReturnValue("");
|
|
89
92
|
buildSkillDescriptions = vi.fn().mockResolvedValue("");
|
|
90
93
|
findRelevant = vi.fn().mockReturnValue([]);
|
|
@@ -136,6 +139,14 @@ vi.mock("../../src/agent/self-analyzer.js", () => ({
|
|
|
136
139
|
analyzeSelfPostTask: vi.fn().mockResolvedValue(undefined),
|
|
137
140
|
}));
|
|
138
141
|
|
|
142
|
+
vi.mock("../../src/agent/prompt-builder.js", () => ({
|
|
143
|
+
buildSystemPrompt: vi.fn().mockResolvedValue("mock system prompt"),
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
vi.mock("../../src/agent/skill-evaluator.js", () => ({
|
|
147
|
+
evaluateAndMaybeCreateSkill: vi.fn().mockResolvedValue(undefined),
|
|
148
|
+
}));
|
|
149
|
+
|
|
139
150
|
const { TaskProcessor } = await import("../../src/agent/processor.js");
|
|
140
151
|
|
|
141
152
|
describe("TaskProcessor", () => {
|
|
@@ -50,7 +50,7 @@ describe("config module", () => {
|
|
|
50
50
|
const config = getConfig();
|
|
51
51
|
expect(config.sessionName).toBe("Default");
|
|
52
52
|
expect(config.model).toBe("claude-sonnet-4-20250514");
|
|
53
|
-
expect(config.taskTimeoutMinutes).toBe(
|
|
53
|
+
expect(config.taskTimeoutMinutes).toBe(0);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it("resolves workspacePath to absolute path", () => {
|