@basemachina/agentic-browser-cli 0.3.0
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/ai-model-NZ7QJNJW.js +32 -0
- package/dist/ai-model-NZ7QJNJW.js.map +1 -0
- package/dist/chunk-KDEQ2AAE.js +93 -0
- package/dist/chunk-KDEQ2AAE.js.map +1 -0
- package/dist/chunk-U6GPCKY3.js +3186 -0
- package/dist/chunk-U6GPCKY3.js.map +1 -0
- package/dist/chunk-VNQYQSMI.js +922 -0
- package/dist/chunk-VNQYQSMI.js.map +1 -0
- package/dist/chunk-XAEHXRUC.js +607 -0
- package/dist/chunk-XAEHXRUC.js.map +1 -0
- package/dist/cli.js +55 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose-EHDWVF7N.js +543 -0
- package/dist/compose-EHDWVF7N.js.map +1 -0
- package/dist/fix-instruction-JLCLTXAN.js +406 -0
- package/dist/fix-instruction-JLCLTXAN.js.map +1 -0
- package/dist/instruction-executor-M5Q6HYSM.js +5202 -0
- package/dist/instruction-executor-M5Q6HYSM.js.map +1 -0
- package/dist/instruction-generator-5RPRYTVR.js +2470 -0
- package/dist/instruction-generator-5RPRYTVR.js.map +1 -0
- package/package.json +29 -0
|
@@ -0,0 +1,2470 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AgentBrowser,
|
|
4
|
+
DownloadManager,
|
|
5
|
+
InMemoryDataStore,
|
|
6
|
+
classifyFailure,
|
|
7
|
+
createDebugLogger,
|
|
8
|
+
createReviewUserPrompt,
|
|
9
|
+
filterSnapshot,
|
|
10
|
+
findElementInSnapshot,
|
|
11
|
+
formatAiMetricsSection,
|
|
12
|
+
formatDebugLogSections,
|
|
13
|
+
formatDuration,
|
|
14
|
+
formatFailureCategoryDistribution,
|
|
15
|
+
formatPerformanceBottlenecks,
|
|
16
|
+
formatRuntimeEnvironment,
|
|
17
|
+
getAgentInstructions,
|
|
18
|
+
getInitialUserMessage,
|
|
19
|
+
getReviewSystemPrompt,
|
|
20
|
+
loadSecrets,
|
|
21
|
+
noopLogger,
|
|
22
|
+
parseAllElements,
|
|
23
|
+
parseAndAppendToMemory,
|
|
24
|
+
readDebugLog,
|
|
25
|
+
renderOptionsMarkdown,
|
|
26
|
+
serializeGeneratorOptions,
|
|
27
|
+
sleep
|
|
28
|
+
} from "./chunk-U6GPCKY3.js";
|
|
29
|
+
import {
|
|
30
|
+
getRawArgs,
|
|
31
|
+
parseModelConfig,
|
|
32
|
+
readContextFile
|
|
33
|
+
} from "./chunk-KDEQ2AAE.js";
|
|
34
|
+
import {
|
|
35
|
+
getModel,
|
|
36
|
+
globalMetrics,
|
|
37
|
+
initModel,
|
|
38
|
+
trackedGenerateObject,
|
|
39
|
+
trackedGenerateText
|
|
40
|
+
} from "./chunk-XAEHXRUC.js";
|
|
41
|
+
import {
|
|
42
|
+
ParsedInstructionSchema,
|
|
43
|
+
cancel,
|
|
44
|
+
initLocale,
|
|
45
|
+
intro,
|
|
46
|
+
log,
|
|
47
|
+
note,
|
|
48
|
+
outro,
|
|
49
|
+
promptMultiselect,
|
|
50
|
+
promptSelect,
|
|
51
|
+
promptText,
|
|
52
|
+
spinner,
|
|
53
|
+
t,
|
|
54
|
+
tf
|
|
55
|
+
} from "./chunk-VNQYQSMI.js";
|
|
56
|
+
|
|
57
|
+
// src/instruction-generator/index.ts
|
|
58
|
+
import { writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
59
|
+
import { dirname } from "path";
|
|
60
|
+
|
|
61
|
+
// src/instruction-generator/config.ts
|
|
62
|
+
import { parseArgs as nodeParseArgs } from "util";
|
|
63
|
+
async function parseArgs() {
|
|
64
|
+
const args = getRawArgs();
|
|
65
|
+
const { values } = nodeParseArgs({
|
|
66
|
+
args,
|
|
67
|
+
options: {
|
|
68
|
+
url: { type: "string" },
|
|
69
|
+
goal: { type: "string" },
|
|
70
|
+
output: { type: "string" },
|
|
71
|
+
screenshots: { type: "string" },
|
|
72
|
+
context: { type: "string" },
|
|
73
|
+
secrets: { type: "string" },
|
|
74
|
+
"max-iterations": { type: "string" },
|
|
75
|
+
"step-delay": { type: "string" },
|
|
76
|
+
"stall-check-interval": { type: "string" },
|
|
77
|
+
"history-window": { type: "string" },
|
|
78
|
+
"max-failures": { type: "string" },
|
|
79
|
+
"no-snapshot-filter": { type: "boolean" },
|
|
80
|
+
"debug-log": { type: "string" },
|
|
81
|
+
debug: { type: "boolean" },
|
|
82
|
+
headless: { type: "string" },
|
|
83
|
+
model: { type: "string" },
|
|
84
|
+
"model-provider": { type: "string" },
|
|
85
|
+
"model-base-url": { type: "string" },
|
|
86
|
+
"model-selector": { type: "string" },
|
|
87
|
+
"model-extraction": { type: "string" },
|
|
88
|
+
"model-exploration": { type: "string" },
|
|
89
|
+
"model-review": { type: "string" },
|
|
90
|
+
"model-fallback": { type: "string" },
|
|
91
|
+
"model-exploration-light": { type: "string" },
|
|
92
|
+
"enable-multi-model": { type: "boolean" },
|
|
93
|
+
video: { type: "string" },
|
|
94
|
+
locale: { type: "string" },
|
|
95
|
+
stealth: { type: "boolean" },
|
|
96
|
+
proxy: { type: "string" }
|
|
97
|
+
},
|
|
98
|
+
strict: true
|
|
99
|
+
});
|
|
100
|
+
initLocale(values.locale);
|
|
101
|
+
if (!values.url) {
|
|
102
|
+
cancel(t("cli.urlRequired"));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (!values.goal) {
|
|
106
|
+
cancel(t("cli.goalRequired"));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
if (!values.output) {
|
|
110
|
+
cancel(t("cli.outputRequired"));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const contextMarkdown = await readContextFile(values.context);
|
|
114
|
+
const secrets = await loadSecrets(values.secrets);
|
|
115
|
+
const aiModelConfig = parseModelConfig(values);
|
|
116
|
+
return {
|
|
117
|
+
url: values.url,
|
|
118
|
+
goal: values.goal,
|
|
119
|
+
output: values.output,
|
|
120
|
+
screenshotDir: values.screenshots,
|
|
121
|
+
contextMarkdown,
|
|
122
|
+
secrets,
|
|
123
|
+
maxIterations: values["max-iterations"] ? Number.parseInt(values["max-iterations"], 10) : 20,
|
|
124
|
+
stepDelay: values["step-delay"] ? Number.parseInt(values["step-delay"], 10) : 500,
|
|
125
|
+
headless: values.headless !== "false",
|
|
126
|
+
stallCheckInterval: values["stall-check-interval"] ? Number.parseInt(values["stall-check-interval"], 10) : 3,
|
|
127
|
+
historyWindow: values["history-window"] ? Number.parseInt(values["history-window"], 10) : 10,
|
|
128
|
+
maxConsecutiveFailures: values["max-failures"] ? Number.parseInt(values["max-failures"], 10) : 3,
|
|
129
|
+
snapshotFilter: !values["no-snapshot-filter"],
|
|
130
|
+
debugLogPath: values["debug-log"],
|
|
131
|
+
debugConsole: values.debug ?? false,
|
|
132
|
+
videoDir: values.video,
|
|
133
|
+
aiModelConfig,
|
|
134
|
+
enableMultiModel: values["enable-multi-model"] ?? false,
|
|
135
|
+
stealth: values.stealth ?? false,
|
|
136
|
+
proxy: values.proxy
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/instruction-generator/exploration-agent.ts
|
|
141
|
+
import { stepCountIs, Output } from "ai";
|
|
142
|
+
import { z as z2 } from "zod";
|
|
143
|
+
|
|
144
|
+
// src/instruction-generator/browser-tool.ts
|
|
145
|
+
import { tool } from "ai";
|
|
146
|
+
import { z } from "zod";
|
|
147
|
+
|
|
148
|
+
// src/harness/loop-detector.ts
|
|
149
|
+
function stepHash(step) {
|
|
150
|
+
const urlPath = extractPath(step.url);
|
|
151
|
+
return `${step.action.action}|${step.action.selector ?? ""}|${urlPath}`;
|
|
152
|
+
}
|
|
153
|
+
function extractPath(url) {
|
|
154
|
+
try {
|
|
155
|
+
return new URL(url).pathname;
|
|
156
|
+
} catch {
|
|
157
|
+
return url;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function detectLoop(recordedSteps, previousNudgeCount, windowSize = 4) {
|
|
161
|
+
const noLoop = {
|
|
162
|
+
isLoop: false,
|
|
163
|
+
loopType: null,
|
|
164
|
+
nudgeMessage: null,
|
|
165
|
+
nudgeCount: previousNudgeCount,
|
|
166
|
+
shouldPromptHuman: false
|
|
167
|
+
};
|
|
168
|
+
if (recordedSteps.length < 2) return noLoop;
|
|
169
|
+
const last = stepHash(recordedSteps[recordedSteps.length - 1]);
|
|
170
|
+
const secondLast = stepHash(recordedSteps[recordedSteps.length - 2]);
|
|
171
|
+
if (last === secondLast) {
|
|
172
|
+
return buildResult("exact_repeat", previousNudgeCount);
|
|
173
|
+
}
|
|
174
|
+
if (recordedSteps.length >= windowSize) {
|
|
175
|
+
const recentSteps = recordedSteps.slice(-windowSize);
|
|
176
|
+
const dataCollectionActions = /* @__PURE__ */ new Set(["download", "extract", "memory_append", "memory_aggregate", "export"]);
|
|
177
|
+
const hasDataCollection = recentSteps.some((s) => dataCollectionActions.has(s.action.action));
|
|
178
|
+
if (!hasDataCollection) {
|
|
179
|
+
const recentHashes = recentSteps.map(stepHash);
|
|
180
|
+
const halfLen = Math.floor(windowSize / 2);
|
|
181
|
+
const firstHalf = recentHashes.slice(0, halfLen).join(",");
|
|
182
|
+
const secondHalf = recentHashes.slice(halfLen, halfLen * 2).join(",");
|
|
183
|
+
if (firstHalf === secondHalf) {
|
|
184
|
+
return buildResult("action_cycle", previousNudgeCount);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (recordedSteps.length >= 4) {
|
|
189
|
+
const recentUrls = recordedSteps.slice(-4).map((s) => extractPath(s.url));
|
|
190
|
+
if (recentUrls[0] === recentUrls[2] && recentUrls[1] === recentUrls[3] && recentUrls[0] !== recentUrls[1]) {
|
|
191
|
+
return buildResult("url_bounce", previousNudgeCount);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const inputActions = /* @__PURE__ */ new Set(["fill", "type", "select", "check", "uncheck"]);
|
|
195
|
+
const selectorCounts = /* @__PURE__ */ new Map();
|
|
196
|
+
for (const step of recordedSteps) {
|
|
197
|
+
if (!step.success) continue;
|
|
198
|
+
if (!inputActions.has(step.action.action)) continue;
|
|
199
|
+
if (!step.action.selector) continue;
|
|
200
|
+
const key = `${step.action.action}|${step.action.selector}`;
|
|
201
|
+
const existing = selectorCounts.get(key);
|
|
202
|
+
if (existing) {
|
|
203
|
+
existing.count++;
|
|
204
|
+
} else {
|
|
205
|
+
selectorCounts.set(key, { count: 1, description: step.action.description });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const refills = [...selectorCounts.entries()].filter(([, v]) => v.count >= 3).map(([k, v]) => ({ selector: k.split("|")[1], count: v.count, description: v.description }));
|
|
209
|
+
if (refills.length > 0) {
|
|
210
|
+
const details = refills.map((r) => `${r.selector} "${r.description}" (${r.count}\u56DE)`).join(", ");
|
|
211
|
+
return buildRefillResult(details, previousNudgeCount);
|
|
212
|
+
}
|
|
213
|
+
return noLoop;
|
|
214
|
+
}
|
|
215
|
+
function buildResult(loopType, previousNudgeCount) {
|
|
216
|
+
const nudgeCount = previousNudgeCount + 1;
|
|
217
|
+
const messages = {
|
|
218
|
+
exact_repeat: "\u540C\u3058\u64CD\u4F5C\u3092\u7E70\u308A\u8FD4\u3057\u3066\u3044\u307E\u3059\u3002\u5225\u306E\u30A2\u30D7\u30ED\u30FC\u30C1\u3092\u8A66\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
219
|
+
action_cycle: "\u64CD\u4F5C\u304C\u30EB\u30FC\u30D7\u3057\u3066\u3044\u307E\u3059\u3002\u7570\u306A\u308B\u6226\u7565\u3092\u691C\u8A0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
220
|
+
url_bounce: "\u540C\u3058URL\u9593\u3092\u884C\u304D\u6765\u3057\u3066\u3044\u307E\u3059\u3002\u76EE\u7684\u306E\u30DA\u30FC\u30B8\u306B\u7559\u307E\u3063\u3066\u64CD\u4F5C\u3092\u9032\u3081\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
isLoop: true,
|
|
224
|
+
loopType,
|
|
225
|
+
nudgeMessage: messages[loopType],
|
|
226
|
+
nudgeCount,
|
|
227
|
+
// 1回目はナッジのみ、2回目以降は人間に確認
|
|
228
|
+
shouldPromptHuman: nudgeCount >= 2
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function buildRefillResult(details, previousNudgeCount) {
|
|
232
|
+
const nudgeCount = previousNudgeCount + 1;
|
|
233
|
+
return {
|
|
234
|
+
isLoop: true,
|
|
235
|
+
loopType: "selector_refill",
|
|
236
|
+
nudgeMessage: `\u540C\u3058\u30D5\u30A9\u30FC\u30E0\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u7E70\u308A\u8FD4\u3057\u5165\u529B\u3057\u3066\u3044\u307E\u3059\u3002\u4EE5\u4E0B\u306F\u65E2\u306B\u5165\u529B\u6E08\u307F\u3067\u3059: ${details}\u3002\u5165\u529B\u6E08\u307F\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u3001\u672A\u5165\u529B\u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u9032\u3093\u3067\u304F\u3060\u3055\u3044\u3002`,
|
|
237
|
+
nudgeCount,
|
|
238
|
+
shouldPromptHuman: nudgeCount >= 2
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/instruction-generator/browser-tool.ts
|
|
243
|
+
var suggestedCaptureSchema = z.object({
|
|
244
|
+
name: z.string(),
|
|
245
|
+
strategy: z.enum(["snapshot", "url", "ai", "expression", "evaluate"]),
|
|
246
|
+
description: z.string().optional(),
|
|
247
|
+
pattern: z.string().optional(),
|
|
248
|
+
group: z.number().optional(),
|
|
249
|
+
prompt: z.string().optional(),
|
|
250
|
+
expression: z.string().optional()
|
|
251
|
+
});
|
|
252
|
+
var aggregationSchema = z.object({
|
|
253
|
+
collection: z.string(),
|
|
254
|
+
field: z.string(),
|
|
255
|
+
operation: z.enum(["sum", "count", "concat", "min", "max", "avg", "unique_count"]),
|
|
256
|
+
outputVariable: z.string()
|
|
257
|
+
});
|
|
258
|
+
var actionSchema = z.object({
|
|
259
|
+
action: z.enum([
|
|
260
|
+
"click",
|
|
261
|
+
"fill",
|
|
262
|
+
"type",
|
|
263
|
+
"select",
|
|
264
|
+
"check",
|
|
265
|
+
"uncheck",
|
|
266
|
+
"navigate",
|
|
267
|
+
"wait",
|
|
268
|
+
"scroll",
|
|
269
|
+
"extract",
|
|
270
|
+
"download",
|
|
271
|
+
"export",
|
|
272
|
+
"memory_append",
|
|
273
|
+
"memory_aggregate"
|
|
274
|
+
]),
|
|
275
|
+
selector: z.string().optional(),
|
|
276
|
+
value: z.string().optional(),
|
|
277
|
+
description: z.string(),
|
|
278
|
+
inputCategory: z.enum(["credential", "user_data", "fixed", "navigation"]).optional(),
|
|
279
|
+
variableName: z.string().optional(),
|
|
280
|
+
suggestedCaptures: z.array(suggestedCaptureSchema).optional(),
|
|
281
|
+
/** extract 用: ページ内で実行する JavaScript */
|
|
282
|
+
script: z.string().optional(),
|
|
283
|
+
/** extract 用: AI にデータを抽出させるプロンプト */
|
|
284
|
+
extractPrompt: z.string().optional(),
|
|
285
|
+
/** memory_append 用: 蓄積先コレクション名 */
|
|
286
|
+
memoryCollection: z.string().optional(),
|
|
287
|
+
/** memory_aggregate 用: 集計設定 */
|
|
288
|
+
aggregation: aggregationSchema.optional(),
|
|
289
|
+
/** download 用: 保存先パス */
|
|
290
|
+
downloadPath: z.string().optional(),
|
|
291
|
+
/** export 用: 出力対象コレクション名 */
|
|
292
|
+
exportCollection: z.string().optional(),
|
|
293
|
+
/** export 用: 出力フォーマット */
|
|
294
|
+
exportFormat: z.enum(["csv", "json"]).optional(),
|
|
295
|
+
/** export 用: 出力先パス */
|
|
296
|
+
exportPath: z.string().optional()
|
|
297
|
+
});
|
|
298
|
+
var browserInputSchema = z.object({
|
|
299
|
+
actions: z.array(actionSchema).describe(
|
|
300
|
+
"\u5B9F\u884C\u3059\u308B\u30D6\u30E9\u30A6\u30B6\u64CD\u4F5C\u306E\u914D\u5217\u3002\u7A7A\u914D\u5217\u306A\u3089\u73FE\u5728\u306E\u30DA\u30FC\u30B8\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u306E\u307F\u8FD4\u3059\u3002"
|
|
301
|
+
)
|
|
302
|
+
});
|
|
303
|
+
function truncateExtractedData(data, maxLength) {
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(data);
|
|
306
|
+
if (Array.isArray(parsed)) {
|
|
307
|
+
const total = parsed.length;
|
|
308
|
+
const preview = parsed.slice(0, 3);
|
|
309
|
+
if (total <= 3) return JSON.stringify(preview);
|
|
310
|
+
return `${JSON.stringify(preview).slice(0, maxLength)} ... (\u5168${total}\u4EF6)`;
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
if (data.length <= maxLength) return data;
|
|
315
|
+
return `${data.slice(0, maxLength)}... (${data.length}\u6587\u5B57)`;
|
|
316
|
+
}
|
|
317
|
+
function getActionDelay(action, baseDelay) {
|
|
318
|
+
switch (action) {
|
|
319
|
+
case "navigate":
|
|
320
|
+
return Math.max(baseDelay, 1e3);
|
|
321
|
+
case "click":
|
|
322
|
+
return Math.round(baseDelay * 0.6);
|
|
323
|
+
case "fill":
|
|
324
|
+
case "type":
|
|
325
|
+
return Math.round(baseDelay * 0.2);
|
|
326
|
+
case "select":
|
|
327
|
+
return Math.round(baseDelay * 0.4);
|
|
328
|
+
default:
|
|
329
|
+
return baseDelay;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function cancellableSleep(ms, ic) {
|
|
333
|
+
if (!ic) return sleep(ms);
|
|
334
|
+
return new Promise((resolve) => {
|
|
335
|
+
const interval = 50;
|
|
336
|
+
let elapsed = 0;
|
|
337
|
+
const timer = setInterval(() => {
|
|
338
|
+
elapsed += interval;
|
|
339
|
+
if (elapsed >= ms || ic.isCancelRequested() || ic.isPauseRequested()) {
|
|
340
|
+
clearInterval(timer);
|
|
341
|
+
resolve();
|
|
342
|
+
}
|
|
343
|
+
}, interval);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function createBrowserTool(browser, recordedSteps, config, dataStore, downloadManager) {
|
|
347
|
+
let ordinal = 0;
|
|
348
|
+
let nudgeCount = 0;
|
|
349
|
+
let lastSnapshot = null;
|
|
350
|
+
let lastExtractedData;
|
|
351
|
+
const dl = config.debugLogger;
|
|
352
|
+
const filledFieldsMap = /* @__PURE__ */ new Map();
|
|
353
|
+
const inputActionTypes = /* @__PURE__ */ new Set(["fill", "type", "select", "check", "uncheck"]);
|
|
354
|
+
const scratchpadEntries = [];
|
|
355
|
+
const MAX_SCRATCHPAD = 10;
|
|
356
|
+
let lastUrl = "";
|
|
357
|
+
return tool({
|
|
358
|
+
description: "\u30D6\u30E9\u30A6\u30B6\u64CD\u4F5C\u3092\u5B9F\u884C\u3057\u3001\u64CD\u4F5C\u5F8C\u306E\u30DA\u30FC\u30B8\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3092\u8FD4\u3059\u3002actions\u304C\u7A7A\u306A\u3089\u73FE\u5728\u306E\u30DA\u30FC\u30B8\u72B6\u614B\u306E\u307F\u8FD4\u3059\u3002",
|
|
359
|
+
inputSchema: browserInputSchema,
|
|
360
|
+
execute: async (inputData) => {
|
|
361
|
+
const ic = config.interventionController;
|
|
362
|
+
let interventionNudge;
|
|
363
|
+
if (ic?.isCancelRequested()) {
|
|
364
|
+
const snapshot = lastSnapshot ?? await browser.snapshot();
|
|
365
|
+
const finalSnapshot2 = config.snapshotFilter ? filterSnapshot(snapshot) : snapshot;
|
|
366
|
+
return {
|
|
367
|
+
results: [],
|
|
368
|
+
snapshot: finalSnapshot2,
|
|
369
|
+
url: await browser.url(),
|
|
370
|
+
nudgeMessage: "\u30E6\u30FC\u30B6\u30FC\u304C\u63A2\u7D22\u306E\u30AD\u30E3\u30F3\u30BB\u30EB\u3092\u8981\u6C42\u3057\u307E\u3057\u305F\u3002goalAchieved: false \u3067\u6700\u7D42\u5831\u544A\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
371
|
+
stepCount: recordedSteps.length
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (ic?.isPauseRequested()) {
|
|
375
|
+
const currentUrl2 = await browser.url();
|
|
376
|
+
const interventionResult = await ic.collectIntervention(
|
|
377
|
+
recordedSteps.length,
|
|
378
|
+
currentUrl2
|
|
379
|
+
);
|
|
380
|
+
if (interventionResult.action === "abort") {
|
|
381
|
+
const snapshot = lastSnapshot ?? await browser.snapshot();
|
|
382
|
+
const finalSnapshot2 = config.snapshotFilter ? filterSnapshot(snapshot) : snapshot;
|
|
383
|
+
return {
|
|
384
|
+
results: [],
|
|
385
|
+
snapshot: finalSnapshot2,
|
|
386
|
+
url: currentUrl2,
|
|
387
|
+
nudgeMessage: "\u30E6\u30FC\u30B6\u30FC\u304C\u63A2\u7D22\u306E\u4E2D\u6B62\u3092\u8981\u6C42\u3057\u307E\u3057\u305F\u3002goalAchieved: false \u3067\u6700\u7D42\u5831\u544A\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
388
|
+
stepCount: recordedSteps.length
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (interventionResult.guidanceText) {
|
|
392
|
+
interventionNudge = tf("browserTool.interventionGuidance", { text: interventionResult.guidanceText });
|
|
393
|
+
}
|
|
394
|
+
dl?.log({
|
|
395
|
+
phase: "generator",
|
|
396
|
+
event: "user_intervention",
|
|
397
|
+
step: recordedSteps.length,
|
|
398
|
+
data: {
|
|
399
|
+
action: interventionResult.action,
|
|
400
|
+
guidanceText: interventionResult.guidanceText,
|
|
401
|
+
url: currentUrl2
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const results = [];
|
|
406
|
+
if (inputData.actions.length === 0) {
|
|
407
|
+
const snapshot = await browser.snapshot();
|
|
408
|
+
lastSnapshot = snapshot;
|
|
409
|
+
const currentUrl2 = await browser.url();
|
|
410
|
+
let finalSnapshot2 = config.snapshotFilter ? filterSnapshot(snapshot) : snapshot;
|
|
411
|
+
let emptyNudge;
|
|
412
|
+
const dataCollectionActionSet2 = /* @__PURE__ */ new Set(["download", "extract", "memory_append", "memory_aggregate"]);
|
|
413
|
+
const hasDataCollectionHistory2 = recordedSteps.some(
|
|
414
|
+
(s) => s.success && dataCollectionActionSet2.has(s.action.action)
|
|
415
|
+
);
|
|
416
|
+
if (hasDataCollectionHistory2) {
|
|
417
|
+
const pageMatch = snapshot.match(/Page\s+(\d+)\s+(?:of|\/)\s+(\d+)/i);
|
|
418
|
+
if (pageMatch) {
|
|
419
|
+
const cp = Number(pageMatch[1]);
|
|
420
|
+
const tp = Number(pageMatch[2]);
|
|
421
|
+
if (cp < tp) {
|
|
422
|
+
const pp = recordedSteps.filter(
|
|
423
|
+
(s) => s.success && (s.action.action === "download" || s.action.action === "extract")
|
|
424
|
+
).length;
|
|
425
|
+
emptyNudge = `Pagination progress: Page ${cp} of ${tp} (${pp} pages processed, ${tp - pp} remaining). You MUST continue processing ALL remaining pages \u2014 do NOT stop early.`;
|
|
426
|
+
const banner = `[PAGINATION INCOMPLETE: ${pp}/${tp} pages processed. ${tp - pp} pages remaining. You MUST continue \u2014 do NOT return goalAchieved yet.]`;
|
|
427
|
+
finalSnapshot2 = `${banner}
|
|
428
|
+
${finalSnapshot2}`;
|
|
429
|
+
if (config.paginationTracker) {
|
|
430
|
+
config.paginationTracker.currentPage = cp;
|
|
431
|
+
config.paginationTracker.totalPages = tp;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
dl?.log({
|
|
437
|
+
phase: "generator",
|
|
438
|
+
event: "snapshot",
|
|
439
|
+
step: recordedSteps.length,
|
|
440
|
+
data: { url: currentUrl2, snapshot, filteredSnapshot: finalSnapshot2 }
|
|
441
|
+
});
|
|
442
|
+
return {
|
|
443
|
+
results: [],
|
|
444
|
+
snapshot: finalSnapshot2,
|
|
445
|
+
url: currentUrl2,
|
|
446
|
+
nudgeMessage: emptyNudge,
|
|
447
|
+
stepCount: recordedSteps.length
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
const snapshotBefore = lastSnapshot ?? "";
|
|
451
|
+
for (const action of inputData.actions) {
|
|
452
|
+
if (ic?.isCancelRequested()) {
|
|
453
|
+
const snapshot = lastSnapshot ?? await browser.snapshot();
|
|
454
|
+
const finalSnapshot2 = config.snapshotFilter ? filterSnapshot(snapshot) : snapshot;
|
|
455
|
+
return {
|
|
456
|
+
results,
|
|
457
|
+
snapshot: finalSnapshot2,
|
|
458
|
+
url: await browser.url(),
|
|
459
|
+
nudgeMessage: "\u30E6\u30FC\u30B6\u30FC\u304C\u63A2\u7D22\u306E\u30AD\u30E3\u30F3\u30BB\u30EB\u3092\u8981\u6C42\u3057\u307E\u3057\u305F\u3002goalAchieved: false \u3067\u6700\u7D42\u5831\u544A\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
460
|
+
stepCount: recordedSteps.length
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (ic?.isPauseRequested()) {
|
|
464
|
+
const currentUrl2 = await browser.url();
|
|
465
|
+
const interventionResult = await ic.collectIntervention(
|
|
466
|
+
recordedSteps.length,
|
|
467
|
+
currentUrl2
|
|
468
|
+
);
|
|
469
|
+
if (interventionResult.action === "abort") {
|
|
470
|
+
const snapshot = lastSnapshot ?? await browser.snapshot();
|
|
471
|
+
const finalSnapshot2 = config.snapshotFilter ? filterSnapshot(snapshot) : snapshot;
|
|
472
|
+
return {
|
|
473
|
+
results,
|
|
474
|
+
snapshot: finalSnapshot2,
|
|
475
|
+
url: currentUrl2,
|
|
476
|
+
nudgeMessage: "\u30E6\u30FC\u30B6\u30FC\u304C\u63A2\u7D22\u306E\u4E2D\u6B62\u3092\u8981\u6C42\u3057\u307E\u3057\u305F\u3002goalAchieved: false \u3067\u6700\u7D42\u5831\u544A\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
477
|
+
stepCount: recordedSteps.length
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (interventionResult.guidanceText) {
|
|
481
|
+
interventionNudge = tf("browserTool.interventionGuidance", { text: interventionResult.guidanceText });
|
|
482
|
+
}
|
|
483
|
+
dl?.log({
|
|
484
|
+
phase: "generator",
|
|
485
|
+
event: "user_intervention",
|
|
486
|
+
step: recordedSteps.length,
|
|
487
|
+
data: {
|
|
488
|
+
action: interventionResult.action,
|
|
489
|
+
guidanceText: interventionResult.guidanceText,
|
|
490
|
+
url: currentUrl2
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
log.step(`${action.description} (${action.action} ${action.selector ?? ""})`);
|
|
495
|
+
if (action.action === "memory_append" && dataStore) {
|
|
496
|
+
const actionStart = performance.now();
|
|
497
|
+
const collection = action.memoryCollection ?? "default";
|
|
498
|
+
const rawValue = action.value ?? lastExtractedData ?? "";
|
|
499
|
+
const success = true;
|
|
500
|
+
const error = void 0;
|
|
501
|
+
parseAndAppendToMemory(rawValue, {
|
|
502
|
+
dataStore,
|
|
503
|
+
debugLogger: dl ?? noopLogger,
|
|
504
|
+
phase: "generator",
|
|
505
|
+
step: ordinal,
|
|
506
|
+
collection
|
|
507
|
+
});
|
|
508
|
+
const explorationAction2 = {
|
|
509
|
+
action: action.action,
|
|
510
|
+
value: rawValue,
|
|
511
|
+
description: action.description,
|
|
512
|
+
memoryCollection: collection
|
|
513
|
+
};
|
|
514
|
+
recordedSteps.push({
|
|
515
|
+
ordinal,
|
|
516
|
+
action: explorationAction2,
|
|
517
|
+
snapshotBefore,
|
|
518
|
+
url: await browser.url(),
|
|
519
|
+
success,
|
|
520
|
+
error,
|
|
521
|
+
durationMs: Math.round(performance.now() - actionStart)
|
|
522
|
+
});
|
|
523
|
+
ordinal++;
|
|
524
|
+
results.push({ description: action.description, success, error });
|
|
525
|
+
const collectionCount = dataStore.count(collection);
|
|
526
|
+
if (collectionCount % 50 === 0 || collectionCount <= 5) {
|
|
527
|
+
addScratchpadEntry(scratchpadEntries, MAX_SCRATCHPAD, `Memory: ${collection} now has ${collectionCount} items`);
|
|
528
|
+
}
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (action.action === "memory_aggregate" && dataStore && action.aggregation) {
|
|
532
|
+
const actionStart = performance.now();
|
|
533
|
+
const agg = action.aggregation;
|
|
534
|
+
let success = true;
|
|
535
|
+
let error;
|
|
536
|
+
let aggregateResult = "";
|
|
537
|
+
try {
|
|
538
|
+
aggregateResult = dataStore.aggregate({
|
|
539
|
+
collection: agg.collection,
|
|
540
|
+
field: agg.field,
|
|
541
|
+
operation: agg.operation
|
|
542
|
+
});
|
|
543
|
+
log.success(`Memory: ${agg.operation}(${agg.collection}.${agg.field}) = "${aggregateResult}"`);
|
|
544
|
+
lastExtractedData = aggregateResult;
|
|
545
|
+
} catch (e) {
|
|
546
|
+
success = false;
|
|
547
|
+
error = e instanceof Error ? e.message : String(e);
|
|
548
|
+
log.error(`Memory aggregate failed: ${error}`);
|
|
549
|
+
}
|
|
550
|
+
const explorationAction2 = {
|
|
551
|
+
action: action.action,
|
|
552
|
+
description: action.description,
|
|
553
|
+
aggregation: agg
|
|
554
|
+
};
|
|
555
|
+
recordedSteps.push({
|
|
556
|
+
ordinal,
|
|
557
|
+
action: explorationAction2,
|
|
558
|
+
snapshotBefore,
|
|
559
|
+
url: await browser.url(),
|
|
560
|
+
success,
|
|
561
|
+
error,
|
|
562
|
+
durationMs: Math.round(performance.now() - actionStart)
|
|
563
|
+
});
|
|
564
|
+
ordinal++;
|
|
565
|
+
results.push({ description: action.description, success, error });
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (action.action === "extract") {
|
|
569
|
+
const actionStart = performance.now();
|
|
570
|
+
let success = true;
|
|
571
|
+
let error;
|
|
572
|
+
let extractResult = "";
|
|
573
|
+
try {
|
|
574
|
+
if (action.script) {
|
|
575
|
+
dl?.log({
|
|
576
|
+
phase: "generator",
|
|
577
|
+
event: "extract",
|
|
578
|
+
step: ordinal,
|
|
579
|
+
data: { script: action.script.slice(0, 200) }
|
|
580
|
+
});
|
|
581
|
+
const result2 = await browser.evaluate(action.script);
|
|
582
|
+
const isArray = Array.isArray(result2);
|
|
583
|
+
const stringifyApplied = typeof result2 !== "string" && result2 !== null && result2 !== void 0;
|
|
584
|
+
extractResult = result2 === null || result2 === void 0 ? "" : typeof result2 === "string" ? result2 : JSON.stringify(result2);
|
|
585
|
+
dl?.log({
|
|
586
|
+
phase: "generator",
|
|
587
|
+
event: "extract_result",
|
|
588
|
+
step: ordinal,
|
|
589
|
+
data: {
|
|
590
|
+
resultType: result2 === null ? "null" : typeof result2,
|
|
591
|
+
isArray,
|
|
592
|
+
arrayLength: isArray ? result2.length : void 0,
|
|
593
|
+
resultPreview: extractResult.slice(0, 200),
|
|
594
|
+
storedLength: extractResult.length,
|
|
595
|
+
stringifyApplied
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
} else if (action.extractPrompt) {
|
|
599
|
+
dl?.log({
|
|
600
|
+
phase: "generator",
|
|
601
|
+
event: "extract",
|
|
602
|
+
step: ordinal,
|
|
603
|
+
data: { extractPrompt: action.extractPrompt.slice(0, 200) }
|
|
604
|
+
});
|
|
605
|
+
const snapshot = await browser.snapshot();
|
|
606
|
+
const { getModel: getModel2, trackedGenerateObject: trackedGenerateObject2 } = await import("./ai-model-NZ7QJNJW.js");
|
|
607
|
+
const extractSchema = z.object({ data: z.string() });
|
|
608
|
+
const aiResult = await trackedGenerateObject2("extraction", {
|
|
609
|
+
model: getModel2("extraction"),
|
|
610
|
+
prompt: `${action.extractPrompt}
|
|
611
|
+
|
|
612
|
+
\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8:
|
|
613
|
+
${snapshot}`,
|
|
614
|
+
schema: extractSchema,
|
|
615
|
+
temperature: 0
|
|
616
|
+
});
|
|
617
|
+
extractResult = aiResult.object?.data ?? "";
|
|
618
|
+
dl?.log({
|
|
619
|
+
phase: "generator",
|
|
620
|
+
event: "extract_result",
|
|
621
|
+
step: ordinal,
|
|
622
|
+
data: {
|
|
623
|
+
resultType: "string (AI)",
|
|
624
|
+
resultPreview: extractResult.slice(0, 200),
|
|
625
|
+
storedLength: extractResult.length
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
lastExtractedData = extractResult;
|
|
630
|
+
log.success(`Extracted: ${extractResult.slice(0, 100)}${extractResult.length > 100 ? "..." : ""}`);
|
|
631
|
+
const extractLen = extractResult.length;
|
|
632
|
+
addScratchpadEntry(scratchpadEntries, MAX_SCRATCHPAD, `Extracted ${extractLen > 200 ? `${extractLen} chars` : "data"} via ${action.script ? "script" : "AI prompt"}`);
|
|
633
|
+
} catch (e) {
|
|
634
|
+
success = false;
|
|
635
|
+
error = e instanceof Error ? e.message : String(e);
|
|
636
|
+
log.error(`Extract failed: ${error}`);
|
|
637
|
+
}
|
|
638
|
+
const explorationAction2 = {
|
|
639
|
+
action: action.action,
|
|
640
|
+
description: action.description,
|
|
641
|
+
script: action.script,
|
|
642
|
+
extractPrompt: action.extractPrompt
|
|
643
|
+
};
|
|
644
|
+
recordedSteps.push({
|
|
645
|
+
ordinal,
|
|
646
|
+
action: explorationAction2,
|
|
647
|
+
snapshotBefore,
|
|
648
|
+
url: await browser.url(),
|
|
649
|
+
success,
|
|
650
|
+
error,
|
|
651
|
+
durationMs: Math.round(performance.now() - actionStart)
|
|
652
|
+
});
|
|
653
|
+
ordinal++;
|
|
654
|
+
results.push({ description: action.description, success, error });
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (action.action === "download") {
|
|
658
|
+
const actionStart = performance.now();
|
|
659
|
+
let success = true;
|
|
660
|
+
let error;
|
|
661
|
+
const downloadPath = action.downloadPath ?? `/tmp/download-${Date.now()}.bin`;
|
|
662
|
+
try {
|
|
663
|
+
if (action.selector) {
|
|
664
|
+
await browser.download(action.selector, downloadPath);
|
|
665
|
+
} else {
|
|
666
|
+
await browser.waitForDownload(downloadPath);
|
|
667
|
+
}
|
|
668
|
+
log.success(`Downloaded: ${downloadPath}`);
|
|
669
|
+
if (downloadManager) {
|
|
670
|
+
downloadManager.addDownload({
|
|
671
|
+
path: downloadPath,
|
|
672
|
+
filename: downloadPath.split("/").pop() ?? "unknown",
|
|
673
|
+
stepOrdinal: ordinal,
|
|
674
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
} catch (e) {
|
|
678
|
+
success = false;
|
|
679
|
+
error = e instanceof Error ? e.message : String(e);
|
|
680
|
+
log.error(`Download failed: ${error}`);
|
|
681
|
+
}
|
|
682
|
+
const explorationAction2 = {
|
|
683
|
+
action: action.action,
|
|
684
|
+
selector: action.selector,
|
|
685
|
+
description: action.description,
|
|
686
|
+
downloadPath
|
|
687
|
+
};
|
|
688
|
+
recordedSteps.push({
|
|
689
|
+
ordinal,
|
|
690
|
+
action: explorationAction2,
|
|
691
|
+
snapshotBefore,
|
|
692
|
+
url: await browser.url(),
|
|
693
|
+
success,
|
|
694
|
+
error,
|
|
695
|
+
durationMs: Math.round(performance.now() - actionStart)
|
|
696
|
+
});
|
|
697
|
+
ordinal++;
|
|
698
|
+
results.push({ description: action.description, success, error });
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (action.action === "export" && dataStore) {
|
|
702
|
+
const actionStart = performance.now();
|
|
703
|
+
const collection = action.exportCollection ?? "default";
|
|
704
|
+
const format = action.exportFormat ?? "csv";
|
|
705
|
+
const exportPath = action.exportPath ?? `/tmp/${collection}.${format}`;
|
|
706
|
+
let success = true;
|
|
707
|
+
let error;
|
|
708
|
+
try {
|
|
709
|
+
const exportItems = dataStore.getAll(collection);
|
|
710
|
+
const sampleKeys = exportItems.length > 0 ? JSON.stringify(Object.keys(exportItems[0])) : "N/A";
|
|
711
|
+
dl?.log({
|
|
712
|
+
phase: "generator",
|
|
713
|
+
event: "export",
|
|
714
|
+
step: ordinal,
|
|
715
|
+
data: {
|
|
716
|
+
collection,
|
|
717
|
+
itemCount: exportItems.length,
|
|
718
|
+
format,
|
|
719
|
+
path: exportPath,
|
|
720
|
+
sampleKeys
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
await dataStore.writeToFile(collection, exportPath, format);
|
|
724
|
+
log.success(`Exported: ${collection} \u2192 ${exportPath} (${format})`);
|
|
725
|
+
lastExtractedData = exportPath;
|
|
726
|
+
if (downloadManager) {
|
|
727
|
+
downloadManager.addDownload({
|
|
728
|
+
path: exportPath,
|
|
729
|
+
filename: exportPath.split("/").pop() ?? "unknown",
|
|
730
|
+
stepOrdinal: ordinal,
|
|
731
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
} catch (e) {
|
|
735
|
+
success = false;
|
|
736
|
+
error = e instanceof Error ? e.message : String(e);
|
|
737
|
+
log.error(`Export failed: ${error}`);
|
|
738
|
+
}
|
|
739
|
+
const explorationAction2 = {
|
|
740
|
+
action: action.action,
|
|
741
|
+
description: action.description,
|
|
742
|
+
exportCollection: collection,
|
|
743
|
+
exportFormat: format,
|
|
744
|
+
exportPath
|
|
745
|
+
};
|
|
746
|
+
recordedSteps.push({
|
|
747
|
+
ordinal,
|
|
748
|
+
action: explorationAction2,
|
|
749
|
+
snapshotBefore,
|
|
750
|
+
url: await browser.url(),
|
|
751
|
+
success,
|
|
752
|
+
error,
|
|
753
|
+
durationMs: Math.round(performance.now() - actionStart)
|
|
754
|
+
});
|
|
755
|
+
ordinal++;
|
|
756
|
+
results.push({ description: action.description, success, error });
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const explorationAction = {
|
|
760
|
+
action: action.action,
|
|
761
|
+
selector: action.selector,
|
|
762
|
+
value: action.value,
|
|
763
|
+
description: action.description,
|
|
764
|
+
inputCategory: action.inputCategory,
|
|
765
|
+
variableName: action.variableName,
|
|
766
|
+
suggestedCaptures: action.suggestedCaptures
|
|
767
|
+
};
|
|
768
|
+
const navigatingActions = /* @__PURE__ */ new Set(["click", "navigate"]);
|
|
769
|
+
const mayNavigate = navigatingActions.has(action.action);
|
|
770
|
+
const urlBeforeAction = mayNavigate ? await browser.url() : "";
|
|
771
|
+
const pageCountBefore = mayNavigate ? await browser.pageCount() : 0;
|
|
772
|
+
const result = await browser.executeStep(explorationAction);
|
|
773
|
+
if (mayNavigate) {
|
|
774
|
+
await browser.waitForPossibleNavigation(
|
|
775
|
+
urlBeforeAction,
|
|
776
|
+
pageCountBefore
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
const stepUrl = await browser.url();
|
|
780
|
+
const recorded = {
|
|
781
|
+
ordinal,
|
|
782
|
+
action: explorationAction,
|
|
783
|
+
snapshotBefore,
|
|
784
|
+
url: stepUrl,
|
|
785
|
+
success: result.success,
|
|
786
|
+
error: result.error,
|
|
787
|
+
durationMs: result.durationMs,
|
|
788
|
+
failureCategory: result.success ? void 0 : classifyFailure({
|
|
789
|
+
error: result.error ?? "unknown error",
|
|
790
|
+
selectorResolved: !!action.selector,
|
|
791
|
+
actionExecuted: true
|
|
792
|
+
})
|
|
793
|
+
};
|
|
794
|
+
recordedSteps.push(recorded);
|
|
795
|
+
ordinal++;
|
|
796
|
+
results.push({
|
|
797
|
+
description: action.description,
|
|
798
|
+
success: result.success,
|
|
799
|
+
error: result.error
|
|
800
|
+
});
|
|
801
|
+
dl?.log({
|
|
802
|
+
phase: "generator",
|
|
803
|
+
event: "action",
|
|
804
|
+
step: ordinal - 1,
|
|
805
|
+
data: {
|
|
806
|
+
action: action.action,
|
|
807
|
+
selector: action.selector,
|
|
808
|
+
value: action.value,
|
|
809
|
+
description: action.description,
|
|
810
|
+
result: {
|
|
811
|
+
success: result.success,
|
|
812
|
+
error: result.error,
|
|
813
|
+
durationMs: result.durationMs
|
|
814
|
+
},
|
|
815
|
+
url: stepUrl,
|
|
816
|
+
failureCategory: recorded.failureCategory
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
if (result.success) {
|
|
820
|
+
log.success(`OK (${result.durationMs}ms)`);
|
|
821
|
+
if (inputActionTypes.has(action.action) && action.selector) {
|
|
822
|
+
filledFieldsMap.set(action.selector, action.description);
|
|
823
|
+
}
|
|
824
|
+
if (mayNavigate && stepUrl !== urlBeforeAction) {
|
|
825
|
+
addScratchpadEntry(scratchpadEntries, MAX_SCRATCHPAD, `Navigated ${shortenUrl(urlBeforeAction)} \u2192 ${shortenUrl(stepUrl)}`);
|
|
826
|
+
lastUrl = stepUrl;
|
|
827
|
+
}
|
|
828
|
+
} else {
|
|
829
|
+
log.error(`FAILED: ${result.error}`);
|
|
830
|
+
addScratchpadEntry(scratchpadEntries, MAX_SCRATCHPAD, `${action.action} ${action.selector ?? ""} failed: ${(result.error ?? "unknown").slice(0, 60)}`);
|
|
831
|
+
}
|
|
832
|
+
await cancellableSleep(getActionDelay(action.action, config.stepDelay), ic);
|
|
833
|
+
}
|
|
834
|
+
let navigateNudge;
|
|
835
|
+
if (inputData.actions.some((a) => a.action === "navigate") && lastSnapshot) {
|
|
836
|
+
const linkCount = parseAllElements(lastSnapshot).filter((e) => e.role === "link").length;
|
|
837
|
+
if (linkCount > 0) {
|
|
838
|
+
navigateNudge = `\u6CE8\u610F: \u30DA\u30FC\u30B8\u4E0A\u306B ${linkCount} \u500B\u306E\u30EA\u30F3\u30AF\u304C\u3042\u308A\u307E\u3059\u3002navigate \u3067URL\u3092\u7D44\u307F\u7ACB\u3066\u308B\u306E\u3067\u306F\u306A\u304F\u3001\u30DA\u30FC\u30B8\u4E0A\u306E\u30EA\u30F3\u30AF\u3092 click \u3067\u8FBF\u3063\u3066\u304F\u3060\u3055\u3044\u3002`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
let nudgeMessage = navigateNudge;
|
|
842
|
+
if (recordedSteps.length >= 2) {
|
|
843
|
+
const loopResult = detectLoop(recordedSteps, nudgeCount);
|
|
844
|
+
if (loopResult.isLoop) {
|
|
845
|
+
nudgeCount = loopResult.nudgeCount;
|
|
846
|
+
const loopNudge = loopResult.nudgeMessage ?? void 0;
|
|
847
|
+
nudgeMessage = nudgeMessage && loopNudge ? `${nudgeMessage}
|
|
848
|
+
${loopNudge}` : loopNudge ?? nudgeMessage;
|
|
849
|
+
log.warn(`Loop detected: ${loopResult.loopType}`);
|
|
850
|
+
dl?.log({
|
|
851
|
+
phase: "generator",
|
|
852
|
+
event: "loop_detection",
|
|
853
|
+
step: recordedSteps.length - 1,
|
|
854
|
+
data: loopResult
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const postSnapshot = await browser.snapshot();
|
|
859
|
+
const currentUrl = await browser.url();
|
|
860
|
+
const textOnlyActions = /* @__PURE__ */ new Set(["fill", "type"]);
|
|
861
|
+
const allTextOnly = inputData.actions.length > 0 && inputData.actions.every((a) => textOnlyActions.has(a.action)) && results.every((r) => r.success);
|
|
862
|
+
let finalSnapshot;
|
|
863
|
+
if (allTextOnly && lastSnapshot) {
|
|
864
|
+
const oldSig = extractStructuralSignature(lastSnapshot);
|
|
865
|
+
const newSig = extractStructuralSignature(postSnapshot);
|
|
866
|
+
if (oldSig === newSig) {
|
|
867
|
+
finalSnapshot = buildDiffSnapshot(postSnapshot, filledFieldsMap);
|
|
868
|
+
dl?.log({
|
|
869
|
+
phase: "generator",
|
|
870
|
+
event: "snapshot_diff",
|
|
871
|
+
step: recordedSteps.length - 1,
|
|
872
|
+
data: { url: currentUrl, reason: "structure_unchanged", elementCount: newSig.split("\n").length }
|
|
873
|
+
});
|
|
874
|
+
} else {
|
|
875
|
+
const filledSelectors = filledFieldsMap.size > 0 ? new Set(filledFieldsMap.keys()) : void 0;
|
|
876
|
+
finalSnapshot = config.snapshotFilter ? filterSnapshot(postSnapshot, { filledSelectors }) : postSnapshot;
|
|
877
|
+
dl?.log({
|
|
878
|
+
phase: "generator",
|
|
879
|
+
event: "snapshot",
|
|
880
|
+
step: recordedSteps.length - 1,
|
|
881
|
+
data: { url: currentUrl, snapshot: postSnapshot, filteredSnapshot: finalSnapshot }
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
const filledSelectors = filledFieldsMap.size > 0 ? new Set(filledFieldsMap.keys()) : void 0;
|
|
886
|
+
finalSnapshot = config.snapshotFilter ? filterSnapshot(postSnapshot, { filledSelectors }) : postSnapshot;
|
|
887
|
+
dl?.log({
|
|
888
|
+
phase: "generator",
|
|
889
|
+
event: "snapshot",
|
|
890
|
+
step: recordedSteps.length - 1,
|
|
891
|
+
data: { url: currentUrl, snapshot: postSnapshot, filteredSnapshot: finalSnapshot }
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
lastSnapshot = postSnapshot;
|
|
895
|
+
if (interventionNudge) {
|
|
896
|
+
nudgeMessage = nudgeMessage ? `${interventionNudge}
|
|
897
|
+
${nudgeMessage}` : interventionNudge;
|
|
898
|
+
}
|
|
899
|
+
const memoryStatus = dataStore ? Object.fromEntries(
|
|
900
|
+
dataStore.listCollections().map((c) => [c, dataStore.count(c)])
|
|
901
|
+
) : void 0;
|
|
902
|
+
const hasExtractAction = inputData.actions.some(
|
|
903
|
+
(a) => a.action === "extract" || a.action === "memory_aggregate"
|
|
904
|
+
);
|
|
905
|
+
const extractedDataPreview = hasExtractAction && lastExtractedData ? truncateExtractedData(lastExtractedData, 500) : void 0;
|
|
906
|
+
const keepRecent = 5;
|
|
907
|
+
if (recordedSteps.length > keepRecent) {
|
|
908
|
+
recordedSteps[recordedSteps.length - keepRecent - 1].snapshotBefore = "";
|
|
909
|
+
}
|
|
910
|
+
const filledFieldsSummary = filledFieldsMap.size > 0 ? [...filledFieldsMap.entries()].map(([sel, desc]) => `${sel}: ${desc}`).join(", ") : void 0;
|
|
911
|
+
let paginationCurrentPage = 0;
|
|
912
|
+
let paginationTotalPages = 0;
|
|
913
|
+
let paginationPagesProcessed = 0;
|
|
914
|
+
const dataCollectionActionSet = /* @__PURE__ */ new Set(["download", "extract", "memory_append", "memory_aggregate"]);
|
|
915
|
+
const hasDataCollectionHistory = recordedSteps.some(
|
|
916
|
+
(s) => s.success && dataCollectionActionSet.has(s.action.action)
|
|
917
|
+
);
|
|
918
|
+
if (hasDataCollectionHistory) {
|
|
919
|
+
const pageMatch = postSnapshot.match(/Page\s+(\d+)\s+(?:of|\/)\s+(\d+)/i);
|
|
920
|
+
if (pageMatch) {
|
|
921
|
+
paginationCurrentPage = Number(pageMatch[1]);
|
|
922
|
+
paginationTotalPages = Number(pageMatch[2]);
|
|
923
|
+
if (paginationCurrentPage < paginationTotalPages) {
|
|
924
|
+
paginationPagesProcessed = recordedSteps.filter(
|
|
925
|
+
(s) => s.success && (s.action.action === "download" || s.action.action === "extract")
|
|
926
|
+
).length;
|
|
927
|
+
const paginationNudge = `Pagination progress: Page ${paginationCurrentPage} of ${paginationTotalPages} (${paginationPagesProcessed} pages processed, ${paginationTotalPages - paginationPagesProcessed} remaining). You MUST continue processing ALL remaining pages \u2014 do NOT stop early.`;
|
|
928
|
+
nudgeMessage = nudgeMessage ? `${nudgeMessage}
|
|
929
|
+
${paginationNudge}` : paginationNudge;
|
|
930
|
+
if (config.paginationTracker) {
|
|
931
|
+
config.paginationTracker.currentPage = paginationCurrentPage;
|
|
932
|
+
config.paginationTracker.totalPages = paginationTotalPages;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (filledFieldsMap.size >= 3) {
|
|
938
|
+
const formRoles = /* @__PURE__ */ new Set(["textbox", "combobox", "checkbox", "spinbutton"]);
|
|
939
|
+
const filledRefs = new Set(
|
|
940
|
+
[...filledFieldsMap.keys()].map((s) => s.replace("@", ""))
|
|
941
|
+
);
|
|
942
|
+
const allElements = parseAllElements(postSnapshot);
|
|
943
|
+
const remainingFormElements = allElements.filter(
|
|
944
|
+
(e) => formRoles.has(e.role) && !filledRefs.has(e.ref)
|
|
945
|
+
);
|
|
946
|
+
if (remainingFormElements.length > 0) {
|
|
947
|
+
const names = remainingFormElements.map((e) => e.name ? `"${e.name}"` : `@${e.ref}`).join(", ");
|
|
948
|
+
const formHint = `Note: ${remainingFormElements.length} form fields not yet filled: ${names}. Fill all remaining fields before submitting.`;
|
|
949
|
+
nudgeMessage = nudgeMessage ? `${nudgeMessage}
|
|
950
|
+
${formHint}` : formHint;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (paginationCurrentPage > 0 && paginationCurrentPage < paginationTotalPages) {
|
|
954
|
+
const banner = `[PAGINATION INCOMPLETE: ${paginationPagesProcessed}/${paginationTotalPages} pages processed. ${paginationTotalPages - paginationPagesProcessed} pages remaining. You MUST continue \u2014 do NOT return goalAchieved yet.]`;
|
|
955
|
+
finalSnapshot = `${banner}
|
|
956
|
+
${finalSnapshot}`;
|
|
957
|
+
}
|
|
958
|
+
const scratchpad = scratchpadEntries.length > 0 ? scratchpadEntries.join("\n") : void 0;
|
|
959
|
+
return {
|
|
960
|
+
results,
|
|
961
|
+
snapshot: finalSnapshot,
|
|
962
|
+
url: currentUrl,
|
|
963
|
+
nudgeMessage,
|
|
964
|
+
stepCount: recordedSteps.length,
|
|
965
|
+
...memoryStatus && Object.keys(memoryStatus).length > 0 ? { memoryStatus } : {},
|
|
966
|
+
...extractedDataPreview ? { extractedData: extractedDataPreview } : {},
|
|
967
|
+
...filledFieldsSummary ? { filledFields: filledFieldsSummary } : {},
|
|
968
|
+
...scratchpad ? { scratchpad } : {}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
function addScratchpadEntry(entries, max, entry) {
|
|
974
|
+
entries.push(entry);
|
|
975
|
+
if (entries.length > max) {
|
|
976
|
+
entries.shift();
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function shortenUrl(url) {
|
|
980
|
+
try {
|
|
981
|
+
const parsed = new URL(url);
|
|
982
|
+
return parsed.pathname + (parsed.search ? parsed.search.slice(0, 30) : "");
|
|
983
|
+
} catch {
|
|
984
|
+
return url.slice(0, 50);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function extractStructuralSignature(snapshot) {
|
|
988
|
+
return snapshot.split("\n").map((line) => {
|
|
989
|
+
const indent = line.length - line.trimStart().length;
|
|
990
|
+
const trimmed = line.trimStart().replace(/^-\s+/, "");
|
|
991
|
+
const roleMatch = trimmed.match(/^(\S+)/);
|
|
992
|
+
const refMatch = line.match(/\[ref=(e\d+)\]/);
|
|
993
|
+
return `${indent}:${roleMatch?.[1] ?? ""}:${refMatch?.[1] ?? ""}`;
|
|
994
|
+
}).join("\n");
|
|
995
|
+
}
|
|
996
|
+
function buildDiffSnapshot(snapshot, filledFieldsMap) {
|
|
997
|
+
const elements = parseAllElements(snapshot);
|
|
998
|
+
const filledRefs = new Set(
|
|
999
|
+
[...filledFieldsMap.keys()].map((sel) => sel.replace("@", ""))
|
|
1000
|
+
);
|
|
1001
|
+
const unfilled = [];
|
|
1002
|
+
const filled = [];
|
|
1003
|
+
for (const el of elements) {
|
|
1004
|
+
const label = `@${el.ref} ${el.role}${el.name ? ` "${el.name}"` : ""}`;
|
|
1005
|
+
if (filledRefs.has(el.ref)) {
|
|
1006
|
+
filled.push(label);
|
|
1007
|
+
} else {
|
|
1008
|
+
unfilled.push(label);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const lines = [
|
|
1012
|
+
`[Structure unchanged after text input. ${elements.length} interactive elements, all refs valid.]`
|
|
1013
|
+
];
|
|
1014
|
+
if (unfilled.length > 0) {
|
|
1015
|
+
lines.push(`Unfilled: ${unfilled.join(", ")}`);
|
|
1016
|
+
}
|
|
1017
|
+
if (filled.length > 0) {
|
|
1018
|
+
lines.push(`Filled (${filled.length}): ${filled.join(", ")}`);
|
|
1019
|
+
}
|
|
1020
|
+
return lines.join("\n");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/instruction-generator/exploration-agent.ts
|
|
1024
|
+
function routeExplorationModel(stepNumber, messages, recordedSteps) {
|
|
1025
|
+
if (stepNumber <= 2) {
|
|
1026
|
+
return { purpose: "exploration", reason: "initial_observation" };
|
|
1027
|
+
}
|
|
1028
|
+
const toolIndices = [];
|
|
1029
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1030
|
+
if (messages[i].role === "tool") toolIndices.push(i);
|
|
1031
|
+
}
|
|
1032
|
+
if (toolIndices.length > 0) {
|
|
1033
|
+
const lastToolMsg = messages[toolIndices[toolIndices.length - 1]];
|
|
1034
|
+
const lastVal = extractToolOutputValue(lastToolMsg);
|
|
1035
|
+
if (lastVal) {
|
|
1036
|
+
const results = lastVal.results;
|
|
1037
|
+
if (results?.some((r) => !r.success)) {
|
|
1038
|
+
return { purpose: "exploration", reason: "previous_step_failed" };
|
|
1039
|
+
}
|
|
1040
|
+
if (lastVal.nudgeMessage) {
|
|
1041
|
+
return { purpose: "exploration", reason: "nudge_active" };
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (recordedSteps.length >= 2) {
|
|
1046
|
+
const last = recordedSteps[recordedSteps.length - 1];
|
|
1047
|
+
const prev = recordedSteps[recordedSteps.length - 2];
|
|
1048
|
+
if (last.url !== prev.url) {
|
|
1049
|
+
return { purpose: "exploration", reason: "page_changed" };
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (toolIndices.length > 0) {
|
|
1053
|
+
const phase = detectExplorationPhase(messages, toolIndices);
|
|
1054
|
+
if (phase === "form_filling") {
|
|
1055
|
+
return { purpose: "exploration-light", reason: "routine_form_fill" };
|
|
1056
|
+
}
|
|
1057
|
+
if (phase === "data_collection") {
|
|
1058
|
+
return { purpose: "exploration-light", reason: "routine_data_collection" };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (recordedSteps.length >= 3) {
|
|
1062
|
+
const recent = recordedSteps.slice(-3);
|
|
1063
|
+
const allSucceeded = recent.every((s) => s.success);
|
|
1064
|
+
const sameUrl = recent.every((s) => s.url === recent[0].url);
|
|
1065
|
+
if (allSucceeded && sameUrl) {
|
|
1066
|
+
return { purpose: "exploration-light", reason: "stable_operation" };
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return { purpose: "exploration", reason: "default" };
|
|
1070
|
+
}
|
|
1071
|
+
async function exploreWithAI(browser, recordedSteps, config, dataStore, downloadManager) {
|
|
1072
|
+
const paginationTracker = { currentPage: 0, totalPages: 0 };
|
|
1073
|
+
const toolConfig = {
|
|
1074
|
+
stepDelay: config.stepDelay,
|
|
1075
|
+
screenshotDir: config.screenshotDir,
|
|
1076
|
+
snapshotFilter: config.snapshotFilter,
|
|
1077
|
+
debugLogger: config.debugLogger,
|
|
1078
|
+
interventionController: config.interventionController,
|
|
1079
|
+
paginationTracker
|
|
1080
|
+
};
|
|
1081
|
+
const browserTool = createBrowserTool(browser, recordedSteps, toolConfig, dataStore, downloadManager);
|
|
1082
|
+
const totalMaxSteps = config.maxIterations * 3;
|
|
1083
|
+
const systemMessage = {
|
|
1084
|
+
role: "system",
|
|
1085
|
+
content: getAgentInstructions(config.goal, config.contextMarkdown, config.secrets),
|
|
1086
|
+
providerOptions: {
|
|
1087
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
const STALL_THRESHOLD = 6;
|
|
1091
|
+
let lastProgressPage = 0;
|
|
1092
|
+
let stepsWithoutProgress = 0;
|
|
1093
|
+
const prepareStep = ({ messages, stepNumber }) => {
|
|
1094
|
+
if (stepNumber <= 1) return void 0;
|
|
1095
|
+
const pruned = pruneOldToolResults(messages, config.historyWindow);
|
|
1096
|
+
if (paginationTracker.currentPage > lastProgressPage) {
|
|
1097
|
+
lastProgressPage = paginationTracker.currentPage;
|
|
1098
|
+
stepsWithoutProgress = 0;
|
|
1099
|
+
} else {
|
|
1100
|
+
stepsWithoutProgress++;
|
|
1101
|
+
}
|
|
1102
|
+
const paginationInProgress = paginationTracker.totalPages > 0 && paginationTracker.currentPage < paginationTracker.totalPages;
|
|
1103
|
+
const notStalled = stepsWithoutProgress < STALL_THRESHOLD;
|
|
1104
|
+
if (config.enableMultiModel) {
|
|
1105
|
+
const decision = routeExplorationModel(stepNumber, messages, recordedSteps);
|
|
1106
|
+
const model = getModel(decision.purpose);
|
|
1107
|
+
if (paginationInProgress && notStalled) {
|
|
1108
|
+
return { messages: pruned, toolChoice: "required", model };
|
|
1109
|
+
}
|
|
1110
|
+
return { messages: pruned, model };
|
|
1111
|
+
}
|
|
1112
|
+
if (paginationInProgress && notStalled) {
|
|
1113
|
+
return { messages: pruned, toolChoice: "required" };
|
|
1114
|
+
}
|
|
1115
|
+
return { messages: pruned };
|
|
1116
|
+
};
|
|
1117
|
+
const result = await trackedGenerateText("exploration", {
|
|
1118
|
+
model: getModel("exploration"),
|
|
1119
|
+
messages: [
|
|
1120
|
+
systemMessage,
|
|
1121
|
+
{ role: "user", content: getInitialUserMessage() }
|
|
1122
|
+
],
|
|
1123
|
+
tools: { browser: browserTool },
|
|
1124
|
+
output: Output.object({ schema: agentResultSchema }),
|
|
1125
|
+
// +1: 構造化出力の生成は追加ステップとしてカウントされる
|
|
1126
|
+
stopWhen: [stepCountIs(totalMaxSteps + 1)],
|
|
1127
|
+
prepareStep
|
|
1128
|
+
});
|
|
1129
|
+
const goalResult = result.output ?? parseAgentResultFallback(result.text);
|
|
1130
|
+
return {
|
|
1131
|
+
goalResult,
|
|
1132
|
+
totalTokens: {
|
|
1133
|
+
promptTokens: result.usage.inputTokens ?? 0,
|
|
1134
|
+
completionTokens: result.usage.outputTokens ?? 0,
|
|
1135
|
+
totalTokens: (result.usage.inputTokens ?? 0) + (result.usage.outputTokens ?? 0)
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function pruneOldToolResults(messages, historyWindow = 10) {
|
|
1140
|
+
let toolIndices = [];
|
|
1141
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1142
|
+
if (messages[i].role === "tool") toolIndices.push(i);
|
|
1143
|
+
}
|
|
1144
|
+
if (toolIndices.length <= 2) return messages;
|
|
1145
|
+
const phase = detectExplorationPhase(messages, toolIndices);
|
|
1146
|
+
const rawSize = phase === "form_filling" ? 4 : 2;
|
|
1147
|
+
const compactSize = phase === "form_filling" ? 4 : 2;
|
|
1148
|
+
if (toolIndices.length > historyWindow) {
|
|
1149
|
+
const cutoffIndex = toolIndices[toolIndices.length - historyWindow];
|
|
1150
|
+
let startIndex = cutoffIndex;
|
|
1151
|
+
if (startIndex > 0 && messages[startIndex - 1].role === "assistant") {
|
|
1152
|
+
startIndex = startIndex - 1;
|
|
1153
|
+
}
|
|
1154
|
+
const preamble = messages.filter(
|
|
1155
|
+
(m, i) => i < startIndex && (m.role === "system" || m.role === "user" && i < 3)
|
|
1156
|
+
);
|
|
1157
|
+
const droppedCount = toolIndices.length - historyWindow;
|
|
1158
|
+
const summaryMsg = {
|
|
1159
|
+
role: "user",
|
|
1160
|
+
content: `[${droppedCount} earlier tool turns omitted for context efficiency]`
|
|
1161
|
+
};
|
|
1162
|
+
messages = [...preamble, summaryMsg, ...messages.slice(startIndex)];
|
|
1163
|
+
toolIndices = [];
|
|
1164
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1165
|
+
if (messages[i].role === "tool") toolIndices.push(i);
|
|
1166
|
+
}
|
|
1167
|
+
if (toolIndices.length <= 2) return messages;
|
|
1168
|
+
}
|
|
1169
|
+
const estimatedChars = estimateMessageChars(messages);
|
|
1170
|
+
const HIGH_PRESSURE_CHARS = 4e5;
|
|
1171
|
+
const isHighPressure = estimatedChars > HIGH_PRESSURE_CHARS;
|
|
1172
|
+
const effectiveRawSize = isHighPressure ? Math.max(1, rawSize - 1) : rawSize;
|
|
1173
|
+
const effectiveCompactSize = isHighPressure ? Math.max(1, compactSize - 1) : compactSize;
|
|
1174
|
+
const recentThreshold = toolIndices[Math.max(0, toolIndices.length - effectiveRawSize)];
|
|
1175
|
+
const compactThreshold = toolIndices.length > effectiveRawSize + effectiveCompactSize ? toolIndices[toolIndices.length - effectiveRawSize - effectiveCompactSize] : 0;
|
|
1176
|
+
return messages.map((msg, i) => {
|
|
1177
|
+
if (msg.role !== "tool") return msg;
|
|
1178
|
+
if (i >= recentThreshold) return msg;
|
|
1179
|
+
if (i >= compactThreshold) return compactToolResult(msg);
|
|
1180
|
+
return summarizeToolResult(msg);
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
function detectExplorationPhase(messages, toolIndices) {
|
|
1184
|
+
const recentTools = toolIndices.slice(-3);
|
|
1185
|
+
let fillCount = 0;
|
|
1186
|
+
let extractCount = 0;
|
|
1187
|
+
for (const idx of recentTools) {
|
|
1188
|
+
const msg = messages[idx];
|
|
1189
|
+
const val = extractToolOutputValue(msg);
|
|
1190
|
+
if (val) {
|
|
1191
|
+
if ("filledFields" in val && val.filledFields) fillCount++;
|
|
1192
|
+
if ("extractedData" in val && val.extractedData) extractCount++;
|
|
1193
|
+
if ("memoryStatus" in val && val.memoryStatus) extractCount++;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
if (fillCount >= 2) return "form_filling";
|
|
1197
|
+
if (extractCount >= 2) return "data_collection";
|
|
1198
|
+
return "exploring";
|
|
1199
|
+
}
|
|
1200
|
+
function extractToolOutputValue(msg) {
|
|
1201
|
+
if (!Array.isArray(msg.content)) return null;
|
|
1202
|
+
const parts = msg.content;
|
|
1203
|
+
for (const part of parts) {
|
|
1204
|
+
if (part.type !== "tool-result") continue;
|
|
1205
|
+
const output = part.output;
|
|
1206
|
+
if (output?.type === "json" && typeof output.value === "object" && output.value !== null) {
|
|
1207
|
+
return output.value;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
function estimateMessageChars(messages) {
|
|
1213
|
+
let total = 0;
|
|
1214
|
+
for (const msg of messages) {
|
|
1215
|
+
if (typeof msg.content === "string") {
|
|
1216
|
+
total += msg.content.length;
|
|
1217
|
+
} else if (Array.isArray(msg.content)) {
|
|
1218
|
+
total += JSON.stringify(msg.content).length;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return total;
|
|
1222
|
+
}
|
|
1223
|
+
function mapToolResultValue(msg, transform) {
|
|
1224
|
+
if (!Array.isArray(msg.content)) return msg;
|
|
1225
|
+
const parts = msg.content;
|
|
1226
|
+
const prunedContent = parts.map((part) => {
|
|
1227
|
+
if (part.type !== "tool-result") return part;
|
|
1228
|
+
const output = part.output;
|
|
1229
|
+
if (typeof output !== "object" || output === null) return part;
|
|
1230
|
+
const out = output;
|
|
1231
|
+
if (out.type === "json" && typeof out.value === "object" && out.value !== null) {
|
|
1232
|
+
const val = { ...out.value };
|
|
1233
|
+
const transformed = transform(val);
|
|
1234
|
+
return { ...part, output: { ...out, value: transformed } };
|
|
1235
|
+
}
|
|
1236
|
+
return part;
|
|
1237
|
+
});
|
|
1238
|
+
return { ...msg, content: prunedContent };
|
|
1239
|
+
}
|
|
1240
|
+
function compactToolResult(msg) {
|
|
1241
|
+
return mapToolResultValue(msg, (val) => {
|
|
1242
|
+
if ("snapshot" in val) {
|
|
1243
|
+
val.snapshot = "[\u7701\u7565]";
|
|
1244
|
+
}
|
|
1245
|
+
if ("extractedData" in val) {
|
|
1246
|
+
delete val.extractedData;
|
|
1247
|
+
}
|
|
1248
|
+
return val;
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
function summarizeToolResult(msg) {
|
|
1252
|
+
return mapToolResultValue(msg, (val) => {
|
|
1253
|
+
if ("snapshot" in val) {
|
|
1254
|
+
delete val.snapshot;
|
|
1255
|
+
}
|
|
1256
|
+
if ("extractedData" in val) {
|
|
1257
|
+
delete val.extractedData;
|
|
1258
|
+
}
|
|
1259
|
+
if ("results" in val && Array.isArray(val.results)) {
|
|
1260
|
+
const results = val.results;
|
|
1261
|
+
const failures = results.filter((r) => !r.success);
|
|
1262
|
+
const successes = results.filter((r) => r.success);
|
|
1263
|
+
const compressed = [...failures];
|
|
1264
|
+
if (successes.length > 0) {
|
|
1265
|
+
compressed.push({ description: `${successes.length}\u4EF6\u6210\u529F`, success: true });
|
|
1266
|
+
}
|
|
1267
|
+
val.results = compressed;
|
|
1268
|
+
}
|
|
1269
|
+
return val;
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
var agentResultSchema = z2.object({
|
|
1273
|
+
goalAchieved: z2.boolean(),
|
|
1274
|
+
summary: z2.string().default("")
|
|
1275
|
+
});
|
|
1276
|
+
function parseAgentResultFallback(text) {
|
|
1277
|
+
try {
|
|
1278
|
+
return agentResultSchema.parse(JSON.parse(text.trim()));
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1282
|
+
if (jsonMatch) {
|
|
1283
|
+
try {
|
|
1284
|
+
return agentResultSchema.parse(JSON.parse(jsonMatch[0]));
|
|
1285
|
+
} catch {
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
const achieved = /ゴール.*達成|goal.*achieved|完了しました/i.test(text);
|
|
1289
|
+
return { goalAchieved: achieved, summary: text.slice(0, 200) };
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/instruction-generator/intervention-controller.ts
|
|
1293
|
+
var InterventionController = class {
|
|
1294
|
+
constructor() {
|
|
1295
|
+
this.pauseRequested = false;
|
|
1296
|
+
this.cancelRequested = false;
|
|
1297
|
+
this.stdinCleanup = null;
|
|
1298
|
+
this.options = {};
|
|
1299
|
+
this.interventions = [];
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* spinner コールバックを後から設定する
|
|
1303
|
+
*/
|
|
1304
|
+
setCallbacks(options) {
|
|
1305
|
+
this.options = options;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* stdin の raw mode キーリスナーを開始。
|
|
1309
|
+
* Enter キーまたは p キーで pauseRequested フラグをセット。
|
|
1310
|
+
*/
|
|
1311
|
+
startListening() {
|
|
1312
|
+
if (!process.stdin.isTTY) return;
|
|
1313
|
+
const onData = (data) => {
|
|
1314
|
+
if (data[0] === 113 || data[0] === 3) {
|
|
1315
|
+
if (!this.cancelRequested) {
|
|
1316
|
+
this.cancelRequested = true;
|
|
1317
|
+
log.warn("\u23F9 \u30AD\u30E3\u30F3\u30BB\u30EB\u3092\u8981\u6C42\u3057\u307E\u3057\u305F...");
|
|
1318
|
+
this.options.onCancel?.();
|
|
1319
|
+
}
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (data[0] === 13 || data[0] === 112) {
|
|
1323
|
+
if (!this.pauseRequested) {
|
|
1324
|
+
this.pauseRequested = true;
|
|
1325
|
+
log.warn("\u23F8 \u4E00\u6642\u505C\u6B62\u3092\u8981\u6C42\u3057\u307E\u3057\u305F\u3002\u6B21\u306E\u30C4\u30FC\u30EB\u547C\u3073\u51FA\u3057\u3067\u505C\u6B62\u3057\u307E\u3059...");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
process.stdin.setRawMode(true);
|
|
1330
|
+
process.stdin.resume();
|
|
1331
|
+
process.stdin.on("data", onData);
|
|
1332
|
+
this.stdinCleanup = () => {
|
|
1333
|
+
process.stdin.off("data", onData);
|
|
1334
|
+
try {
|
|
1335
|
+
process.stdin.setRawMode(false);
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
process.stdin.pause();
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* 一時停止が要求されているか確認
|
|
1343
|
+
*/
|
|
1344
|
+
isPauseRequested() {
|
|
1345
|
+
return this.pauseRequested;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* キャンセルが要求されているか確認
|
|
1349
|
+
*/
|
|
1350
|
+
isCancelRequested() {
|
|
1351
|
+
return this.cancelRequested;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* 一時停止中にユーザーから介入指示を収集。
|
|
1355
|
+
* stdin raw mode を一時的に解除して @clack/prompts を使う。
|
|
1356
|
+
*/
|
|
1357
|
+
async collectIntervention(currentStepIndex, currentUrl) {
|
|
1358
|
+
this.suspendRawMode();
|
|
1359
|
+
this.options.onPause?.();
|
|
1360
|
+
try {
|
|
1361
|
+
log.info(
|
|
1362
|
+
`--- \u4E00\u6642\u505C\u6B62 (\u30B9\u30C6\u30C3\u30D7 #${currentStepIndex}, URL: ${currentUrl}) ---`
|
|
1363
|
+
);
|
|
1364
|
+
const action = await promptSelect(
|
|
1365
|
+
"\u64CD\u4F5C\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
1366
|
+
[
|
|
1367
|
+
{
|
|
1368
|
+
value: "guidance",
|
|
1369
|
+
label: "\u30AC\u30A4\u30C0\u30F3\u30B9\u3092\u5165\u529B\u3057\u3066\u7D9A\u884C",
|
|
1370
|
+
hint: "AI\u306E\u63A2\u7D22\u65B9\u91DD\u3092\u4FEE\u6B63"
|
|
1371
|
+
},
|
|
1372
|
+
{ value: "abort", label: "\u63A2\u7D22\u3092\u4E2D\u6B62" }
|
|
1373
|
+
]
|
|
1374
|
+
);
|
|
1375
|
+
if (action === "abort") {
|
|
1376
|
+
return { action: "abort" };
|
|
1377
|
+
}
|
|
1378
|
+
const guidanceText = await promptText("\u30AC\u30A4\u30C0\u30F3\u30B9 (AI\u3078\u306E\u6307\u793A)", {
|
|
1379
|
+
validate: (v) => !v?.trim() ? "\u30AC\u30A4\u30C0\u30F3\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044" : void 0
|
|
1380
|
+
});
|
|
1381
|
+
const record = {
|
|
1382
|
+
stepIndex: currentStepIndex,
|
|
1383
|
+
userInstruction: guidanceText.trim(),
|
|
1384
|
+
url: currentUrl,
|
|
1385
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1386
|
+
};
|
|
1387
|
+
this.interventions.push(record);
|
|
1388
|
+
log.success(`\u30AC\u30A4\u30C0\u30F3\u30B9\u3092\u8A18\u9332\u3057\u307E\u3057\u305F: "${guidanceText.trim()}"`);
|
|
1389
|
+
return { action: "guidance", guidanceText: guidanceText.trim() };
|
|
1390
|
+
} finally {
|
|
1391
|
+
this.pauseRequested = false;
|
|
1392
|
+
this.resumeRawMode();
|
|
1393
|
+
this.options.onResume?.();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* リスナーを停止しリソースを解放
|
|
1398
|
+
*/
|
|
1399
|
+
dispose() {
|
|
1400
|
+
if (this.stdinCleanup) {
|
|
1401
|
+
this.stdinCleanup();
|
|
1402
|
+
this.stdinCleanup = null;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
suspendRawMode() {
|
|
1406
|
+
if (this.stdinCleanup) {
|
|
1407
|
+
try {
|
|
1408
|
+
process.stdin.setRawMode(false);
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
process.stdin.pause();
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
resumeRawMode() {
|
|
1415
|
+
if (this.stdinCleanup) {
|
|
1416
|
+
try {
|
|
1417
|
+
process.stdin.setRawMode(true);
|
|
1418
|
+
} catch {
|
|
1419
|
+
}
|
|
1420
|
+
process.stdin.resume();
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
// src/instruction-generator/explorer.ts
|
|
1426
|
+
import { mkdir } from "fs/promises";
|
|
1427
|
+
import { join } from "path";
|
|
1428
|
+
async function explore(config) {
|
|
1429
|
+
const browser = new AgentBrowser();
|
|
1430
|
+
const recordedSteps = [];
|
|
1431
|
+
const debugLogger = createDebugLogger({
|
|
1432
|
+
filePath: config.debugLogPath,
|
|
1433
|
+
console: config.debugConsole
|
|
1434
|
+
});
|
|
1435
|
+
const interventionController = new InterventionController();
|
|
1436
|
+
if (config.screenshotDir) {
|
|
1437
|
+
await mkdir(config.screenshotDir, { recursive: true });
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
const s = spinner();
|
|
1441
|
+
s.start(`${config.url} \u3092\u958B\u3044\u3066\u3044\u307E\u3059...`);
|
|
1442
|
+
await browser.open(config.url, {
|
|
1443
|
+
headless: config.headless,
|
|
1444
|
+
stealth: config.stealth,
|
|
1445
|
+
proxy: config.proxy
|
|
1446
|
+
});
|
|
1447
|
+
await sleep(1e3);
|
|
1448
|
+
s.stop("\u30DA\u30FC\u30B8\u8AAD\u307F\u8FBC\u307F\u5B8C\u4E86");
|
|
1449
|
+
if (config.videoDir) {
|
|
1450
|
+
try {
|
|
1451
|
+
await browser.startRecording(config.videoDir);
|
|
1452
|
+
log.info(`\u9332\u753B\u958B\u59CB: ${config.videoDir}`);
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
log.warn(`\u9332\u753B\u958B\u59CB\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${e instanceof Error ? e.message : String(e)}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (config.screenshotDir) {
|
|
1458
|
+
try {
|
|
1459
|
+
const initialPath = join(config.screenshotDir, "initial.png");
|
|
1460
|
+
await browser.screenshot(initialPath);
|
|
1461
|
+
log.step(`Initial screenshot saved: ${initialPath}`);
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
interventionController.setCallbacks({
|
|
1466
|
+
onPause: () => s.stop("\u23F8 \u4E00\u6642\u505C\u6B62"),
|
|
1467
|
+
onResume: () => s.start("\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u63A2\u7D22\u4E2D... [Enter: \u4E00\u6642\u505C\u6B62 / q: \u30AD\u30E3\u30F3\u30BB\u30EB]"),
|
|
1468
|
+
onCancel: () => s.stop("\u23F9 \u30AD\u30E3\u30F3\u30BB\u30EB")
|
|
1469
|
+
});
|
|
1470
|
+
const dataStore = new InMemoryDataStore();
|
|
1471
|
+
const outputDir = config.screenshotDir ? join(config.screenshotDir, "..") : void 0;
|
|
1472
|
+
const downloadManager = outputDir ? new DownloadManager(outputDir) : void 0;
|
|
1473
|
+
const maxSteps = config.maxIterations * 3;
|
|
1474
|
+
s.start(
|
|
1475
|
+
`\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u63A2\u7D22\u4E2D (\u6700\u5927 ${maxSteps} \u30B9\u30C6\u30C3\u30D7)... [Enter: \u4E00\u6642\u505C\u6B62 / q: \u30AD\u30E3\u30F3\u30BB\u30EB]`
|
|
1476
|
+
);
|
|
1477
|
+
interventionController.startListening();
|
|
1478
|
+
const agentStart = performance.now();
|
|
1479
|
+
const result = await exploreWithAI(browser, recordedSteps, {
|
|
1480
|
+
goal: config.goal,
|
|
1481
|
+
contextMarkdown: config.contextMarkdown,
|
|
1482
|
+
secrets: config.secrets,
|
|
1483
|
+
maxIterations: config.maxIterations,
|
|
1484
|
+
stepDelay: config.stepDelay,
|
|
1485
|
+
snapshotFilter: config.snapshotFilter,
|
|
1486
|
+
screenshotDir: config.screenshotDir,
|
|
1487
|
+
debugLogger,
|
|
1488
|
+
interventionController,
|
|
1489
|
+
historyWindow: config.historyWindow,
|
|
1490
|
+
enableMultiModel: config.enableMultiModel
|
|
1491
|
+
}, dataStore, downloadManager);
|
|
1492
|
+
interventionController.dispose();
|
|
1493
|
+
s.stop(`\u63A2\u7D22\u5B8C\u4E86 (${formatDuration(performance.now() - agentStart)})`);
|
|
1494
|
+
const { goalResult } = result;
|
|
1495
|
+
log.info(`Goal achieved: ${goalResult.goalAchieved}`);
|
|
1496
|
+
log.info(`Summary: ${goalResult.summary}`);
|
|
1497
|
+
log.info(`Tokens: prompt=${result.totalTokens.promptTokens}, completion=${result.totalTokens.completionTokens}, total=${result.totalTokens.totalTokens}`);
|
|
1498
|
+
if (dataStore.listCollections().length > 0) {
|
|
1499
|
+
log.info(`Memory collections: ${dataStore.listCollections().join(", ")}`);
|
|
1500
|
+
if (outputDir) {
|
|
1501
|
+
for (const collection of dataStore.listCollections()) {
|
|
1502
|
+
const filePath = join(outputDir, `${collection}.json`);
|
|
1503
|
+
await dataStore.writeToFile(collection, filePath, "json");
|
|
1504
|
+
log.info(`Memory data exported: ${filePath}`);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (downloadManager && downloadManager.getDownloads().length > 0) {
|
|
1509
|
+
log.info(`Downloaded files: ${downloadManager.getDownloads().length}`);
|
|
1510
|
+
for (const dl of downloadManager.getDownloads()) {
|
|
1511
|
+
log.info(` - ${dl.filename} (${dl.path})`);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
await debugLogger.flush();
|
|
1515
|
+
const interventions = interventionController.interventions;
|
|
1516
|
+
const additionalGuidance = interventions.map((iv) => iv.userInstruction);
|
|
1517
|
+
return {
|
|
1518
|
+
recordedSteps,
|
|
1519
|
+
goalAchieved: goalResult.goalAchieved,
|
|
1520
|
+
additionalGuidance,
|
|
1521
|
+
interventions,
|
|
1522
|
+
cancelled: interventionController.isCancelRequested()
|
|
1523
|
+
};
|
|
1524
|
+
} catch (err) {
|
|
1525
|
+
const interventions = interventionController.interventions;
|
|
1526
|
+
return {
|
|
1527
|
+
recordedSteps,
|
|
1528
|
+
goalAchieved: false,
|
|
1529
|
+
additionalGuidance: interventions.map((iv) => iv.userInstruction),
|
|
1530
|
+
interventions,
|
|
1531
|
+
cancelled: interventionController.isCancelRequested(),
|
|
1532
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
1533
|
+
};
|
|
1534
|
+
} finally {
|
|
1535
|
+
try {
|
|
1536
|
+
interventionController.dispose();
|
|
1537
|
+
} catch {
|
|
1538
|
+
}
|
|
1539
|
+
if (browser.isRecording()) {
|
|
1540
|
+
try {
|
|
1541
|
+
const result = await browser.stopRecording();
|
|
1542
|
+
for (const p of result.paths) {
|
|
1543
|
+
log.info(`\u9332\u753B\u5B8C\u4E86: ${p}`);
|
|
1544
|
+
}
|
|
1545
|
+
} catch (e) {
|
|
1546
|
+
log.warn(`\u9332\u753B\u505C\u6B62\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${e instanceof Error ? e.message : String(e)}`);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
await browser.close();
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/instruction-generator/instruction-builder.ts
|
|
1557
|
+
import { stringify } from "yaml";
|
|
1558
|
+
|
|
1559
|
+
// src/browser/selector-builder.ts
|
|
1560
|
+
var ROLE_TAG_MAP = {
|
|
1561
|
+
textbox: { tagName: "input", inputType: "text" },
|
|
1562
|
+
button: { tagName: "button", role: "button" },
|
|
1563
|
+
link: { tagName: "a", role: "link" },
|
|
1564
|
+
checkbox: { tagName: "input", inputType: "checkbox" },
|
|
1565
|
+
radio: { tagName: "input", inputType: "radio" },
|
|
1566
|
+
combobox: { tagName: "select", role: "combobox" },
|
|
1567
|
+
searchbox: { tagName: "input", inputType: "search" },
|
|
1568
|
+
slider: { tagName: "input", inputType: "range" },
|
|
1569
|
+
spinbutton: { tagName: "input", inputType: "number" },
|
|
1570
|
+
switch: { tagName: "input", inputType: "checkbox", role: "switch" },
|
|
1571
|
+
tab: { tagName: "button", role: "tab" },
|
|
1572
|
+
menuitem: { tagName: "button", role: "menuitem" },
|
|
1573
|
+
option: { tagName: "option", role: "option" },
|
|
1574
|
+
heading: { tagName: "h2", role: "heading" },
|
|
1575
|
+
img: { tagName: "img", role: "img" },
|
|
1576
|
+
navigation: { tagName: "nav", role: "navigation" },
|
|
1577
|
+
listitem: { tagName: "li", role: "listitem" }
|
|
1578
|
+
};
|
|
1579
|
+
function buildSelector(element) {
|
|
1580
|
+
const mapping = ROLE_TAG_MAP[element.role];
|
|
1581
|
+
const tagName = mapping?.tagName ?? element.role;
|
|
1582
|
+
const selector = { tagName };
|
|
1583
|
+
if (mapping?.role) {
|
|
1584
|
+
selector.role = mapping.role;
|
|
1585
|
+
} else if (element.role !== tagName) {
|
|
1586
|
+
selector.role = element.role;
|
|
1587
|
+
}
|
|
1588
|
+
if (mapping?.inputType) {
|
|
1589
|
+
selector.inputType = mapping.inputType;
|
|
1590
|
+
}
|
|
1591
|
+
if (element.name) {
|
|
1592
|
+
selector.ariaLabel = element.name;
|
|
1593
|
+
if (element.role === "button" || element.role === "link") {
|
|
1594
|
+
selector.innerText = element.name.slice(0, 200);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
if (element.attributes.placeholder) {
|
|
1598
|
+
selector.placeholder = element.attributes.placeholder;
|
|
1599
|
+
}
|
|
1600
|
+
if (element.attributes.name) {
|
|
1601
|
+
selector.name = element.attributes.name;
|
|
1602
|
+
}
|
|
1603
|
+
return selector;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/instruction-generator/instruction-builder.ts
|
|
1607
|
+
function toActionType(action) {
|
|
1608
|
+
switch (action) {
|
|
1609
|
+
case "fill":
|
|
1610
|
+
case "type":
|
|
1611
|
+
return "input";
|
|
1612
|
+
case "check":
|
|
1613
|
+
case "uncheck":
|
|
1614
|
+
case "click":
|
|
1615
|
+
return "click";
|
|
1616
|
+
case "select":
|
|
1617
|
+
return "select";
|
|
1618
|
+
case "navigate":
|
|
1619
|
+
return "navigate";
|
|
1620
|
+
case "scroll":
|
|
1621
|
+
return "scroll";
|
|
1622
|
+
case "wait":
|
|
1623
|
+
return "wait";
|
|
1624
|
+
case "extract":
|
|
1625
|
+
return "extract";
|
|
1626
|
+
case "download":
|
|
1627
|
+
return "download";
|
|
1628
|
+
case "export":
|
|
1629
|
+
return "export";
|
|
1630
|
+
case "memory_append":
|
|
1631
|
+
case "memory_aggregate":
|
|
1632
|
+
return "memory";
|
|
1633
|
+
default:
|
|
1634
|
+
return "click";
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
function categoryToSource(category) {
|
|
1638
|
+
switch (category) {
|
|
1639
|
+
case "credential":
|
|
1640
|
+
return "prompt";
|
|
1641
|
+
case "user_data":
|
|
1642
|
+
return "prompt";
|
|
1643
|
+
case "fixed":
|
|
1644
|
+
return "fixed";
|
|
1645
|
+
case "navigation":
|
|
1646
|
+
return "fixed";
|
|
1647
|
+
default:
|
|
1648
|
+
return "prompt";
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function buildInstructionYaml(input) {
|
|
1652
|
+
const { goal, startUrl, recordedSteps, goalAchieved, stepDelay, reviewResult, humanGuidance, interventions } = input;
|
|
1653
|
+
const successfulSteps = recordedSteps.filter((s) => s.success);
|
|
1654
|
+
const reviewMap = new Map(
|
|
1655
|
+
reviewResult?.reviewedSteps.map((r) => [r.originalOrdinal, r])
|
|
1656
|
+
);
|
|
1657
|
+
const filteredSteps = reviewResult ? successfulSteps.filter((s) => reviewMap.get(s.ordinal)?.keep !== false) : successfulSteps;
|
|
1658
|
+
const variables = {};
|
|
1659
|
+
const usedVariableNames = /* @__PURE__ */ new Set();
|
|
1660
|
+
filteredSteps.forEach((recorded) => {
|
|
1661
|
+
const { action } = recorded;
|
|
1662
|
+
const actionType = toActionType(action.action);
|
|
1663
|
+
if (actionType === "input" && action.value !== void 0) {
|
|
1664
|
+
const varName = action.variableName ?? `input_${recorded.ordinal}`;
|
|
1665
|
+
if (!usedVariableNames.has(varName)) {
|
|
1666
|
+
usedVariableNames.add(varName);
|
|
1667
|
+
const source = categoryToSource(action.inputCategory);
|
|
1668
|
+
variables[varName] = {
|
|
1669
|
+
source,
|
|
1670
|
+
description: action.description,
|
|
1671
|
+
...source === "prompt" && {
|
|
1672
|
+
required: true,
|
|
1673
|
+
sensitive: action.inputCategory === "credential"
|
|
1674
|
+
},
|
|
1675
|
+
...source === "fixed" && { value: action.value }
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
const steps = filteredSteps.map((recorded, index) => {
|
|
1681
|
+
const actionType = toActionType(recorded.action.action);
|
|
1682
|
+
const review = reviewMap.get(recorded.ordinal);
|
|
1683
|
+
let selector;
|
|
1684
|
+
if (recorded.action.selector) {
|
|
1685
|
+
const ref = recorded.action.selector.replace("@", "");
|
|
1686
|
+
const element = findElementInSnapshot(recorded.snapshotBefore, ref);
|
|
1687
|
+
if (element) {
|
|
1688
|
+
selector = buildSelector(element);
|
|
1689
|
+
} else {
|
|
1690
|
+
selector = { tagName: "unknown", ariaLabel: recorded.action.description };
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
let value;
|
|
1694
|
+
if (actionType === "input" && recorded.action.value !== void 0) {
|
|
1695
|
+
const varName = recorded.action.variableName ?? `input_${recorded.ordinal}`;
|
|
1696
|
+
value = `{{${varName}}}`;
|
|
1697
|
+
}
|
|
1698
|
+
const step = {
|
|
1699
|
+
ordinal: index,
|
|
1700
|
+
description: recorded.action.description,
|
|
1701
|
+
action: {
|
|
1702
|
+
type: actionType,
|
|
1703
|
+
...selector && { selector },
|
|
1704
|
+
...actionType === "input" && value !== void 0 && { value },
|
|
1705
|
+
...actionType === "select" && recorded.action.value !== void 0 && {
|
|
1706
|
+
optionText: recorded.action.value
|
|
1707
|
+
},
|
|
1708
|
+
...actionType === "navigate" && recorded.action.value && {
|
|
1709
|
+
url: recorded.action.value
|
|
1710
|
+
},
|
|
1711
|
+
...actionType === "extract" && recorded.action.script && {
|
|
1712
|
+
script: recorded.action.script
|
|
1713
|
+
},
|
|
1714
|
+
...actionType === "download" && recorded.action.downloadPath && {
|
|
1715
|
+
downloadPath: recorded.action.downloadPath
|
|
1716
|
+
},
|
|
1717
|
+
...actionType === "export" && recorded.action.exportCollection && {
|
|
1718
|
+
exportCollection: recorded.action.exportCollection
|
|
1719
|
+
},
|
|
1720
|
+
...actionType === "export" && recorded.action.exportFormat && {
|
|
1721
|
+
exportFormat: recorded.action.exportFormat
|
|
1722
|
+
},
|
|
1723
|
+
...actionType === "export" && recorded.action.exportPath && {
|
|
1724
|
+
exportPath: recorded.action.exportPath
|
|
1725
|
+
}
|
|
1726
|
+
},
|
|
1727
|
+
url: recorded.url,
|
|
1728
|
+
riskLevel: review?.riskLevel ?? "low",
|
|
1729
|
+
requiresConfirmation: review?.requiresConfirmation ?? false
|
|
1730
|
+
};
|
|
1731
|
+
const captures = buildCaptures(recorded.action.suggestedCaptures);
|
|
1732
|
+
if (captures.length > 0) {
|
|
1733
|
+
step.captures = captures;
|
|
1734
|
+
}
|
|
1735
|
+
if (recorded.action.action === "memory_append" && recorded.action.memoryCollection) {
|
|
1736
|
+
step.memoryOperations = [{
|
|
1737
|
+
type: "append",
|
|
1738
|
+
collection: recorded.action.memoryCollection,
|
|
1739
|
+
source: recorded.action.variableName ?? "extractedData"
|
|
1740
|
+
}];
|
|
1741
|
+
} else if (recorded.action.action === "memory_aggregate" && recorded.action.aggregation) {
|
|
1742
|
+
const agg = recorded.action.aggregation;
|
|
1743
|
+
step.memoryOperations = [{
|
|
1744
|
+
type: "aggregate",
|
|
1745
|
+
collection: agg.collection,
|
|
1746
|
+
field: agg.field,
|
|
1747
|
+
operation: agg.operation,
|
|
1748
|
+
outputVariable: agg.outputVariable
|
|
1749
|
+
}];
|
|
1750
|
+
}
|
|
1751
|
+
return step;
|
|
1752
|
+
});
|
|
1753
|
+
const baseUrl = new URL(startUrl).origin;
|
|
1754
|
+
const instruction = {
|
|
1755
|
+
title: goal,
|
|
1756
|
+
settings: {
|
|
1757
|
+
baseUrl,
|
|
1758
|
+
defaultTimeout: 1e4,
|
|
1759
|
+
pauseBetweenSteps: stepDelay,
|
|
1760
|
+
stopOnError: true
|
|
1761
|
+
},
|
|
1762
|
+
metadata: {
|
|
1763
|
+
startUrl,
|
|
1764
|
+
goal,
|
|
1765
|
+
goalAchieved,
|
|
1766
|
+
totalSteps: filteredSteps.length,
|
|
1767
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
if (Object.keys(variables).length > 0) {
|
|
1771
|
+
instruction.variables = variables;
|
|
1772
|
+
}
|
|
1773
|
+
instruction.steps = steps;
|
|
1774
|
+
const allNotes = [];
|
|
1775
|
+
if (humanGuidance && humanGuidance.length > 0) {
|
|
1776
|
+
allNotes.push(
|
|
1777
|
+
...humanGuidance.map((g, i) => `[\u65B9\u91DD\u4FEE\u6B63 ${i + 1}] ${g}`)
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
if (interventions && interventions.length > 0) {
|
|
1781
|
+
allNotes.push(
|
|
1782
|
+
...interventions.map(
|
|
1783
|
+
(iv) => `[\u63A2\u7D22\u4E2D\u4ECB\u5165 \u30B9\u30C6\u30C3\u30D7#${iv.stepIndex}] ${iv.userInstruction}`
|
|
1784
|
+
)
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
if (allNotes.length > 0) {
|
|
1788
|
+
instruction.notes = allNotes.join("\n");
|
|
1789
|
+
}
|
|
1790
|
+
ParsedInstructionSchema.parse(instruction);
|
|
1791
|
+
return stringify(instruction, { lineWidth: 120 });
|
|
1792
|
+
}
|
|
1793
|
+
function buildCaptures(suggested) {
|
|
1794
|
+
if (!suggested || suggested.length === 0) return [];
|
|
1795
|
+
return suggested.map((c) => ({
|
|
1796
|
+
name: c.name,
|
|
1797
|
+
strategy: c.strategy,
|
|
1798
|
+
required: false,
|
|
1799
|
+
...c.description && { description: c.description },
|
|
1800
|
+
...c.pattern && { pattern: c.pattern },
|
|
1801
|
+
...c.group !== void 0 && c.group !== 1 && { group: c.group },
|
|
1802
|
+
...c.prompt && { prompt: c.prompt },
|
|
1803
|
+
...c.expression && { expression: c.expression }
|
|
1804
|
+
}));
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/instruction-generator/prompts.ts
|
|
1808
|
+
import { z as z3 } from "zod";
|
|
1809
|
+
var reviewResponseSchema = z3.object({
|
|
1810
|
+
reviewedSteps: z3.array(z3.object({
|
|
1811
|
+
originalOrdinal: z3.number(),
|
|
1812
|
+
keep: z3.boolean(),
|
|
1813
|
+
removalReason: z3.string().optional(),
|
|
1814
|
+
riskLevel: z3.enum(["low", "medium", "high"]),
|
|
1815
|
+
requiresConfirmation: z3.boolean(),
|
|
1816
|
+
confirmationReason: z3.string().optional()
|
|
1817
|
+
})),
|
|
1818
|
+
summary: z3.string()
|
|
1819
|
+
});
|
|
1820
|
+
function createReviewPrompt(goal, recordedSteps, goalAchieved, interventions) {
|
|
1821
|
+
const system = getReviewSystemPrompt();
|
|
1822
|
+
const userPrompt = createReviewUserPrompt(goal, recordedSteps, goalAchieved, interventions);
|
|
1823
|
+
return { system, userPrompt };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/instruction-generator/review-editor.ts
|
|
1827
|
+
function buildSummaryText(reviewResult, recordedSteps, humanGuidance) {
|
|
1828
|
+
const lines = [];
|
|
1829
|
+
const keptSteps = reviewResult.reviewedSteps.filter((s) => s.keep);
|
|
1830
|
+
if (keptSteps.length > 0) {
|
|
1831
|
+
lines.push(t("review.keptSteps"));
|
|
1832
|
+
for (const step of keptSteps) {
|
|
1833
|
+
const recorded = recordedSteps.find(
|
|
1834
|
+
(r) => r.ordinal === step.originalOrdinal
|
|
1835
|
+
);
|
|
1836
|
+
const desc = recorded?.action.description ?? t("common.unknown");
|
|
1837
|
+
lines.push(
|
|
1838
|
+
` #${step.originalOrdinal}: ${desc} [risk: ${step.riskLevel}]`
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const confirmSteps = keptSteps.filter((s) => s.requiresConfirmation);
|
|
1843
|
+
lines.push("");
|
|
1844
|
+
lines.push(t("review.confirmationRequired"));
|
|
1845
|
+
if (confirmSteps.length > 0) {
|
|
1846
|
+
for (const step of confirmSteps) {
|
|
1847
|
+
const recorded = recordedSteps.find(
|
|
1848
|
+
(r) => r.ordinal === step.originalOrdinal
|
|
1849
|
+
);
|
|
1850
|
+
const desc = recorded?.action.description ?? t("common.unknown");
|
|
1851
|
+
const reason = step.confirmationReason ? ` \u2014 ${step.confirmationReason}` : "";
|
|
1852
|
+
lines.push(
|
|
1853
|
+
` #${step.originalOrdinal}: ${desc} [risk: ${step.riskLevel}]${reason}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
} else {
|
|
1857
|
+
lines.push(" " + t("common.none"));
|
|
1858
|
+
}
|
|
1859
|
+
const removedSteps = reviewResult.reviewedSteps.filter((s) => !s.keep);
|
|
1860
|
+
if (removedSteps.length > 0) {
|
|
1861
|
+
lines.push("");
|
|
1862
|
+
lines.push(t("review.removedSteps"));
|
|
1863
|
+
for (const step of removedSteps) {
|
|
1864
|
+
const recorded = recordedSteps.find(
|
|
1865
|
+
(r) => r.ordinal === step.originalOrdinal
|
|
1866
|
+
);
|
|
1867
|
+
const desc = recorded?.action.description ?? t("common.unknown");
|
|
1868
|
+
const reason = step.removalReason ?? "";
|
|
1869
|
+
lines.push(` #${step.originalOrdinal}: ${desc} (\u7406\u7531: ${reason})`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const captureSteps = recordedSteps.filter(
|
|
1873
|
+
(r) => r.action.suggestedCaptures && r.action.suggestedCaptures.length > 0
|
|
1874
|
+
);
|
|
1875
|
+
if (captureSteps.length > 0) {
|
|
1876
|
+
lines.push("");
|
|
1877
|
+
lines.push(t("review.capturedValues"));
|
|
1878
|
+
for (const r of captureSteps) {
|
|
1879
|
+
for (const c of r.action.suggestedCaptures) {
|
|
1880
|
+
lines.push(
|
|
1881
|
+
` Step #${r.ordinal} \u2192 ${c.name} (${c.strategy}${c.pattern ? `: ${c.pattern}` : ""})`
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
lines.push("");
|
|
1887
|
+
lines.push(t("review.notes"));
|
|
1888
|
+
if (humanGuidance.length > 0) {
|
|
1889
|
+
for (const [i, n] of humanGuidance.entries()) {
|
|
1890
|
+
lines.push(` ${tf("review.guidancePrefix", { index: i + 1 })} ${n}`);
|
|
1891
|
+
}
|
|
1892
|
+
} else {
|
|
1893
|
+
lines.push(" " + t("common.none"));
|
|
1894
|
+
}
|
|
1895
|
+
lines.push("");
|
|
1896
|
+
lines.push(tf("review.summaryLabel", { summary: reviewResult.summary }));
|
|
1897
|
+
return lines.join("\n");
|
|
1898
|
+
}
|
|
1899
|
+
function cloneReviewResult(r) {
|
|
1900
|
+
return {
|
|
1901
|
+
reviewedSteps: r.reviewedSteps.map((s) => ({ ...s })),
|
|
1902
|
+
summary: r.summary
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
function deriveSuggestedNotes(reviewResult, recordedSteps) {
|
|
1906
|
+
const suggestions = [];
|
|
1907
|
+
const highRiskSteps = reviewResult.reviewedSteps.filter(
|
|
1908
|
+
(s) => s.keep && s.riskLevel === "high"
|
|
1909
|
+
);
|
|
1910
|
+
if (highRiskSteps.length > 0) {
|
|
1911
|
+
const ordinals = highRiskSteps.map((s) => `#${s.originalOrdinal}`).join(", ");
|
|
1912
|
+
suggestions.push(tf("review.highRiskNote", { ordinals }));
|
|
1913
|
+
}
|
|
1914
|
+
const credentialSteps = recordedSteps.filter(
|
|
1915
|
+
(r) => r.action.inputCategory === "credential"
|
|
1916
|
+
);
|
|
1917
|
+
if (credentialSteps.length > 0) {
|
|
1918
|
+
suggestions.push(t("review.credentialNote"));
|
|
1919
|
+
}
|
|
1920
|
+
const removedCount = reviewResult.reviewedSteps.filter(
|
|
1921
|
+
(s) => !s.keep
|
|
1922
|
+
).length;
|
|
1923
|
+
if (removedCount >= 3) {
|
|
1924
|
+
suggestions.push(tf("review.removedStepsNote", { count: removedCount }));
|
|
1925
|
+
}
|
|
1926
|
+
if (reviewResult.summary) {
|
|
1927
|
+
suggestions.push(`AI\u30EC\u30D3\u30E5\u30FC\u8981\u7D04: ${reviewResult.summary}`);
|
|
1928
|
+
}
|
|
1929
|
+
return suggestions;
|
|
1930
|
+
}
|
|
1931
|
+
async function handleToggleConfirmation(reviewResult, recordedSteps) {
|
|
1932
|
+
const keptSteps = reviewResult.reviewedSteps.filter((s) => s.keep);
|
|
1933
|
+
if (keptSteps.length === 0) {
|
|
1934
|
+
log.warn("\u4FDD\u6301\u3055\u308C\u308B\u30B9\u30C6\u30C3\u30D7\u304C\u3042\u308A\u307E\u305B\u3093\u3002");
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
const initialValues = keptSteps.filter((s) => s.requiresConfirmation).map((s) => s.originalOrdinal);
|
|
1938
|
+
const selected = await promptMultiselect(
|
|
1939
|
+
t("review.selectConfirmSteps"),
|
|
1940
|
+
keptSteps.map((step) => {
|
|
1941
|
+
const recorded = recordedSteps.find(
|
|
1942
|
+
(r) => r.ordinal === step.originalOrdinal
|
|
1943
|
+
);
|
|
1944
|
+
const desc = recorded?.action.description ?? t("common.unknown");
|
|
1945
|
+
return {
|
|
1946
|
+
value: step.originalOrdinal,
|
|
1947
|
+
label: `#${step.originalOrdinal}: ${desc}`,
|
|
1948
|
+
hint: `risk: ${step.riskLevel}`
|
|
1949
|
+
};
|
|
1950
|
+
}),
|
|
1951
|
+
{ initialValues }
|
|
1952
|
+
);
|
|
1953
|
+
for (const step of keptSteps) {
|
|
1954
|
+
step.requiresConfirmation = selected.includes(step.originalOrdinal);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
async function handleEditNotes(humanGuidance, reviewResult, recordedSteps) {
|
|
1958
|
+
const suggestions = deriveSuggestedNotes(reviewResult, recordedSteps);
|
|
1959
|
+
const options = [
|
|
1960
|
+
{ value: "add_custom", label: t("review.addCustomNote") }
|
|
1961
|
+
];
|
|
1962
|
+
if (suggestions.length > 0) {
|
|
1963
|
+
options.push({
|
|
1964
|
+
value: "add_suggested",
|
|
1965
|
+
label: t("review.addSuggestedNote"),
|
|
1966
|
+
hint: `${suggestions.length} ${t("common.items")}`
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
if (humanGuidance.length > 0) {
|
|
1970
|
+
options.push({
|
|
1971
|
+
value: "delete",
|
|
1972
|
+
label: t("review.deleteNote"),
|
|
1973
|
+
hint: `${humanGuidance.length} ${t("common.items")}`
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
const action = await promptSelect(
|
|
1977
|
+
t("review.selectNotesAction"),
|
|
1978
|
+
options
|
|
1979
|
+
);
|
|
1980
|
+
switch (action) {
|
|
1981
|
+
case "add_custom": {
|
|
1982
|
+
const content = await promptText(t("review.noteContent"), {
|
|
1983
|
+
validate: (v) => !v?.trim() ? t("review.noteContentRequired") : void 0
|
|
1984
|
+
});
|
|
1985
|
+
humanGuidance.push(content.trim());
|
|
1986
|
+
log.success(tf("review.notesAdded", { count: 1 }));
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
case "add_suggested": {
|
|
1990
|
+
const selected = await promptMultiselect(
|
|
1991
|
+
"\u8FFD\u52A0\u3059\u308B\u30CE\u30FC\u30C8\u3092\u9078\u629E",
|
|
1992
|
+
suggestions.map((s, i) => ({
|
|
1993
|
+
value: i,
|
|
1994
|
+
label: s
|
|
1995
|
+
}))
|
|
1996
|
+
);
|
|
1997
|
+
for (const idx of selected) {
|
|
1998
|
+
humanGuidance.push(suggestions[idx]);
|
|
1999
|
+
}
|
|
2000
|
+
log.success(tf("review.notesAdded", { count: selected.length }));
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
case "delete": {
|
|
2004
|
+
const toDelete = await promptMultiselect(
|
|
2005
|
+
"\u524A\u9664\u3059\u308B\u30CE\u30FC\u30C8\u3092\u9078\u629E",
|
|
2006
|
+
humanGuidance.map((g, i) => ({
|
|
2007
|
+
value: i,
|
|
2008
|
+
label: `${tf("review.guidancePrefix", { index: i + 1 })} ${g}`
|
|
2009
|
+
}))
|
|
2010
|
+
);
|
|
2011
|
+
for (const idx of [...toDelete].sort((a, b) => b - a)) {
|
|
2012
|
+
humanGuidance.splice(idx, 1);
|
|
2013
|
+
}
|
|
2014
|
+
log.success(tf("review.notesDeleted", { count: toDelete.length }));
|
|
2015
|
+
break;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
async function handleEditCaptures(recordedSteps) {
|
|
2020
|
+
const action = await promptSelect(
|
|
2021
|
+
t("review.selectCaptureAction"),
|
|
2022
|
+
[
|
|
2023
|
+
{ value: "accept_all", label: t("review.acceptAllCaptures") },
|
|
2024
|
+
{ value: "reject", label: t("review.rejectCaptures") },
|
|
2025
|
+
{ value: "add", label: t("review.addCapture") }
|
|
2026
|
+
]
|
|
2027
|
+
);
|
|
2028
|
+
switch (action) {
|
|
2029
|
+
case "accept_all": {
|
|
2030
|
+
log.success(t("review.allCapturesAccepted"));
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
case "reject": {
|
|
2034
|
+
const captureSteps = recordedSteps.filter(
|
|
2035
|
+
(r) => r.action.suggestedCaptures && r.action.suggestedCaptures.length > 0
|
|
2036
|
+
);
|
|
2037
|
+
if (captureSteps.length === 0) {
|
|
2038
|
+
log.warn(t("review.noCapturesAvailable"));
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
const allCaptures = [];
|
|
2042
|
+
for (const r of captureSteps) {
|
|
2043
|
+
for (const c of r.action.suggestedCaptures) {
|
|
2044
|
+
allCaptures.push({
|
|
2045
|
+
value: `${r.ordinal}:${c.name}`,
|
|
2046
|
+
label: `Step #${r.ordinal} \u2192 ${c.name} (${c.strategy})`
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
const toReject = await promptMultiselect(
|
|
2051
|
+
t("review.selectCapturesToReject"),
|
|
2052
|
+
allCaptures
|
|
2053
|
+
);
|
|
2054
|
+
for (const key of toReject) {
|
|
2055
|
+
const [ordStr, name] = key.split(":");
|
|
2056
|
+
const step = recordedSteps.find((r) => r.ordinal === Number(ordStr));
|
|
2057
|
+
if (step?.action.suggestedCaptures) {
|
|
2058
|
+
step.action.suggestedCaptures = step.action.suggestedCaptures.filter(
|
|
2059
|
+
(c) => c.name !== name
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
log.success(tf("review.capturesRejected", { count: toReject.length }));
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
2066
|
+
case "add": {
|
|
2067
|
+
const ordinalStr = await promptText(t("review.captureStepNumber"), {
|
|
2068
|
+
validate: (v) => {
|
|
2069
|
+
if (!v?.trim()) return t("review.captureStepRequired");
|
|
2070
|
+
if (Number.isNaN(Number(v))) return t("review.captureStepNumeric");
|
|
2071
|
+
return void 0;
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
const ordinal = Number(ordinalStr);
|
|
2075
|
+
const step = recordedSteps.find((r) => r.ordinal === ordinal);
|
|
2076
|
+
if (!step) {
|
|
2077
|
+
log.warn(tf("review.stepNotFound", { ordinal }));
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
const name = await promptText(t("review.captureVarName"), {
|
|
2081
|
+
validate: (v) => !v?.trim() ? t("review.captureVarRequired") : void 0
|
|
2082
|
+
});
|
|
2083
|
+
const strategy = await promptSelect(
|
|
2084
|
+
t("review.captureStrategy"),
|
|
2085
|
+
[
|
|
2086
|
+
{ value: "snapshot", label: t("review.strategySnapshot") },
|
|
2087
|
+
{ value: "url", label: t("review.strategyUrl") },
|
|
2088
|
+
{ value: "ai", label: t("review.strategyAi") },
|
|
2089
|
+
{ value: "expression", label: t("review.strategyExpression") }
|
|
2090
|
+
]
|
|
2091
|
+
);
|
|
2092
|
+
const newCapture = { name, strategy };
|
|
2093
|
+
if (strategy === "snapshot" || strategy === "url") {
|
|
2094
|
+
newCapture.pattern = await promptText(t("review.regexPattern"));
|
|
2095
|
+
} else if (strategy === "ai") {
|
|
2096
|
+
newCapture.prompt = await promptText(t("review.aiExtractionPrompt"));
|
|
2097
|
+
} else if (strategy === "expression") {
|
|
2098
|
+
newCapture.expression = await promptText(t("review.templateExpression"));
|
|
2099
|
+
}
|
|
2100
|
+
if (!step.action.suggestedCaptures) {
|
|
2101
|
+
step.action.suggestedCaptures = [];
|
|
2102
|
+
}
|
|
2103
|
+
step.action.suggestedCaptures.push(newCapture);
|
|
2104
|
+
log.success(tf("review.captureAdded", { ordinal, name }));
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
async function promptReviewEdits(reviewResult, recordedSteps, humanGuidance) {
|
|
2110
|
+
const edited = cloneReviewResult(reviewResult);
|
|
2111
|
+
const editedGuidance = [...humanGuidance];
|
|
2112
|
+
while (true) {
|
|
2113
|
+
const summaryText = buildSummaryText(edited, recordedSteps, editedGuidance);
|
|
2114
|
+
note(summaryText, t("review.reviewResult"));
|
|
2115
|
+
const hasCaptureSteps = recordedSteps.some(
|
|
2116
|
+
(r) => r.action.suggestedCaptures && r.action.suggestedCaptures.length > 0
|
|
2117
|
+
);
|
|
2118
|
+
const menuOptions = [
|
|
2119
|
+
{ value: "confirm", label: t("review.confirm"), hint: t("review.confirmHint") },
|
|
2120
|
+
{
|
|
2121
|
+
value: "toggle_confirmation",
|
|
2122
|
+
label: t("review.toggleConfirmation"),
|
|
2123
|
+
hint: "multiselect"
|
|
2124
|
+
},
|
|
2125
|
+
...hasCaptureSteps ? [
|
|
2126
|
+
{
|
|
2127
|
+
value: "edit_captures",
|
|
2128
|
+
label: t("review.editCaptures"),
|
|
2129
|
+
hint: t("review.editCapturesHint")
|
|
2130
|
+
}
|
|
2131
|
+
] : [],
|
|
2132
|
+
{ value: "edit_notes", label: t("review.editNotes") }
|
|
2133
|
+
];
|
|
2134
|
+
const action = await promptSelect(
|
|
2135
|
+
t("review.selectAction"),
|
|
2136
|
+
menuOptions
|
|
2137
|
+
);
|
|
2138
|
+
if (action === "confirm") {
|
|
2139
|
+
break;
|
|
2140
|
+
}
|
|
2141
|
+
switch (action) {
|
|
2142
|
+
case "toggle_confirmation":
|
|
2143
|
+
await handleToggleConfirmation(edited, recordedSteps);
|
|
2144
|
+
break;
|
|
2145
|
+
case "edit_captures":
|
|
2146
|
+
await handleEditCaptures(recordedSteps);
|
|
2147
|
+
break;
|
|
2148
|
+
case "edit_notes":
|
|
2149
|
+
await handleEditNotes(editedGuidance, edited, recordedSteps);
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return {
|
|
2154
|
+
reviewResult: edited,
|
|
2155
|
+
humanGuidance: editedGuidance
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// src/instruction-generator/report-generator.ts
|
|
2160
|
+
function formatOutcome(outcome) {
|
|
2161
|
+
switch (outcome) {
|
|
2162
|
+
case "cancelled":
|
|
2163
|
+
return "Cancelled by user";
|
|
2164
|
+
case "goal_achieved":
|
|
2165
|
+
return "Goal achieved";
|
|
2166
|
+
case "goal_not_achieved":
|
|
2167
|
+
return "Goal not achieved";
|
|
2168
|
+
case "error":
|
|
2169
|
+
return "Error";
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function toReportableSteps(steps) {
|
|
2173
|
+
return steps.map((s) => ({
|
|
2174
|
+
ordinal: s.ordinal + 1,
|
|
2175
|
+
description: `${s.action.action}: ${s.action.description}`,
|
|
2176
|
+
status: s.success ? "success" : "failed",
|
|
2177
|
+
durationMs: s.durationMs ?? 0,
|
|
2178
|
+
error: s.error,
|
|
2179
|
+
failureCategory: s.failureCategory
|
|
2180
|
+
}));
|
|
2181
|
+
}
|
|
2182
|
+
async function generateExplorationReport(input) {
|
|
2183
|
+
const lines = [];
|
|
2184
|
+
lines.push("# Exploration Report");
|
|
2185
|
+
lines.push("");
|
|
2186
|
+
lines.push("## Session Info");
|
|
2187
|
+
lines.push(`- **Date**: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
2188
|
+
lines.push(`- **Goal**: ${input.goal}`);
|
|
2189
|
+
lines.push(`- **Start URL**: ${input.startUrl}`);
|
|
2190
|
+
lines.push(`- **Outcome**: ${formatOutcome(input.outcome)}`);
|
|
2191
|
+
lines.push("");
|
|
2192
|
+
if (input.cliOptions && input.cliOptions.length > 0) {
|
|
2193
|
+
lines.push("## CLI Options");
|
|
2194
|
+
lines.push(...renderOptionsMarkdown(input.cliOptions));
|
|
2195
|
+
lines.push("");
|
|
2196
|
+
}
|
|
2197
|
+
lines.push("## User Feedback");
|
|
2198
|
+
if (input.userFeedback) {
|
|
2199
|
+
lines.push(input.userFeedback);
|
|
2200
|
+
} else {
|
|
2201
|
+
lines.push("(No feedback provided)");
|
|
2202
|
+
}
|
|
2203
|
+
lines.push("");
|
|
2204
|
+
if (input.errorMessage) {
|
|
2205
|
+
lines.push("## Error Details");
|
|
2206
|
+
lines.push("```");
|
|
2207
|
+
lines.push(input.errorMessage);
|
|
2208
|
+
lines.push("```");
|
|
2209
|
+
lines.push("");
|
|
2210
|
+
}
|
|
2211
|
+
const successCount = input.recordedSteps.filter((s) => s.success).length;
|
|
2212
|
+
const failedCount = input.recordedSteps.filter((s) => !s.success).length;
|
|
2213
|
+
lines.push("## Execution Summary");
|
|
2214
|
+
lines.push(`- **Total Steps**: ${input.recordedSteps.length}`);
|
|
2215
|
+
lines.push(`- **Successful**: ${successCount}`);
|
|
2216
|
+
lines.push(`- **Failed**: ${failedCount}`);
|
|
2217
|
+
lines.push(`- **Interventions**: ${input.interventions.length}`);
|
|
2218
|
+
if (input.totalDurationMs !== void 0) {
|
|
2219
|
+
lines.push(`- **Duration**: ${input.totalDurationMs}ms`);
|
|
2220
|
+
}
|
|
2221
|
+
lines.push("");
|
|
2222
|
+
if (input.recordedSteps.length > 0) {
|
|
2223
|
+
lines.push("## Recorded Steps");
|
|
2224
|
+
for (const step of input.recordedSteps) {
|
|
2225
|
+
const tag = step.success ? "DONE" : "FAILED";
|
|
2226
|
+
const errorInfo = !step.success && step.error ? ` \u2014 Error: ${step.error}` : "";
|
|
2227
|
+
const category = step.failureCategory ? ` [${step.failureCategory}]` : "";
|
|
2228
|
+
const duration = step.durationMs !== void 0 ? ` (${step.durationMs}ms)` : "";
|
|
2229
|
+
lines.push(
|
|
2230
|
+
`${step.ordinal + 1}. [${tag}] ${step.action.action}: ${step.action.description} (URL: ${step.url})${duration}${errorInfo}${category}`
|
|
2231
|
+
);
|
|
2232
|
+
}
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
}
|
|
2235
|
+
if (input.interventions.length > 0) {
|
|
2236
|
+
lines.push("## Interventions");
|
|
2237
|
+
for (const iv of input.interventions) {
|
|
2238
|
+
lines.push(
|
|
2239
|
+
`- **Step ${iv.stepIndex}** (${iv.timestamp}): "${iv.userInstruction}" (URL: ${iv.url})`
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
lines.push("");
|
|
2243
|
+
}
|
|
2244
|
+
const debugEntries = input.debugLogPath ? await readDebugLog(input.debugLogPath) : [];
|
|
2245
|
+
const reportableSteps = toReportableSteps(input.recordedSteps);
|
|
2246
|
+
const hasInsights = input.aiMetrics && input.aiMetrics.totalCalls > 0 || reportableSteps.some((s) => s.status === "failed") || reportableSteps.some((s) => s.durationMs > 3e3) || debugEntries.length > 0;
|
|
2247
|
+
if (hasInsights) {
|
|
2248
|
+
lines.push("## System Insights");
|
|
2249
|
+
lines.push("");
|
|
2250
|
+
lines.push("> agentic-browser \u306E\u6A5F\u80FD\u6539\u5584\u30FB\u30D0\u30B0\u4FEE\u6B63\u306E\u305F\u3081\u306E\u5206\u6790\u60C5\u5831");
|
|
2251
|
+
lines.push("");
|
|
2252
|
+
lines.push(...formatRuntimeEnvironment());
|
|
2253
|
+
if (input.aiMetrics && input.aiMetrics.totalCalls > 0) {
|
|
2254
|
+
lines.push(...formatAiMetricsSection(input.aiMetrics));
|
|
2255
|
+
}
|
|
2256
|
+
const failedSteps = reportableSteps.filter((s) => s.status === "failed");
|
|
2257
|
+
lines.push(...formatFailureCategoryDistribution(failedSteps));
|
|
2258
|
+
lines.push(...formatPerformanceBottlenecks(reportableSteps));
|
|
2259
|
+
}
|
|
2260
|
+
lines.push(...formatDebugLogSections(debugEntries));
|
|
2261
|
+
return lines.join("\n");
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// src/instruction-generator/index.ts
|
|
2265
|
+
async function main() {
|
|
2266
|
+
const totalStart = performance.now();
|
|
2267
|
+
const config = await parseArgs();
|
|
2268
|
+
await initModel(config.aiModelConfig);
|
|
2269
|
+
const cliOptions = serializeGeneratorOptions(config);
|
|
2270
|
+
intro("instruction-generator");
|
|
2271
|
+
log.info(`URL: ${config.url}`);
|
|
2272
|
+
log.info(`Goal: ${config.goal}`);
|
|
2273
|
+
log.info(`Output: ${config.output}`);
|
|
2274
|
+
log.info(`Max iterations: ${config.maxIterations}`);
|
|
2275
|
+
log.info(`Step delay: ${config.stepDelay}ms`);
|
|
2276
|
+
log.info(`Headless: ${config.headless}`);
|
|
2277
|
+
if (config.contextMarkdown) {
|
|
2278
|
+
log.info(`Context: loaded (${config.contextMarkdown.split("\n").length} lines)`);
|
|
2279
|
+
log.info("--- Context Markdown ---");
|
|
2280
|
+
log.info(config.contextMarkdown);
|
|
2281
|
+
log.info("--- End Context ---");
|
|
2282
|
+
}
|
|
2283
|
+
if (config.secrets && Object.keys(config.secrets.values).length > 0) {
|
|
2284
|
+
log.info(`Secrets: ${Object.keys(config.secrets.values).length} keys loaded`);
|
|
2285
|
+
}
|
|
2286
|
+
const exploreStart = performance.now();
|
|
2287
|
+
const { recordedSteps, goalAchieved, additionalGuidance, interventions, cancelled, error: exploreError } = await explore(config);
|
|
2288
|
+
log.info(`Steps recorded: ${recordedSteps.length}`);
|
|
2289
|
+
log.info(`Goal achieved: ${goalAchieved}`);
|
|
2290
|
+
if (cancelled) {
|
|
2291
|
+
log.warn("Exploration was cancelled by user.");
|
|
2292
|
+
}
|
|
2293
|
+
if (interventions.length > 0) {
|
|
2294
|
+
log.info(`User interventions: ${interventions.length}`);
|
|
2295
|
+
}
|
|
2296
|
+
log.info(`Exploration completed (${formatDuration(performance.now() - exploreStart)})`);
|
|
2297
|
+
if (exploreError) {
|
|
2298
|
+
log.error(`Exploration failed: ${exploreError.message}`);
|
|
2299
|
+
const report = await generateExplorationReport({
|
|
2300
|
+
goal: config.goal,
|
|
2301
|
+
startUrl: config.url,
|
|
2302
|
+
outcome: "error",
|
|
2303
|
+
userFeedback: "",
|
|
2304
|
+
recordedSteps,
|
|
2305
|
+
interventions,
|
|
2306
|
+
debugLogPath: config.debugLogPath,
|
|
2307
|
+
errorMessage: exploreError.stack ?? exploreError.message,
|
|
2308
|
+
cliOptions,
|
|
2309
|
+
aiMetrics: globalMetrics.getSummary(),
|
|
2310
|
+
totalDurationMs: Math.round(performance.now() - totalStart)
|
|
2311
|
+
});
|
|
2312
|
+
const reportPath = config.output.replace(/\.\w+$/, "-report.md");
|
|
2313
|
+
await mkdir2(dirname(reportPath), { recursive: true });
|
|
2314
|
+
await writeFile(reportPath, report, "utf-8");
|
|
2315
|
+
log.info(`Error report written to: ${reportPath}`);
|
|
2316
|
+
outro(`Exploration failed. Report: ${reportPath}`);
|
|
2317
|
+
process.exit(1);
|
|
2318
|
+
}
|
|
2319
|
+
if (cancelled || !goalAchieved) {
|
|
2320
|
+
const userFeedback = await promptText(
|
|
2321
|
+
t("generator.feedbackPrompt"),
|
|
2322
|
+
{ validate: () => void 0 }
|
|
2323
|
+
);
|
|
2324
|
+
const report = await generateExplorationReport({
|
|
2325
|
+
goal: config.goal,
|
|
2326
|
+
startUrl: config.url,
|
|
2327
|
+
outcome: cancelled ? "cancelled" : "goal_not_achieved",
|
|
2328
|
+
userFeedback: userFeedback?.trim() ?? "",
|
|
2329
|
+
recordedSteps,
|
|
2330
|
+
interventions,
|
|
2331
|
+
debugLogPath: config.debugLogPath,
|
|
2332
|
+
cliOptions,
|
|
2333
|
+
aiMetrics: globalMetrics.getSummary(),
|
|
2334
|
+
totalDurationMs: Math.round(performance.now() - totalStart)
|
|
2335
|
+
});
|
|
2336
|
+
const reportPath = config.output.replace(/\.\w+$/, "-report.md");
|
|
2337
|
+
await mkdir2(dirname(reportPath), { recursive: true });
|
|
2338
|
+
await writeFile(reportPath, report, "utf-8");
|
|
2339
|
+
log.info(`Report written to: ${reportPath}`);
|
|
2340
|
+
if (cancelled) {
|
|
2341
|
+
outro(t("generator.cancelled"));
|
|
2342
|
+
process.exit(0);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
if (recordedSteps.length === 0) {
|
|
2346
|
+
log.error("No steps recorded. Cannot generate instruction.");
|
|
2347
|
+
process.exit(1);
|
|
2348
|
+
}
|
|
2349
|
+
try {
|
|
2350
|
+
const debugLogger = createDebugLogger({
|
|
2351
|
+
filePath: config.debugLogPath,
|
|
2352
|
+
console: config.debugConsole
|
|
2353
|
+
});
|
|
2354
|
+
let reviewResult;
|
|
2355
|
+
const reviewSpinner = spinner();
|
|
2356
|
+
try {
|
|
2357
|
+
const reviewStart = performance.now();
|
|
2358
|
+
reviewSpinner.start(t("generator.aiReviewing"));
|
|
2359
|
+
const { system: reviewSystem, userPrompt: reviewUserPrompt } = createReviewPrompt(config.goal, recordedSteps, goalAchieved, interventions);
|
|
2360
|
+
debugLogger.log({
|
|
2361
|
+
phase: "generator",
|
|
2362
|
+
event: "ai_review",
|
|
2363
|
+
data: { prompt: `${reviewSystem}
|
|
2364
|
+
---
|
|
2365
|
+
${reviewUserPrompt}`.slice(0, 500) }
|
|
2366
|
+
});
|
|
2367
|
+
const { object } = await trackedGenerateObject("review", {
|
|
2368
|
+
model: getModel("review"),
|
|
2369
|
+
messages: [
|
|
2370
|
+
{
|
|
2371
|
+
role: "system",
|
|
2372
|
+
content: reviewSystem,
|
|
2373
|
+
providerOptions: {
|
|
2374
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
2375
|
+
}
|
|
2376
|
+
},
|
|
2377
|
+
{ role: "user", content: reviewUserPrompt }
|
|
2378
|
+
],
|
|
2379
|
+
schema: reviewResponseSchema,
|
|
2380
|
+
temperature: 0
|
|
2381
|
+
});
|
|
2382
|
+
reviewResult = object;
|
|
2383
|
+
debugLogger.log({
|
|
2384
|
+
phase: "generator",
|
|
2385
|
+
event: "ai_review",
|
|
2386
|
+
data: {
|
|
2387
|
+
parsedResult: reviewResult
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
const kept = reviewResult.reviewedSteps.filter((s) => s.keep).length;
|
|
2391
|
+
const removed = reviewResult.reviewedSteps.filter((s) => !s.keep).length;
|
|
2392
|
+
reviewSpinner.stop(`AI review done: ${kept} kept, ${removed} removed (${formatDuration(performance.now() - reviewStart)})`);
|
|
2393
|
+
log.info(`Summary: ${reviewResult.summary}`);
|
|
2394
|
+
} catch (error) {
|
|
2395
|
+
reviewSpinner.stop(tf("generator.aiReviewFailed", { error: error instanceof Error ? error.message : String(error) }));
|
|
2396
|
+
reviewResult = void 0;
|
|
2397
|
+
}
|
|
2398
|
+
let finalGuidance = additionalGuidance;
|
|
2399
|
+
if (reviewResult) {
|
|
2400
|
+
const edited = await promptReviewEdits(
|
|
2401
|
+
reviewResult,
|
|
2402
|
+
recordedSteps,
|
|
2403
|
+
additionalGuidance
|
|
2404
|
+
);
|
|
2405
|
+
reviewResult = edited.reviewResult;
|
|
2406
|
+
finalGuidance = edited.humanGuidance;
|
|
2407
|
+
}
|
|
2408
|
+
if (reviewResult) {
|
|
2409
|
+
const keptWithConfirm = reviewResult.reviewedSteps.filter((s) => s.keep && s.requiresConfirmation).map((s) => `#${s.originalOrdinal}`);
|
|
2410
|
+
log.info(`Confirmation required for steps: ${keptWithConfirm.join(", ") || t("common.none")}`);
|
|
2411
|
+
}
|
|
2412
|
+
const yamlStart = performance.now();
|
|
2413
|
+
const yaml = buildInstructionYaml({
|
|
2414
|
+
goal: config.goal,
|
|
2415
|
+
startUrl: config.url,
|
|
2416
|
+
recordedSteps,
|
|
2417
|
+
goalAchieved,
|
|
2418
|
+
stepDelay: config.stepDelay,
|
|
2419
|
+
reviewResult,
|
|
2420
|
+
humanGuidance: finalGuidance,
|
|
2421
|
+
interventions
|
|
2422
|
+
});
|
|
2423
|
+
await mkdir2(dirname(config.output), { recursive: true });
|
|
2424
|
+
await writeFile(config.output, yaml, "utf-8");
|
|
2425
|
+
log.info(`YAML generation + write (${formatDuration(performance.now() - yamlStart)})`);
|
|
2426
|
+
await debugLogger.flush();
|
|
2427
|
+
log.success(`Instruction written to: ${config.output}`);
|
|
2428
|
+
if (!goalAchieved) {
|
|
2429
|
+
log.warn("Goal was NOT fully achieved. Instruction may be incomplete.");
|
|
2430
|
+
}
|
|
2431
|
+
const metrics = globalMetrics.getSummary();
|
|
2432
|
+
if (metrics.totalCalls > 0) {
|
|
2433
|
+
const purposeSummary = Object.entries(metrics.byPurpose).map(([p, b]) => `${p}(${b.calls})`).join(" ");
|
|
2434
|
+
log.info(
|
|
2435
|
+
`AI Metrics: ${metrics.totalCalls} calls, ${metrics.totalInputTokens.toLocaleString()} in / ${metrics.totalOutputTokens.toLocaleString()} out tokens, cache ${(metrics.cacheHitRate * 100).toFixed(1)}%, $${metrics.estimatedCostUsd.toFixed(4)}, ${(metrics.totalDurationMs / 1e3).toFixed(1)}s \u2014 ${purposeSummary}`
|
|
2436
|
+
);
|
|
2437
|
+
if (metrics.byModel && Object.keys(metrics.byModel).length > 1) {
|
|
2438
|
+
const modelSummary = Object.entries(metrics.byModel).map(([id, b]) => `${id}($${b.estimatedCostUsd.toFixed(4)})`).join(" ");
|
|
2439
|
+
log.info(`Models: ${modelSummary}`);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
outro(`Total: ${formatDuration(performance.now() - totalStart)}`);
|
|
2443
|
+
} catch (postExploreError) {
|
|
2444
|
+
const errorMessage = postExploreError instanceof Error ? postExploreError.stack ?? postExploreError.message : String(postExploreError);
|
|
2445
|
+
log.error(`Post-exploration phase failed: ${errorMessage}`);
|
|
2446
|
+
const report = await generateExplorationReport({
|
|
2447
|
+
goal: config.goal,
|
|
2448
|
+
startUrl: config.url,
|
|
2449
|
+
outcome: "error",
|
|
2450
|
+
userFeedback: "",
|
|
2451
|
+
recordedSteps,
|
|
2452
|
+
interventions,
|
|
2453
|
+
debugLogPath: config.debugLogPath,
|
|
2454
|
+
errorMessage,
|
|
2455
|
+
cliOptions,
|
|
2456
|
+
aiMetrics: globalMetrics.getSummary(),
|
|
2457
|
+
totalDurationMs: Math.round(performance.now() - totalStart)
|
|
2458
|
+
});
|
|
2459
|
+
const reportPath = config.output.replace(/\.\w+$/, "-report.md");
|
|
2460
|
+
await mkdir2(dirname(reportPath), { recursive: true });
|
|
2461
|
+
await writeFile(reportPath, report, "utf-8");
|
|
2462
|
+
log.info(`Error report written to: ${reportPath}`);
|
|
2463
|
+
process.exit(1);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
main().catch((error) => {
|
|
2467
|
+
cancel(`Fatal error: ${error instanceof Error ? error.message : String(error)}`);
|
|
2468
|
+
process.exit(1);
|
|
2469
|
+
});
|
|
2470
|
+
//# sourceMappingURL=instruction-generator-5RPRYTVR.js.map
|