@refrainai/cli 0.4.1
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-FM6GWCID.js +37 -0
- package/dist/ai-model-FM6GWCID.js.map +1 -0
- package/dist/chunk-2BVDAJZT.js +236 -0
- package/dist/chunk-2BVDAJZT.js.map +1 -0
- package/dist/chunk-2H7UOFLK.js +11 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-7UCVPKD4.js +902 -0
- package/dist/chunk-7UCVPKD4.js.map +1 -0
- package/dist/chunk-AG3CFMYU.js +36 -0
- package/dist/chunk-AG3CFMYU.js.map +1 -0
- package/dist/chunk-CLYJHKPY.js +1131 -0
- package/dist/chunk-CLYJHKPY.js.map +1 -0
- package/dist/chunk-D5SI2PHK.js +74 -0
- package/dist/chunk-D5SI2PHK.js.map +1 -0
- package/dist/chunk-DJVUITRB.js +9084 -0
- package/dist/chunk-DJVUITRB.js.map +1 -0
- package/dist/chunk-H47NWH7N.js +4427 -0
- package/dist/chunk-H47NWH7N.js.map +1 -0
- package/dist/chunk-HQDXLWAY.js +109 -0
- package/dist/chunk-HQDXLWAY.js.map +1 -0
- package/dist/chunk-IGFCYKHC.js +1974 -0
- package/dist/chunk-IGFCYKHC.js.map +1 -0
- package/dist/chunk-RT664YIO.js +245 -0
- package/dist/chunk-RT664YIO.js.map +1 -0
- package/dist/chunk-RYIJPYM3.js +164 -0
- package/dist/chunk-RYIJPYM3.js.map +1 -0
- package/dist/chunk-TDSM3UXI.js +40 -0
- package/dist/chunk-TDSM3UXI.js.map +1 -0
- package/dist/chunk-UGPXCQY3.js +778 -0
- package/dist/chunk-UGPXCQY3.js.map +1 -0
- package/dist/chunk-VPK2MQAZ.js +589 -0
- package/dist/chunk-VPK2MQAZ.js.map +1 -0
- package/dist/chunk-WEYR56ZN.js +953 -0
- package/dist/chunk-WEYR56ZN.js.map +1 -0
- package/dist/chunk-XMFCXPYU.js +275 -0
- package/dist/chunk-XMFCXPYU.js.map +1 -0
- package/dist/chunk-Z33FCOTZ.js +251 -0
- package/dist/chunk-Z33FCOTZ.js.map +1 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose-MTSIJY5D.js +547 -0
- package/dist/compose-MTSIJY5D.js.map +1 -0
- package/dist/config-ZSUNCFXR.js +9 -0
- package/dist/config-ZSUNCFXR.js.map +1 -0
- package/dist/fix-runbook-ZSBOTLC2.js +294 -0
- package/dist/fix-runbook-ZSBOTLC2.js.map +1 -0
- package/dist/google-sheets-DRWIVEVC.js +482 -0
- package/dist/google-sheets-DRWIVEVC.js.map +1 -0
- package/dist/registry-LZLYTNDJ.js +17 -0
- package/dist/registry-LZLYTNDJ.js.map +1 -0
- package/dist/runbook-data-helpers-KRR2SH76.js +16 -0
- package/dist/runbook-data-helpers-KRR2SH76.js.map +1 -0
- package/dist/runbook-executor-K7T6RJWJ.js +1480 -0
- package/dist/runbook-executor-K7T6RJWJ.js.map +1 -0
- package/dist/runbook-generator-MPXJBQ5N.js +800 -0
- package/dist/runbook-generator-MPXJBQ5N.js.map +1 -0
- package/dist/runbook-schema-3T6TP3JJ.js +35 -0
- package/dist/runbook-schema-3T6TP3JJ.js.map +1 -0
- package/dist/runbook-store-G5GUOWRR.js +11 -0
- package/dist/runbook-store-G5GUOWRR.js.map +1 -0
- package/dist/schema-5G6UQSPT.js +91 -0
- package/dist/schema-5G6UQSPT.js.map +1 -0
- package/dist/server-AG3LXQBI.js +8778 -0
- package/dist/server-AG3LXQBI.js.map +1 -0
- package/dist/tenant-ai-config-QPFEJUVJ.js +14 -0
- package/dist/tenant-ai-config-QPFEJUVJ.js.map +1 -0
- package/dist/yaml-patcher-VGUS2JGH.js +15 -0
- package/dist/yaml-patcher-VGUS2JGH.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,4427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AgentBrowser,
|
|
4
|
+
NoopLogger,
|
|
5
|
+
NoopSpinner,
|
|
6
|
+
buildFallbackMessages,
|
|
7
|
+
buildSelectorMessages,
|
|
8
|
+
buildSuggestionUserPrompt,
|
|
9
|
+
buildVisionMessages,
|
|
10
|
+
classifyFailure,
|
|
11
|
+
countElements,
|
|
12
|
+
createDebugLogger,
|
|
13
|
+
findElementInSnapshot,
|
|
14
|
+
formatDuration,
|
|
15
|
+
getRecoveryHint,
|
|
16
|
+
getSuggestionSystemPrompt,
|
|
17
|
+
parseAllElements,
|
|
18
|
+
parseAndAppendToMemory,
|
|
19
|
+
sanitizeBrowserError,
|
|
20
|
+
sleep
|
|
21
|
+
} from "./chunk-DJVUITRB.js";
|
|
22
|
+
import {
|
|
23
|
+
filterSnapshot
|
|
24
|
+
} from "./chunk-XMFCXPYU.js";
|
|
25
|
+
import {
|
|
26
|
+
resolveTemplate
|
|
27
|
+
} from "./chunk-D5SI2PHK.js";
|
|
28
|
+
import {
|
|
29
|
+
formatRiskHint,
|
|
30
|
+
getLocale,
|
|
31
|
+
log,
|
|
32
|
+
note,
|
|
33
|
+
promptConfirm,
|
|
34
|
+
promptSelect,
|
|
35
|
+
promptText,
|
|
36
|
+
t,
|
|
37
|
+
tf
|
|
38
|
+
} from "./chunk-7UCVPKD4.js";
|
|
39
|
+
import {
|
|
40
|
+
getActiveMetricsCollector,
|
|
41
|
+
getModel,
|
|
42
|
+
trackedGenerateObject,
|
|
43
|
+
withAIRetry
|
|
44
|
+
} from "./chunk-UGPXCQY3.js";
|
|
45
|
+
|
|
46
|
+
// src/runbook-executor/types.ts
|
|
47
|
+
var SELF_HEAL_DEFAULTS = {
|
|
48
|
+
enableSelectorCache: true,
|
|
49
|
+
enableAgentFallback: true,
|
|
50
|
+
enableVisionFallback: true,
|
|
51
|
+
maxRetries: 5,
|
|
52
|
+
retryWarningThreshold: 3
|
|
53
|
+
};
|
|
54
|
+
function applyExecutorDefaults(config) {
|
|
55
|
+
if (config.selfHeal) {
|
|
56
|
+
config.enableSelectorCache ??= SELF_HEAL_DEFAULTS.enableSelectorCache;
|
|
57
|
+
config.enableAgentFallback ??= SELF_HEAL_DEFAULTS.enableAgentFallback;
|
|
58
|
+
config.enableVisionFallback ??= SELF_HEAL_DEFAULTS.enableVisionFallback;
|
|
59
|
+
config.maxRetries ??= SELF_HEAL_DEFAULTS.maxRetries;
|
|
60
|
+
config.retryWarningThreshold ??= SELF_HEAL_DEFAULTS.retryWarningThreshold;
|
|
61
|
+
config.executionStrategy ??= buildSelfHealStrategy(config.maxRetries);
|
|
62
|
+
config.skipConfirmation ??= true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function buildSelfHealStrategy(maxRetries) {
|
|
66
|
+
const changeTimeoutCount = Math.max(maxRetries - 1, 0);
|
|
67
|
+
const changeTimeouts = [];
|
|
68
|
+
for (let i = 0; i < changeTimeoutCount; i++) {
|
|
69
|
+
changeTimeouts.push(8e3 + i * 2e3);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
maxRetries,
|
|
73
|
+
changeTimeouts,
|
|
74
|
+
finalRetryStabilizeMs: 5e3,
|
|
75
|
+
domStabilityMs: 8e3
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/plan/errors.ts
|
|
80
|
+
var PlanLimitError = class extends Error {
|
|
81
|
+
constructor(message, limitType = "hard") {
|
|
82
|
+
super(message);
|
|
83
|
+
this.name = "PlanLimitError";
|
|
84
|
+
this.limitType = limitType;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var PlanFeatureError = class extends Error {
|
|
88
|
+
constructor(message) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "PlanFeatureError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/plan/key-kind.ts
|
|
95
|
+
function detectKeyKind(key) {
|
|
96
|
+
if (key.startsWith("rfn_")) return "opaque";
|
|
97
|
+
if (key.startsWith("rfnd-")) return "debug";
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/plan/debug-token.ts
|
|
102
|
+
import {
|
|
103
|
+
createHash,
|
|
104
|
+
createCipheriv,
|
|
105
|
+
createDecipheriv,
|
|
106
|
+
randomBytes,
|
|
107
|
+
sign,
|
|
108
|
+
verify,
|
|
109
|
+
createPublicKey,
|
|
110
|
+
createPrivateKey
|
|
111
|
+
} from "crypto";
|
|
112
|
+
var DEBUG_PREFIX = "rfnd-";
|
|
113
|
+
var IV_SIZE = 12;
|
|
114
|
+
var AUTH_TAG_SIZE = 16;
|
|
115
|
+
var SIGNATURE_SIZE = 64;
|
|
116
|
+
var EMBEDDED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
117
|
+
MCowBQYDK2VwAyEADummyDevPublicKeyForDevelopmentOnly00000000000=
|
|
118
|
+
-----END PUBLIC KEY-----`;
|
|
119
|
+
function deriveAesKey(publicKeyPem) {
|
|
120
|
+
const publicKey = createPublicKey(publicKeyPem);
|
|
121
|
+
const rawKey = publicKey.export({ type: "spki", format: "der" });
|
|
122
|
+
return createHash("sha256").update(rawKey).digest();
|
|
123
|
+
}
|
|
124
|
+
function generateDebugToken(tenantId, privateKeyPem, publicKeyPem) {
|
|
125
|
+
if (!tenantId) {
|
|
126
|
+
throw new Error("tenantId is required");
|
|
127
|
+
}
|
|
128
|
+
const payload = {
|
|
129
|
+
tid: tenantId,
|
|
130
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
131
|
+
};
|
|
132
|
+
const payloadBytes = Buffer.from(JSON.stringify(payload), "utf-8");
|
|
133
|
+
const privateKey = createPrivateKey(privateKeyPem);
|
|
134
|
+
const signature = sign(null, payloadBytes, privateKey);
|
|
135
|
+
if (signature.length !== SIGNATURE_SIZE) {
|
|
136
|
+
throw new Error(`Unexpected Ed25519 signature size: ${signature.length}`);
|
|
137
|
+
}
|
|
138
|
+
const plaintext = Buffer.concat([payloadBytes, signature]);
|
|
139
|
+
const aesKey = deriveAesKey(publicKeyPem);
|
|
140
|
+
const iv = randomBytes(IV_SIZE);
|
|
141
|
+
const cipher = createCipheriv("aes-256-gcm", aesKey, iv);
|
|
142
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
143
|
+
const authTag = cipher.getAuthTag();
|
|
144
|
+
const combined = Buffer.concat([iv, encrypted, authTag]);
|
|
145
|
+
return `${DEBUG_PREFIX}${combined.toString("base64url")}`;
|
|
146
|
+
}
|
|
147
|
+
function verifyDebugToken(token, publicKeyPemOverride) {
|
|
148
|
+
if (!token.startsWith(DEBUG_PREFIX)) return void 0;
|
|
149
|
+
const publicKeyPem = publicKeyPemOverride ?? getDebugPublicKey();
|
|
150
|
+
try {
|
|
151
|
+
const combined = Buffer.from(token.slice(DEBUG_PREFIX.length), "base64url");
|
|
152
|
+
if (combined.length < IV_SIZE + 1 + AUTH_TAG_SIZE) return void 0;
|
|
153
|
+
const iv = combined.subarray(0, IV_SIZE);
|
|
154
|
+
const authTag = combined.subarray(combined.length - AUTH_TAG_SIZE);
|
|
155
|
+
const encrypted = combined.subarray(IV_SIZE, combined.length - AUTH_TAG_SIZE);
|
|
156
|
+
const aesKey = deriveAesKey(publicKeyPem);
|
|
157
|
+
const decipher = createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
158
|
+
decipher.setAuthTag(authTag);
|
|
159
|
+
const plaintext = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
160
|
+
if (plaintext.length <= SIGNATURE_SIZE) return void 0;
|
|
161
|
+
const payloadBytes = plaintext.subarray(0, plaintext.length - SIGNATURE_SIZE);
|
|
162
|
+
const signature = plaintext.subarray(plaintext.length - SIGNATURE_SIZE);
|
|
163
|
+
const publicKey = createPublicKey(publicKeyPem);
|
|
164
|
+
const valid = verify(null, payloadBytes, publicKey, signature);
|
|
165
|
+
if (!valid) return void 0;
|
|
166
|
+
const payload = JSON.parse(payloadBytes.toString("utf-8"));
|
|
167
|
+
if (typeof payload.tid !== "string" || !payload.tid) return void 0;
|
|
168
|
+
if (typeof payload.iat !== "number") return void 0;
|
|
169
|
+
return {
|
|
170
|
+
tenantId: payload.tid,
|
|
171
|
+
issuedAt: new Date(payload.iat * 1e3)
|
|
172
|
+
};
|
|
173
|
+
} catch {
|
|
174
|
+
return void 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function getDebugSigningKey() {
|
|
178
|
+
return process.env.REFRAIN_DEBUG_SIGNING_KEY;
|
|
179
|
+
}
|
|
180
|
+
function getDebugPublicKey() {
|
|
181
|
+
return process.env.REFRAIN_DEBUG_PUBLIC_KEY ?? EMBEDDED_PUBLIC_KEY_PEM;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/plan/key-cache.ts
|
|
185
|
+
import { createHash as createHash2 } from "crypto";
|
|
186
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
187
|
+
import { join } from "path";
|
|
188
|
+
import { homedir } from "os";
|
|
189
|
+
var CONFIG_DIR = join(homedir(), ".config", "refrain");
|
|
190
|
+
var CACHE_FILE = join(CONFIG_DIR, "key-cache.json");
|
|
191
|
+
function hashKey(apiKey) {
|
|
192
|
+
return createHash2("sha256").update(apiKey).digest("hex");
|
|
193
|
+
}
|
|
194
|
+
async function loadCacheFile() {
|
|
195
|
+
try {
|
|
196
|
+
const data = await readFile(CACHE_FILE, "utf-8");
|
|
197
|
+
const parsed = JSON.parse(data);
|
|
198
|
+
if (parsed.version === 1 && parsed.entries) return parsed;
|
|
199
|
+
return { version: 1, entries: {} };
|
|
200
|
+
} catch {
|
|
201
|
+
return { version: 1, entries: {} };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function saveCacheFile(cache) {
|
|
205
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
206
|
+
await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
207
|
+
}
|
|
208
|
+
async function getCachedKey(apiKey, allowExpired = false) {
|
|
209
|
+
const cache = await loadCacheFile();
|
|
210
|
+
const hash = hashKey(apiKey);
|
|
211
|
+
const entry = cache.entries[hash];
|
|
212
|
+
if (!entry) return void 0;
|
|
213
|
+
const now = /* @__PURE__ */ new Date();
|
|
214
|
+
const expires = new Date(entry.expiresAt);
|
|
215
|
+
if (!allowExpired && expires <= now) {
|
|
216
|
+
delete cache.entries[hash];
|
|
217
|
+
await saveCacheFile(cache);
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
return entry;
|
|
221
|
+
}
|
|
222
|
+
async function setCachedKey(apiKey, data) {
|
|
223
|
+
const cache = await loadCacheFile();
|
|
224
|
+
const hash = hashKey(apiKey);
|
|
225
|
+
cache.entries[hash] = data;
|
|
226
|
+
await saveCacheFile(cache);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/plan/key-validator.ts
|
|
230
|
+
var DEFAULT_SERVER_URL = "https://api.therefrain.ai";
|
|
231
|
+
async function validateOpaqueKey(apiKey, serverUrl, forceRefresh = false) {
|
|
232
|
+
if (!forceRefresh) {
|
|
233
|
+
const cached = await getCachedKey(apiKey);
|
|
234
|
+
if (cached) {
|
|
235
|
+
return planFromCachedData(cached);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const baseUrl = serverUrl ?? process.env.REFRAIN_API_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
239
|
+
try {
|
|
240
|
+
const response = await fetch(`${baseUrl}/api/public/validate-key`, {
|
|
241
|
+
headers: {
|
|
242
|
+
Authorization: `Bearer ${apiKey}`
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
if (response.ok) {
|
|
246
|
+
const data = await response.json();
|
|
247
|
+
if (data.valid && data.tier && data.tenantId) {
|
|
248
|
+
const basePlan = TIER_TO_PLAN[data.tier] ?? COMMUNITY_PLAN;
|
|
249
|
+
const plan = {
|
|
250
|
+
...basePlan,
|
|
251
|
+
tenantId: data.tenantId,
|
|
252
|
+
// サーバーが limits/features を返した場合はそれを使用
|
|
253
|
+
...data.limits ? { limits: data.limits } : {},
|
|
254
|
+
...data.features ? { features: data.features } : {}
|
|
255
|
+
};
|
|
256
|
+
const cacheData = {
|
|
257
|
+
tier: data.tier,
|
|
258
|
+
tenantId: data.tenantId,
|
|
259
|
+
limits: plan.limits,
|
|
260
|
+
features: plan.features,
|
|
261
|
+
expiresAt: data.expiresAt ?? new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString()
|
|
262
|
+
};
|
|
263
|
+
await setCachedKey(apiKey, cacheData);
|
|
264
|
+
return plan;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return COMMUNITY_PLAN;
|
|
268
|
+
} catch {
|
|
269
|
+
return offlineFallback(apiKey);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function offlineFallback(apiKey) {
|
|
273
|
+
const expired = await getCachedKey(apiKey, true);
|
|
274
|
+
if (expired) {
|
|
275
|
+
console.warn(
|
|
276
|
+
"[refrain] Warning: Using expired key cache (offline mode). Connect to the API server to refresh."
|
|
277
|
+
);
|
|
278
|
+
return planFromCachedData(expired);
|
|
279
|
+
}
|
|
280
|
+
console.warn(
|
|
281
|
+
"[refrain] Warning: Cannot validate API key (offline, no cache). Falling back to Community plan."
|
|
282
|
+
);
|
|
283
|
+
return COMMUNITY_PLAN;
|
|
284
|
+
}
|
|
285
|
+
function planFromCachedData(data) {
|
|
286
|
+
const basePlan = TIER_TO_PLAN[data.tier] ?? COMMUNITY_PLAN;
|
|
287
|
+
return {
|
|
288
|
+
...basePlan,
|
|
289
|
+
tenantId: data.tenantId,
|
|
290
|
+
limits: data.limits,
|
|
291
|
+
features: data.features
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/plan/index.ts
|
|
296
|
+
var COMMUNITY_PLAN = {
|
|
297
|
+
tier: "community",
|
|
298
|
+
limits: {
|
|
299
|
+
maxStepsPerRunbook: 100,
|
|
300
|
+
maxBatchRows: 20,
|
|
301
|
+
maxConcurrentJobs: Number.POSITIVE_INFINITY,
|
|
302
|
+
maxMonthlyJobs: Number.POSITIVE_INFINITY,
|
|
303
|
+
maxMonthlySteps: 300,
|
|
304
|
+
maxJobDurationMs: 10 * 60 * 1e3,
|
|
305
|
+
// 10 minutes
|
|
306
|
+
maxStepDurationMs: Number.POSITIVE_INFINITY,
|
|
307
|
+
maxVideoRecordings: 0,
|
|
308
|
+
maxScreenshots: 10,
|
|
309
|
+
maxRunbookVersions: 1
|
|
310
|
+
},
|
|
311
|
+
features: {
|
|
312
|
+
selectorCache: false,
|
|
313
|
+
agentFallback: true,
|
|
314
|
+
visionFallback: false,
|
|
315
|
+
selfHealMode: false,
|
|
316
|
+
hitl: false,
|
|
317
|
+
modelOverrides: false,
|
|
318
|
+
remoteExecution: false,
|
|
319
|
+
stealthMode: false,
|
|
320
|
+
screenshotSave: true,
|
|
321
|
+
videoRecording: false,
|
|
322
|
+
scheduling: false,
|
|
323
|
+
skills: false,
|
|
324
|
+
samlSso: false,
|
|
325
|
+
auditLog: false,
|
|
326
|
+
runbookApprovalWorkflow: false,
|
|
327
|
+
runbookRollback: false
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var PRO_PLAN = {
|
|
331
|
+
tier: "pro",
|
|
332
|
+
limits: {
|
|
333
|
+
maxStepsPerRunbook: 500,
|
|
334
|
+
maxBatchRows: 100,
|
|
335
|
+
maxConcurrentJobs: Number.POSITIVE_INFINITY,
|
|
336
|
+
maxMonthlyJobs: Number.POSITIVE_INFINITY,
|
|
337
|
+
maxMonthlySteps: 1500,
|
|
338
|
+
maxJobDurationMs: 30 * 60 * 1e3,
|
|
339
|
+
// 30 minutes
|
|
340
|
+
maxStepDurationMs: Number.POSITIVE_INFINITY,
|
|
341
|
+
maxVideoRecordings: 0,
|
|
342
|
+
maxScreenshots: 0,
|
|
343
|
+
maxRunbookVersions: 1
|
|
344
|
+
},
|
|
345
|
+
features: {
|
|
346
|
+
selectorCache: true,
|
|
347
|
+
agentFallback: true,
|
|
348
|
+
visionFallback: true,
|
|
349
|
+
selfHealMode: false,
|
|
350
|
+
hitl: false,
|
|
351
|
+
modelOverrides: false,
|
|
352
|
+
remoteExecution: false,
|
|
353
|
+
stealthMode: true,
|
|
354
|
+
screenshotSave: false,
|
|
355
|
+
videoRecording: false,
|
|
356
|
+
scheduling: false,
|
|
357
|
+
skills: true,
|
|
358
|
+
samlSso: false,
|
|
359
|
+
auditLog: false,
|
|
360
|
+
runbookApprovalWorkflow: false,
|
|
361
|
+
runbookRollback: false
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
var TEAM_PLAN = {
|
|
365
|
+
tier: "team",
|
|
366
|
+
limits: {
|
|
367
|
+
maxStepsPerRunbook: Number.POSITIVE_INFINITY,
|
|
368
|
+
maxBatchRows: 500,
|
|
369
|
+
maxConcurrentJobs: 10,
|
|
370
|
+
maxMonthlyJobs: 2e3,
|
|
371
|
+
maxMonthlySteps: 8e3,
|
|
372
|
+
maxJobDurationMs: 2 * 60 * 60 * 1e3,
|
|
373
|
+
// 2 hours
|
|
374
|
+
maxStepDurationMs: 5 * 60 * 1e3,
|
|
375
|
+
// 5 minutes
|
|
376
|
+
maxVideoRecordings: 0,
|
|
377
|
+
maxScreenshots: 100,
|
|
378
|
+
maxRunbookVersions: 1
|
|
379
|
+
},
|
|
380
|
+
features: {
|
|
381
|
+
selectorCache: true,
|
|
382
|
+
agentFallback: true,
|
|
383
|
+
visionFallback: true,
|
|
384
|
+
selfHealMode: true,
|
|
385
|
+
hitl: false,
|
|
386
|
+
modelOverrides: false,
|
|
387
|
+
remoteExecution: true,
|
|
388
|
+
stealthMode: true,
|
|
389
|
+
screenshotSave: true,
|
|
390
|
+
videoRecording: false,
|
|
391
|
+
scheduling: true,
|
|
392
|
+
skills: true,
|
|
393
|
+
samlSso: false,
|
|
394
|
+
auditLog: false,
|
|
395
|
+
runbookApprovalWorkflow: false,
|
|
396
|
+
runbookRollback: false
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
var BUSINESS_PLAN = {
|
|
400
|
+
tier: "business",
|
|
401
|
+
limits: {
|
|
402
|
+
maxStepsPerRunbook: Number.POSITIVE_INFINITY,
|
|
403
|
+
maxBatchRows: 1e3,
|
|
404
|
+
maxConcurrentJobs: 50,
|
|
405
|
+
maxMonthlyJobs: 5e3,
|
|
406
|
+
maxMonthlySteps: 2e4,
|
|
407
|
+
maxJobDurationMs: 4 * 60 * 60 * 1e3,
|
|
408
|
+
// 4 hours
|
|
409
|
+
maxStepDurationMs: 5 * 60 * 1e3,
|
|
410
|
+
// 5 minutes
|
|
411
|
+
maxVideoRecordings: 10,
|
|
412
|
+
maxScreenshots: 500,
|
|
413
|
+
maxRunbookVersions: 10
|
|
414
|
+
},
|
|
415
|
+
features: {
|
|
416
|
+
selectorCache: true,
|
|
417
|
+
agentFallback: true,
|
|
418
|
+
visionFallback: true,
|
|
419
|
+
selfHealMode: true,
|
|
420
|
+
hitl: true,
|
|
421
|
+
modelOverrides: true,
|
|
422
|
+
remoteExecution: true,
|
|
423
|
+
stealthMode: true,
|
|
424
|
+
screenshotSave: true,
|
|
425
|
+
videoRecording: true,
|
|
426
|
+
scheduling: true,
|
|
427
|
+
skills: true,
|
|
428
|
+
samlSso: false,
|
|
429
|
+
auditLog: false,
|
|
430
|
+
runbookApprovalWorkflow: true,
|
|
431
|
+
runbookRollback: true
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
var ENTERPRISE_PLAN = {
|
|
435
|
+
tier: "enterprise",
|
|
436
|
+
limits: {
|
|
437
|
+
maxStepsPerRunbook: Number.POSITIVE_INFINITY,
|
|
438
|
+
maxBatchRows: Number.POSITIVE_INFINITY,
|
|
439
|
+
maxConcurrentJobs: Number.POSITIVE_INFINITY,
|
|
440
|
+
maxMonthlyJobs: Number.POSITIVE_INFINITY,
|
|
441
|
+
maxMonthlySteps: Number.POSITIVE_INFINITY,
|
|
442
|
+
maxJobDurationMs: 24 * 60 * 60 * 1e3,
|
|
443
|
+
// 24 hours
|
|
444
|
+
maxStepDurationMs: 10 * 60 * 1e3,
|
|
445
|
+
// 10 minutes
|
|
446
|
+
maxVideoRecordings: Number.POSITIVE_INFINITY,
|
|
447
|
+
maxScreenshots: Number.POSITIVE_INFINITY,
|
|
448
|
+
maxRunbookVersions: Number.POSITIVE_INFINITY
|
|
449
|
+
},
|
|
450
|
+
features: {
|
|
451
|
+
selectorCache: true,
|
|
452
|
+
agentFallback: true,
|
|
453
|
+
visionFallback: true,
|
|
454
|
+
selfHealMode: true,
|
|
455
|
+
hitl: true,
|
|
456
|
+
modelOverrides: true,
|
|
457
|
+
remoteExecution: true,
|
|
458
|
+
stealthMode: true,
|
|
459
|
+
screenshotSave: true,
|
|
460
|
+
videoRecording: true,
|
|
461
|
+
scheduling: true,
|
|
462
|
+
skills: true,
|
|
463
|
+
samlSso: true,
|
|
464
|
+
auditLog: true,
|
|
465
|
+
runbookApprovalWorkflow: true,
|
|
466
|
+
runbookRollback: true
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var PLAN_PRICING = {
|
|
470
|
+
community: 0,
|
|
471
|
+
pro: 55,
|
|
472
|
+
team: 269,
|
|
473
|
+
business: 499,
|
|
474
|
+
enterprise: null
|
|
475
|
+
};
|
|
476
|
+
var TIER_TO_PLAN = {
|
|
477
|
+
community: COMMUNITY_PLAN,
|
|
478
|
+
pro: PRO_PLAN,
|
|
479
|
+
team: TEAM_PLAN,
|
|
480
|
+
business: BUSINESS_PLAN,
|
|
481
|
+
enterprise: ENTERPRISE_PLAN
|
|
482
|
+
};
|
|
483
|
+
function resolvePlanFromTier(tier, tenantId) {
|
|
484
|
+
const basePlan = TIER_TO_PLAN[tier] ?? COMMUNITY_PLAN;
|
|
485
|
+
return { ...basePlan, tenantId };
|
|
486
|
+
}
|
|
487
|
+
async function resolvePlan(apiKey, options) {
|
|
488
|
+
if (!apiKey) return COMMUNITY_PLAN;
|
|
489
|
+
const kind = detectKeyKind(apiKey);
|
|
490
|
+
if (!kind) return COMMUNITY_PLAN;
|
|
491
|
+
if (kind === "debug") {
|
|
492
|
+
const verified = verifyDebugToken(apiKey);
|
|
493
|
+
if (!verified) return COMMUNITY_PLAN;
|
|
494
|
+
return { ...ENTERPRISE_PLAN, tenantId: verified.tenantId };
|
|
495
|
+
}
|
|
496
|
+
return validateOpaqueKey(apiKey, options?.serverUrl, options?.forceRefresh);
|
|
497
|
+
}
|
|
498
|
+
function getApiKey(cliValue) {
|
|
499
|
+
return cliValue ?? process.env.REFRAIN_API_KEY;
|
|
500
|
+
}
|
|
501
|
+
function enforceFeatureGates(config, plan) {
|
|
502
|
+
config.enableSelectorCache ??= plan.features.selectorCache;
|
|
503
|
+
config.enableAgentFallback ??= plan.features.agentFallback;
|
|
504
|
+
config.enableVisionFallback ??= plan.features.visionFallback;
|
|
505
|
+
if (!plan.features.selectorCache) config.enableSelectorCache = false;
|
|
506
|
+
if (!plan.features.agentFallback) config.enableAgentFallback = false;
|
|
507
|
+
if (!plan.features.visionFallback) config.enableVisionFallback = false;
|
|
508
|
+
if (!plan.features.selfHealMode && config.selfHeal) {
|
|
509
|
+
throw new PlanFeatureError(
|
|
510
|
+
"Self-healing mode requires a Team plan. Upgrade: https://therefrain.ai/pricing"
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
if (!plan.features.hitl) {
|
|
514
|
+
if (config.approvalMode && config.approvalMode !== "web") {
|
|
515
|
+
throw new PlanFeatureError(
|
|
516
|
+
`${config.approvalMode} approval requires a Business plan. Upgrade: https://therefrain.ai/pricing`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
if (config.notifyMode) {
|
|
520
|
+
throw new PlanFeatureError(
|
|
521
|
+
`${config.notifyMode} notification requires a Business plan. Upgrade: https://therefrain.ai/pricing`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (!plan.features.modelOverrides && config.aiModelConfig?.modelOverrides) {
|
|
526
|
+
config.aiModelConfig.modelOverrides = void 0;
|
|
527
|
+
}
|
|
528
|
+
if (!plan.features.screenshotSave && config.screenshotDir) {
|
|
529
|
+
throw new PlanFeatureError(
|
|
530
|
+
"Screenshot saving requires a Team plan (or Community tier for evaluation). Upgrade: https://therefrain.ai/pricing"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (!plan.features.videoRecording && config.videoDir) {
|
|
534
|
+
throw new PlanFeatureError(
|
|
535
|
+
"Video recording requires a Business plan. Upgrade: https://therefrain.ai/pricing"
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (!plan.features.skills && config.skills?.length) {
|
|
539
|
+
throw new PlanFeatureError(
|
|
540
|
+
"Skills require a Pro plan. Upgrade: https://therefrain.ai/pricing"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
if (!plan.features.stealthMode) {
|
|
544
|
+
if (config.stealth) {
|
|
545
|
+
throw new PlanFeatureError(
|
|
546
|
+
"Stealth mode requires a Pro plan. Upgrade: https://therefrain.ai/pricing"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (config.proxy) {
|
|
550
|
+
throw new PlanFeatureError(
|
|
551
|
+
"Proxy support requires a Pro plan. Upgrade: https://therefrain.ai/pricing"
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function validateStepLimit(stepCount, plan) {
|
|
557
|
+
if (stepCount > plan.limits.maxStepsPerRunbook) {
|
|
558
|
+
throw new PlanLimitError(
|
|
559
|
+
`This runbook has ${stepCount} steps, but your plan allows up to ${plan.limits.maxStepsPerRunbook}. Upgrade: https://therefrain.ai/pricing`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function validateBatchLimit(rowCount, plan) {
|
|
564
|
+
if (rowCount > plan.limits.maxBatchRows) {
|
|
565
|
+
throw new PlanLimitError(
|
|
566
|
+
`Batch has ${rowCount} rows, but your plan allows up to ${plan.limits.maxBatchRows}. Upgrade: https://therefrain.ai/pricing`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function formatPlanLabel(plan) {
|
|
571
|
+
const stepsLabel = plan.limits.maxStepsPerRunbook === Number.POSITIVE_INFINITY ? "unlimited" : `${plan.limits.maxStepsPerRunbook} steps`;
|
|
572
|
+
const batchLabel = plan.limits.maxBatchRows === Number.POSITIVE_INFINITY ? "unlimited" : `${plan.limits.maxBatchRows} batch rows`;
|
|
573
|
+
const monthlyLabel = plan.limits.maxMonthlySteps === Number.POSITIVE_INFINITY ? "unlimited" : `${plan.limits.maxMonthlySteps.toLocaleString("en-US")}/mo`;
|
|
574
|
+
return `${plan.tier.charAt(0).toUpperCase() + plan.tier.slice(1)} (${stepsLabel}, ${batchLabel}, ${monthlyLabel})`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/harness/selector-cache.ts
|
|
578
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
579
|
+
import { existsSync } from "fs";
|
|
580
|
+
import { createHash as createHash3 } from "crypto";
|
|
581
|
+
import { join as join2, dirname, basename } from "path";
|
|
582
|
+
function buildCacheKey(stepOrdinal, url, selector) {
|
|
583
|
+
const selectorHash = createHash3("sha256").update(JSON.stringify(selector)).digest("hex").slice(0, 12);
|
|
584
|
+
const urlNormalized = url.split("?")[0];
|
|
585
|
+
return `${stepOrdinal}:${urlNormalized}:${selectorHash}`;
|
|
586
|
+
}
|
|
587
|
+
function buildSnapshotHash(snapshot) {
|
|
588
|
+
return createHash3("sha256").update(snapshot).digest("hex").slice(0, 16);
|
|
589
|
+
}
|
|
590
|
+
function cacheFilePath(runbookPath) {
|
|
591
|
+
const dir = dirname(runbookPath);
|
|
592
|
+
const base = basename(runbookPath, ".yaml");
|
|
593
|
+
return join2(dir, `${base}.selector-cache.json`);
|
|
594
|
+
}
|
|
595
|
+
async function loadCache(runbookPath) {
|
|
596
|
+
const path = cacheFilePath(runbookPath);
|
|
597
|
+
if (!existsSync(path)) {
|
|
598
|
+
return { version: 1, runbookPath, entries: {} };
|
|
599
|
+
}
|
|
600
|
+
try {
|
|
601
|
+
const raw = await readFile2(path, "utf-8");
|
|
602
|
+
const parsed = JSON.parse(raw);
|
|
603
|
+
parsed.runbookPath = runbookPath;
|
|
604
|
+
return parsed;
|
|
605
|
+
} catch {
|
|
606
|
+
return { version: 1, runbookPath, entries: {} };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function saveCache(cache) {
|
|
610
|
+
const path = cacheFilePath(cache.runbookPath);
|
|
611
|
+
try {
|
|
612
|
+
await writeFile2(path, JSON.stringify(cache, null, 2), "utf-8");
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function lookupCache(cache, key) {
|
|
617
|
+
return cache.entries[key];
|
|
618
|
+
}
|
|
619
|
+
function updateCache(cache, key, ref, snapshotHash) {
|
|
620
|
+
const existing = cache.entries[key];
|
|
621
|
+
cache.entries[key] = {
|
|
622
|
+
ref,
|
|
623
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
624
|
+
snapshotHash,
|
|
625
|
+
hitCount: (existing?.hitCount ?? 0) + 1
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function invalidateCacheEntry(cache, key) {
|
|
629
|
+
delete cache.entries[key];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/runbook-executor/executor.ts
|
|
633
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
634
|
+
import { join as join3 } from "path";
|
|
635
|
+
|
|
636
|
+
// src/harness/selector-resolver.ts
|
|
637
|
+
import { z } from "zod";
|
|
638
|
+
var selectorResponseSchema = z.object({
|
|
639
|
+
reasoning: z.string().describe("\u30DE\u30C3\u30C1\u30F3\u30B0\u5224\u65AD\u306E\u7406\u7531\u30921-2\u6587\u3067\u8AAC\u660E"),
|
|
640
|
+
ref: z.string().describe("\u30DE\u30C3\u30C1\u3057\u305F\u8981\u7D20\u306Eref\u5024\uFF08\u4F8B: e3\uFF09\u3002@\u306F\u542B\u3081\u306A\u3044\u3002\u8A72\u5F53\u8981\u7D20\u304C\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u5185\u306B\u5B58\u5728\u3057\u306A\u3044\u5834\u5408\u306F\u7A7A\u6587\u5B57")
|
|
641
|
+
});
|
|
642
|
+
async function resolveImpl(snapshot, selector, stepDescription, url, contextMarkdown, debugLogger, retryContext, workingMemorySummary, aiProvider) {
|
|
643
|
+
const { system, userPrompt } = buildSelectorMessages(snapshot, selector, stepDescription, url, contextMarkdown, retryContext, workingMemorySummary);
|
|
644
|
+
const prompt = `${system}
|
|
645
|
+
---
|
|
646
|
+
${userPrompt}`;
|
|
647
|
+
debugLogger?.log({
|
|
648
|
+
phase: "executor",
|
|
649
|
+
event: "ai_selector_prompt",
|
|
650
|
+
data: { prompt, selector, stepDescription, url }
|
|
651
|
+
});
|
|
652
|
+
const model = aiProvider ? aiProvider.getModel("selector") : getModel("selector");
|
|
653
|
+
const { object } = await withAIRetry(
|
|
654
|
+
() => trackedGenerateObject("selector", {
|
|
655
|
+
model,
|
|
656
|
+
messages: [
|
|
657
|
+
{
|
|
658
|
+
role: "system",
|
|
659
|
+
content: system,
|
|
660
|
+
providerOptions: {
|
|
661
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
{ role: "user", content: userPrompt }
|
|
665
|
+
],
|
|
666
|
+
schema: selectorResponseSchema,
|
|
667
|
+
temperature: 0
|
|
668
|
+
}),
|
|
669
|
+
{ label: "selector-resolve" }
|
|
670
|
+
);
|
|
671
|
+
debugLogger?.log({
|
|
672
|
+
phase: "executor",
|
|
673
|
+
event: "ai_selector_response",
|
|
674
|
+
data: { parsedResult: object }
|
|
675
|
+
});
|
|
676
|
+
return { ref: object.ref ?? "", aiResponseText: JSON.stringify(object), reasoning: object.reasoning, prompt };
|
|
677
|
+
}
|
|
678
|
+
async function resolveWithAIDetailed(snapshot, selector, stepDescription, url, contextMarkdown, debugLogger, retryContext, workingMemorySummary, aiProvider) {
|
|
679
|
+
return resolveImpl(snapshot, selector, stepDescription, url, contextMarkdown, debugLogger, retryContext, workingMemorySummary, aiProvider);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/harness/failure-registry.ts
|
|
683
|
+
var MAX_PATTERNS = 50;
|
|
684
|
+
var FailureRegistry = class {
|
|
685
|
+
constructor() {
|
|
686
|
+
this.patterns = [];
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* 失敗を記録する。
|
|
690
|
+
* 同じ urlPattern + selectorHint + category の組み合わせは count をインクリメント。
|
|
691
|
+
*/
|
|
692
|
+
record(url, selector, category, stepOrdinal, resolution) {
|
|
693
|
+
const urlPattern = normalizeUrlPattern(url);
|
|
694
|
+
const selectorHint = buildSelectorHint(selector);
|
|
695
|
+
const existing = this.patterns.find(
|
|
696
|
+
(p) => p.urlPattern === urlPattern && p.selectorHint === selectorHint && p.category === category
|
|
697
|
+
);
|
|
698
|
+
if (existing) {
|
|
699
|
+
existing.count++;
|
|
700
|
+
existing.lastStepOrdinal = stepOrdinal;
|
|
701
|
+
if (resolution) {
|
|
702
|
+
existing.recoveryUsed = resolution.method;
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
if (this.patterns.length >= MAX_PATTERNS) {
|
|
706
|
+
this.patterns.shift();
|
|
707
|
+
}
|
|
708
|
+
this.patterns.push({
|
|
709
|
+
urlPattern,
|
|
710
|
+
selectorHint,
|
|
711
|
+
category,
|
|
712
|
+
recoveryUsed: resolution?.method,
|
|
713
|
+
count: 1,
|
|
714
|
+
lastStepOrdinal: stepOrdinal
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* URL + セレクタに関連する回復ヒントを返す。
|
|
720
|
+
* 類似パターンがなければ undefined。
|
|
721
|
+
*/
|
|
722
|
+
getRelevantHints(url, selector) {
|
|
723
|
+
const urlPattern = normalizeUrlPattern(url);
|
|
724
|
+
const selectorHint = buildSelectorHint(selector);
|
|
725
|
+
const relevant = this.patterns.filter(
|
|
726
|
+
(p) => p.urlPattern === urlPattern || p.selectorHint === selectorHint
|
|
727
|
+
);
|
|
728
|
+
if (relevant.length === 0) return void 0;
|
|
729
|
+
const recoveryMethods = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const p of relevant) {
|
|
731
|
+
if (p.recoveryUsed) {
|
|
732
|
+
recoveryMethods.set(
|
|
733
|
+
p.recoveryUsed,
|
|
734
|
+
(recoveryMethods.get(p.recoveryUsed) ?? 0) + p.count
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const parts = [];
|
|
739
|
+
const categoryCounts = /* @__PURE__ */ new Map();
|
|
740
|
+
for (const p of relevant) {
|
|
741
|
+
categoryCounts.set(p.category, (categoryCounts.get(p.category) ?? 0) + p.count);
|
|
742
|
+
}
|
|
743
|
+
const categoryStr = [...categoryCounts.entries()].map(([cat, count]) => `${cat}(${count}x)`).join(", ");
|
|
744
|
+
parts.push(`Similar failures on ${urlPattern}: ${categoryStr}`);
|
|
745
|
+
if (recoveryMethods.size > 0) {
|
|
746
|
+
const recoveryStr = [...recoveryMethods.entries()].sort((a, b) => b[1] - a[1]).map(([method, count]) => `${method}(${count}x)`).join(", ");
|
|
747
|
+
parts.push(`Resolved by: ${recoveryStr}`);
|
|
748
|
+
}
|
|
749
|
+
return parts.join(". ");
|
|
750
|
+
}
|
|
751
|
+
/** 蓄積パターン数 */
|
|
752
|
+
get size() {
|
|
753
|
+
return this.patterns.length;
|
|
754
|
+
}
|
|
755
|
+
/** パターンをクリア */
|
|
756
|
+
clear() {
|
|
757
|
+
this.patterns = [];
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
function normalizeUrlPattern(url) {
|
|
761
|
+
try {
|
|
762
|
+
const parsed = new URL(url);
|
|
763
|
+
const normalizedPath = parsed.pathname.split("/").map((segment) => {
|
|
764
|
+
if (/^\d+$/.test(segment)) return "*";
|
|
765
|
+
if (/^[0-9a-f]{8,}$/i.test(segment)) return "*";
|
|
766
|
+
return segment;
|
|
767
|
+
}).join("/");
|
|
768
|
+
return `${parsed.hostname}${normalizedPath}`;
|
|
769
|
+
} catch {
|
|
770
|
+
return url;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function buildSelectorHint(selector) {
|
|
774
|
+
const parts = [];
|
|
775
|
+
if (selector.role) parts.push(`role:${selector.role}`);
|
|
776
|
+
if (selector.name) parts.push(`name:${selector.name}`);
|
|
777
|
+
if (selector.ariaLabel) parts.push(`aria:${selector.ariaLabel}`);
|
|
778
|
+
if (selector.dataTestId) parts.push(`testid:${selector.dataTestId}`);
|
|
779
|
+
return parts.join("|") || "unknown";
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/harness/goal-agent.ts
|
|
783
|
+
import { z as z2 } from "zod";
|
|
784
|
+
var agentFallbackSchema = z2.object({
|
|
785
|
+
analysis: z2.string().describe("\u30DA\u30FC\u30B8\u306E\u73FE\u72B6\u306E\u5206\u6790\uFF08200\u5B57\u4EE5\u5185\uFF09"),
|
|
786
|
+
alternativeRef: z2.string().describe(
|
|
787
|
+
"\u4EE3\u66FF\u3068\u306A\u308B\u64CD\u4F5C\u5BFE\u8C61\u8981\u7D20\u306Eref\u5024\uFF08\u4F8B: e5\uFF09\u3002@\u306F\u542B\u3081\u306A\u3044\u3002\u898B\u3064\u304B\u3089\u306A\u3044\u5834\u5408\u306F\u7A7A\u6587\u5B57\u3002"
|
|
788
|
+
),
|
|
789
|
+
alternativeStrategy: z2.enum([
|
|
790
|
+
"direct_ref",
|
|
791
|
+
"scroll_up_and_retry",
|
|
792
|
+
"expand_collapsed",
|
|
793
|
+
"dismiss_overlay",
|
|
794
|
+
"tab_navigation",
|
|
795
|
+
"not_found"
|
|
796
|
+
]).describe("\u63A8\u5968\u3059\u308B\u4EE3\u66FF\u6226\u7565"),
|
|
797
|
+
prerequisiteRef: z2.string().optional().describe(
|
|
798
|
+
"alternativeStrategy \u304C expand_collapsed/dismiss_overlay \u306E\u5834\u5408: \u5148\u306B\u64CD\u4F5C\u3059\u308B\u8981\u7D20\u306Eref"
|
|
799
|
+
),
|
|
800
|
+
reasoning: z2.string().describe("\u4EE3\u66FF\u30D1\u30B9\u3092\u9078\u3093\u3060\u7406\u7531\uFF08100\u5B57\u4EE5\u5185\uFF09")
|
|
801
|
+
});
|
|
802
|
+
async function runGoalAgent(browser, step, failureHistory, contextMarkdown, debugLogger, aiProvider) {
|
|
803
|
+
try {
|
|
804
|
+
await browser.scroll("up", 2e3);
|
|
805
|
+
await browser.waitForDOMStability(3e3);
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
const fullSnapshot = filterSnapshot(await browser.snapshot());
|
|
809
|
+
const currentUrl = await browser.url();
|
|
810
|
+
const { system, userPrompt } = buildFallbackMessages(
|
|
811
|
+
fullSnapshot,
|
|
812
|
+
step,
|
|
813
|
+
failureHistory,
|
|
814
|
+
currentUrl,
|
|
815
|
+
contextMarkdown
|
|
816
|
+
);
|
|
817
|
+
try {
|
|
818
|
+
const model = aiProvider ? aiProvider.getModel("fallback") : getModel("fallback");
|
|
819
|
+
const { object } = await trackedGenerateObject("fallback", {
|
|
820
|
+
model,
|
|
821
|
+
messages: [
|
|
822
|
+
{
|
|
823
|
+
role: "system",
|
|
824
|
+
content: system,
|
|
825
|
+
providerOptions: {
|
|
826
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
{ role: "user", content: userPrompt }
|
|
830
|
+
],
|
|
831
|
+
schema: agentFallbackSchema,
|
|
832
|
+
temperature: 0.3
|
|
833
|
+
});
|
|
834
|
+
if (object.alternativeStrategy === "not_found" || !object.alternativeRef) {
|
|
835
|
+
return { ...object, alternativeRef: object.alternativeRef ?? "", success: false };
|
|
836
|
+
}
|
|
837
|
+
if (object.prerequisiteRef && (object.alternativeStrategy === "expand_collapsed" || object.alternativeStrategy === "dismiss_overlay")) {
|
|
838
|
+
debugLogger?.log({
|
|
839
|
+
phase: "executor",
|
|
840
|
+
event: "agent_fallback_prerequisite",
|
|
841
|
+
data: {
|
|
842
|
+
strategy: object.alternativeStrategy,
|
|
843
|
+
prerequisiteRef: object.prerequisiteRef
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
try {
|
|
847
|
+
await browser.click(`@${object.prerequisiteRef}`);
|
|
848
|
+
await browser.waitForDOMStability(2e3);
|
|
849
|
+
} catch {
|
|
850
|
+
return { ...object, alternativeRef: object.alternativeRef ?? "", success: false };
|
|
851
|
+
}
|
|
852
|
+
const updatedSnapshot = await browser.snapshot();
|
|
853
|
+
if (findElementInSnapshot(updatedSnapshot, object.alternativeRef) === null) {
|
|
854
|
+
return { ...object, success: false };
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return { ...object, success: true };
|
|
858
|
+
} catch (error) {
|
|
859
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
860
|
+
console.warn(`[goal-agent] Agent fallback AI call failed: ${msg}`);
|
|
861
|
+
return {
|
|
862
|
+
analysis: "AI\u547C\u3073\u51FA\u3057\u306B\u5931\u6557",
|
|
863
|
+
alternativeRef: "",
|
|
864
|
+
alternativeStrategy: "not_found",
|
|
865
|
+
reasoning: "AI fallback failed",
|
|
866
|
+
success: false
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/harness/vision-resolver.ts
|
|
872
|
+
import { z as z3 } from "zod";
|
|
873
|
+
var visionResponseSchema = z3.object({
|
|
874
|
+
reasoning: z3.string().describe(
|
|
875
|
+
"\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u5185\u3067\u8981\u7D20\u3092\u7279\u5B9A\u3057\u305F\u7406\u7531\uFF081-3\u6587\uFF09"
|
|
876
|
+
),
|
|
877
|
+
ref: z3.string().describe(
|
|
878
|
+
"\u7279\u5B9A\u3057\u305F\u8981\u7D20\u306E ref \u5024\uFF08\u4F8B: e3\uFF09\u3002@\u306F\u542B\u3081\u306A\u3044\u3002\u8A72\u5F53\u3059\u308B\u8981\u7D20\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u5834\u5408\u306F\u7A7A\u6587\u5B57"
|
|
879
|
+
),
|
|
880
|
+
annotationNumber: z3.number().optional().describe(
|
|
881
|
+
"\u7279\u5B9A\u3057\u305F\u8981\u7D20\u306E\u30A2\u30CE\u30C6\u30FC\u30B7\u30E7\u30F3\u756A\u53F7\uFF08\u30B9\u30AF\u30EA\u30FC\u30F3\u30B7\u30E7\u30C3\u30C8\u4E0A\u306E\u756A\u53F7\u30E9\u30D9\u30EB\uFF09"
|
|
882
|
+
),
|
|
883
|
+
confidence: z3.number().min(0).max(1).describe(
|
|
884
|
+
"\u7279\u5B9A\u306E\u78BA\u4FE1\u5EA6\uFF080.0-1.0\uFF09"
|
|
885
|
+
)
|
|
886
|
+
});
|
|
887
|
+
async function resolveWithVision(imageBuffer, annotations, selector, stepDescription, url, failureHistory, debugLogger, aiProvider) {
|
|
888
|
+
debugLogger?.log({
|
|
889
|
+
phase: "executor",
|
|
890
|
+
event: "vision_fallback_start",
|
|
891
|
+
data: {
|
|
892
|
+
annotationCount: annotations.length,
|
|
893
|
+
selector,
|
|
894
|
+
stepDescription,
|
|
895
|
+
url,
|
|
896
|
+
failureHistoryCount: failureHistory.length
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
if (annotations.length === 0) {
|
|
900
|
+
debugLogger?.log({
|
|
901
|
+
phase: "executor",
|
|
902
|
+
event: "vision_fallback_skip",
|
|
903
|
+
data: { reason: "no annotations" }
|
|
904
|
+
});
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
const { system, userTextContent } = buildVisionMessages(
|
|
908
|
+
annotations,
|
|
909
|
+
selector,
|
|
910
|
+
stepDescription,
|
|
911
|
+
url,
|
|
912
|
+
failureHistory
|
|
913
|
+
);
|
|
914
|
+
try {
|
|
915
|
+
const model = aiProvider ? aiProvider.getModel("vision") : getModel("vision");
|
|
916
|
+
const { object } = await withAIRetry(
|
|
917
|
+
() => trackedGenerateObject("vision", {
|
|
918
|
+
model,
|
|
919
|
+
messages: [
|
|
920
|
+
{
|
|
921
|
+
role: "system",
|
|
922
|
+
content: system,
|
|
923
|
+
providerOptions: {
|
|
924
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
role: "user",
|
|
929
|
+
content: [
|
|
930
|
+
{ type: "text", text: userTextContent },
|
|
931
|
+
{ type: "image", image: imageBuffer }
|
|
932
|
+
]
|
|
933
|
+
}
|
|
934
|
+
],
|
|
935
|
+
schema: visionResponseSchema,
|
|
936
|
+
temperature: 0
|
|
937
|
+
}),
|
|
938
|
+
{ label: "vision-resolve" }
|
|
939
|
+
);
|
|
940
|
+
debugLogger?.log({
|
|
941
|
+
phase: "executor",
|
|
942
|
+
event: "vision_fallback_result",
|
|
943
|
+
data: {
|
|
944
|
+
ref: object.ref,
|
|
945
|
+
reasoning: object.reasoning,
|
|
946
|
+
annotationNumber: object.annotationNumber,
|
|
947
|
+
confidence: object.confidence
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
if (!object.ref) return null;
|
|
951
|
+
const matchingAnnotation = annotations.find((a) => a.ref === object.ref);
|
|
952
|
+
if (!matchingAnnotation) {
|
|
953
|
+
debugLogger?.log({
|
|
954
|
+
phase: "executor",
|
|
955
|
+
event: "vision_fallback_invalid_ref",
|
|
956
|
+
data: {
|
|
957
|
+
ref: object.ref,
|
|
958
|
+
availableRefs: annotations.map((a) => a.ref)
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
return {
|
|
964
|
+
ref: object.ref,
|
|
965
|
+
reasoning: object.reasoning,
|
|
966
|
+
annotationNumber: object.annotationNumber,
|
|
967
|
+
confidence: object.confidence
|
|
968
|
+
};
|
|
969
|
+
} catch (error) {
|
|
970
|
+
debugLogger?.log({
|
|
971
|
+
phase: "executor",
|
|
972
|
+
event: "vision_fallback_error",
|
|
973
|
+
data: { error: error instanceof Error ? error.message : String(error) }
|
|
974
|
+
});
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/harness/action-validator.ts
|
|
980
|
+
var COMPATIBLE_ROLES = {
|
|
981
|
+
click: /* @__PURE__ */ new Set([
|
|
982
|
+
"button",
|
|
983
|
+
"link",
|
|
984
|
+
"checkbox",
|
|
985
|
+
"radio",
|
|
986
|
+
"tab",
|
|
987
|
+
"menuitem",
|
|
988
|
+
"menuitemcheckbox",
|
|
989
|
+
"menuitemradio",
|
|
990
|
+
"switch",
|
|
991
|
+
"option",
|
|
992
|
+
"treeitem",
|
|
993
|
+
"row",
|
|
994
|
+
"cell",
|
|
995
|
+
"gridcell",
|
|
996
|
+
"img",
|
|
997
|
+
"figure"
|
|
998
|
+
]),
|
|
999
|
+
input: /* @__PURE__ */ new Set(["textbox", "searchbox", "combobox", "spinbutton", "textarea"]),
|
|
1000
|
+
select: /* @__PURE__ */ new Set(["combobox", "listbox", "select"]),
|
|
1001
|
+
download: /* @__PURE__ */ new Set(["link", "button", "menuitem", "menuitemcheckbox", "menuitemradio"]),
|
|
1002
|
+
hover: /* @__PURE__ */ new Set(),
|
|
1003
|
+
// 空 = 全ロール OK
|
|
1004
|
+
key: /* @__PURE__ */ new Set()
|
|
1005
|
+
// 空 = セレクタ不要、グローバルキーボード操作
|
|
1006
|
+
};
|
|
1007
|
+
var CLICK_INCOMPATIBLE_ROLES = /* @__PURE__ */ new Set([
|
|
1008
|
+
"textbox",
|
|
1009
|
+
"searchbox",
|
|
1010
|
+
"textarea",
|
|
1011
|
+
"spinbutton"
|
|
1012
|
+
]);
|
|
1013
|
+
var VALUE_REQUIRED_ACTIONS = /* @__PURE__ */ new Set(["input", "select"]);
|
|
1014
|
+
function isValidRole(role) {
|
|
1015
|
+
return role.length > 1 && /^[a-z]/.test(role);
|
|
1016
|
+
}
|
|
1017
|
+
function validateAction(snapshot, ref, actionType, value, refs) {
|
|
1018
|
+
const errors = [];
|
|
1019
|
+
const warnings = [];
|
|
1020
|
+
let element = null;
|
|
1021
|
+
const refEntry = refs?.[ref];
|
|
1022
|
+
if (refEntry) {
|
|
1023
|
+
element = { ref, role: refEntry.role, name: refEntry.name ?? "", attributes: {} };
|
|
1024
|
+
} else {
|
|
1025
|
+
element = findElementInSnapshot(snapshot, ref);
|
|
1026
|
+
}
|
|
1027
|
+
if (!element) {
|
|
1028
|
+
errors.push({
|
|
1029
|
+
code: "ref_not_found",
|
|
1030
|
+
message: `[ref=${ref}] \u304C\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u5185\u306B\u5B58\u5728\u3057\u307E\u305B\u3093`
|
|
1031
|
+
});
|
|
1032
|
+
return { valid: false, errors, warnings };
|
|
1033
|
+
}
|
|
1034
|
+
const normalizedAction = normalizeActionType(actionType);
|
|
1035
|
+
if (normalizedAction !== "hover" && isValidRole(element.role)) {
|
|
1036
|
+
const compatible = COMPATIBLE_ROLES[normalizedAction];
|
|
1037
|
+
if (compatible && compatible.size > 0) {
|
|
1038
|
+
if (!compatible.has(element.role)) {
|
|
1039
|
+
if (normalizedAction === "click" && !CLICK_INCOMPATIBLE_ROLES.has(element.role)) {
|
|
1040
|
+
} else {
|
|
1041
|
+
errors.push({
|
|
1042
|
+
code: "type_mismatch",
|
|
1043
|
+
message: `\u30A2\u30AF\u30B7\u30E7\u30F3 "${actionType}" \u306F role="${element.role}" \u306E\u8981\u7D20\u3068\u4E92\u63DB\u6027\u304C\u3042\u308A\u307E\u305B\u3093\uFF08ref=${ref}\uFF09`
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (VALUE_REQUIRED_ACTIONS.has(normalizedAction) && (!value || value.trim() === "")) {
|
|
1050
|
+
errors.push({
|
|
1051
|
+
code: "value_required",
|
|
1052
|
+
message: `\u30A2\u30AF\u30B7\u30E7\u30F3 "${actionType}" \u306B\u306F\u5024\u304C\u5FC5\u8981\u3067\u3059\uFF08ref=${ref}\uFF09`
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (element.attributes.disabled || element.attributes.disabled === "true") {
|
|
1056
|
+
warnings.push({
|
|
1057
|
+
code: "element_disabled",
|
|
1058
|
+
message: `\u8981\u7D20\u304C disabled \u3067\u3059\uFF08ref=${ref}, role=${element.role}\uFF09`
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
if (element.attributes.readonly || element.attributes.readonly === "true") {
|
|
1062
|
+
if (normalizedAction === "input") {
|
|
1063
|
+
warnings.push({
|
|
1064
|
+
code: "element_readonly",
|
|
1065
|
+
message: `\u8981\u7D20\u304C readonly \u3067\u3059\uFF08ref=${ref}, role=${element.role}\uFF09`
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const overlayWarning = checkOverlay(snapshot, ref);
|
|
1070
|
+
if (overlayWarning) {
|
|
1071
|
+
warnings.push(overlayWarning);
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
valid: errors.length === 0,
|
|
1075
|
+
errors,
|
|
1076
|
+
warnings
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function normalizeActionType(actionType) {
|
|
1080
|
+
switch (actionType) {
|
|
1081
|
+
case "fill":
|
|
1082
|
+
case "type":
|
|
1083
|
+
case "input":
|
|
1084
|
+
return "input";
|
|
1085
|
+
case "click":
|
|
1086
|
+
return "click";
|
|
1087
|
+
case "select":
|
|
1088
|
+
return "select";
|
|
1089
|
+
case "hover":
|
|
1090
|
+
return "hover";
|
|
1091
|
+
default:
|
|
1092
|
+
return actionType;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function checkOverlay(snapshot, targetRef) {
|
|
1096
|
+
const lines = snapshot.split("\n");
|
|
1097
|
+
let dialogLineIndex = -1;
|
|
1098
|
+
let dialogIndent = -1;
|
|
1099
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1100
|
+
const trimmed = lines[i].trimStart();
|
|
1101
|
+
if (trimmed.startsWith("dialog") || trimmed.startsWith("alertdialog")) {
|
|
1102
|
+
dialogLineIndex = i;
|
|
1103
|
+
dialogIndent = lines[i].length - trimmed.length;
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (dialogLineIndex === -1) return null;
|
|
1108
|
+
const refPattern = `[ref=${targetRef}]`;
|
|
1109
|
+
let targetLineIndex = -1;
|
|
1110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1111
|
+
if (lines[i].includes(refPattern)) {
|
|
1112
|
+
targetLineIndex = i;
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (targetLineIndex === -1) return null;
|
|
1117
|
+
if (targetLineIndex > dialogLineIndex) {
|
|
1118
|
+
const targetIndent = lines[targetLineIndex].length - lines[targetLineIndex].trimStart().length;
|
|
1119
|
+
if (targetIndent > dialogIndent) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
code: "possible_overlay",
|
|
1125
|
+
message: `dialog/alertdialog \u304C\u5B58\u5728\u3057\u3001\u30BF\u30FC\u30B2\u30C3\u30C8\u8981\u7D20\uFF08ref=${targetRef}\uFF09\u304C\u305D\u306E\u5916\u5074\u306B\u3042\u308A\u307E\u3059\u3002\u30E2\u30FC\u30C0\u30EB\u306B\u906E\u3089\u308C\u3066\u3044\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059`
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/harness/deterministic-resolver.ts
|
|
1130
|
+
function resolveDeterministic(snapshot, selector, refs) {
|
|
1131
|
+
const noMatch = {
|
|
1132
|
+
ref: null,
|
|
1133
|
+
matchType: null,
|
|
1134
|
+
confidence: 0
|
|
1135
|
+
};
|
|
1136
|
+
const parsedElements = [];
|
|
1137
|
+
const elements = [];
|
|
1138
|
+
if (refs) {
|
|
1139
|
+
for (const [ref, entry] of Object.entries(refs)) {
|
|
1140
|
+
elements.push({ ref, role: entry.role, name: entry.name ?? "" });
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
const parsed = parseAllElements(snapshot);
|
|
1144
|
+
parsedElements.push(...parsed);
|
|
1145
|
+
for (const el of parsed) {
|
|
1146
|
+
elements.push({
|
|
1147
|
+
ref: el.ref,
|
|
1148
|
+
role: el.role,
|
|
1149
|
+
name: el.name,
|
|
1150
|
+
placeholder: el.attributes.placeholder
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (elements.length === 0) return noMatch;
|
|
1155
|
+
const selectorRole = normalizeString(selector.role);
|
|
1156
|
+
const selectorAriaLabel = normalizeString(selector.ariaLabel);
|
|
1157
|
+
const selectorText = normalizeString(selector.text);
|
|
1158
|
+
const selectorInnerText = normalizeString(selector.innerText);
|
|
1159
|
+
const selectorPlaceholder = normalizeString(selector.placeholder);
|
|
1160
|
+
if (!selectorAriaLabel && !selectorText && !selectorInnerText && !selectorPlaceholder && !selectorRole) return noMatch;
|
|
1161
|
+
const labelLower = selectorAriaLabel?.toLowerCase();
|
|
1162
|
+
const textLower = selectorText?.toLowerCase();
|
|
1163
|
+
const innerTextLower = selectorInnerText?.toLowerCase();
|
|
1164
|
+
let p1 = null;
|
|
1165
|
+
let p2 = null;
|
|
1166
|
+
let p3 = null;
|
|
1167
|
+
let p4 = null;
|
|
1168
|
+
let p5 = null;
|
|
1169
|
+
let p6 = null;
|
|
1170
|
+
let p7 = null;
|
|
1171
|
+
let p8 = null;
|
|
1172
|
+
for (const el of elements) {
|
|
1173
|
+
const nameExact = selectorAriaLabel ? el.name === selectorAriaLabel : false;
|
|
1174
|
+
const roleMatch = selectorRole ? el.role === selectorRole : false;
|
|
1175
|
+
if (roleMatch && nameExact) {
|
|
1176
|
+
if (!p1) {
|
|
1177
|
+
p1 = { ref: el.ref, count: 1 };
|
|
1178
|
+
} else {
|
|
1179
|
+
p1.count++;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (nameExact) {
|
|
1183
|
+
if (!p2) {
|
|
1184
|
+
p2 = { ref: el.ref, count: 1 };
|
|
1185
|
+
} else {
|
|
1186
|
+
p2.count++;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const nameLower = labelLower || textLower || innerTextLower ? el.name.toLowerCase() : "";
|
|
1190
|
+
if (roleMatch && labelLower && nameLower.includes(labelLower)) {
|
|
1191
|
+
if (!p3) {
|
|
1192
|
+
p3 = { ref: el.ref, count: 1 };
|
|
1193
|
+
} else {
|
|
1194
|
+
p3.count++;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
if (roleMatch && textLower && nameLower.includes(textLower)) {
|
|
1198
|
+
if (!p4) {
|
|
1199
|
+
p4 = { ref: el.ref, count: 1 };
|
|
1200
|
+
} else {
|
|
1201
|
+
p4.count++;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (!selectorRole && labelLower && nameLower.includes(labelLower)) {
|
|
1205
|
+
if (!p5) {
|
|
1206
|
+
p5 = { ref: el.ref, count: 1 };
|
|
1207
|
+
} else {
|
|
1208
|
+
p5.count++;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (roleMatch && innerTextLower && nameLower.includes(innerTextLower)) {
|
|
1212
|
+
if (!p6) {
|
|
1213
|
+
p6 = { ref: el.ref, count: 1 };
|
|
1214
|
+
} else {
|
|
1215
|
+
p6.count++;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (selectorPlaceholder && el.placeholder && el.placeholder === selectorPlaceholder) {
|
|
1219
|
+
if (!p7) {
|
|
1220
|
+
p7 = { ref: el.ref, count: 1 };
|
|
1221
|
+
} else {
|
|
1222
|
+
p7.count++;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (selectorRole && el.role === selectorRole) {
|
|
1226
|
+
if (!p8) {
|
|
1227
|
+
p8 = { ref: el.ref, count: 1 };
|
|
1228
|
+
} else {
|
|
1229
|
+
p8.count++;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (p1 && p1.count === 1) return { ref: p1.ref, matchType: "exact_role_and_name", confidence: 1 };
|
|
1234
|
+
if (p2 && p2.count === 1) return { ref: p2.ref, matchType: "exact_name", confidence: 0.9 };
|
|
1235
|
+
if (p3 && p3.count === 1) return { ref: p3.ref, matchType: "role_and_partial_name", confidence: 0.7 };
|
|
1236
|
+
if (p4 && p4.count === 1) return { ref: p4.ref, matchType: "role_and_text", confidence: 0.6 };
|
|
1237
|
+
if (p5 && p5.count === 1) return { ref: p5.ref, matchType: "role_and_partial_name", confidence: 0.6 };
|
|
1238
|
+
if (p6 && p6.count === 1) return { ref: p6.ref, matchType: "role_and_innerText", confidence: 0.55 };
|
|
1239
|
+
if (p7 && p7.count === 1) return { ref: p7.ref, matchType: "placeholder", confidence: 0.5 };
|
|
1240
|
+
if (p8 && p8.count === 1) return { ref: p8.ref, matchType: "unique_role", confidence: 0.4 };
|
|
1241
|
+
return noMatch;
|
|
1242
|
+
}
|
|
1243
|
+
function normalizeString(value) {
|
|
1244
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
1245
|
+
return value.trim();
|
|
1246
|
+
}
|
|
1247
|
+
return void 0;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/harness/confirmation.ts
|
|
1251
|
+
function needsConfirmation(step, skipAll) {
|
|
1252
|
+
if (skipAll) return false;
|
|
1253
|
+
return step.requiresConfirmation;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/runbook-executor/confirmation.ts
|
|
1257
|
+
var CliConfirmationProvider = class {
|
|
1258
|
+
async confirm(step, _stepIndex, _context) {
|
|
1259
|
+
return promptConfirmation(step);
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
async function promptConfirmation(step) {
|
|
1263
|
+
note(
|
|
1264
|
+
`${tf("confirmation.stepInfo", { ordinal: step.ordinal, description: step.description })}
|
|
1265
|
+
${tf("confirmation.riskLevelLabel", { level: step.riskLevel, hint: formatRiskHint(step.riskLevel) })}`,
|
|
1266
|
+
t("confirmation.title")
|
|
1267
|
+
);
|
|
1268
|
+
return promptSelect(t("confirmation.selectAction"), [
|
|
1269
|
+
{ value: "approve", label: t("confirmation.approve") },
|
|
1270
|
+
{ value: "skip", label: t("confirmation.skip") },
|
|
1271
|
+
{ value: "abort", label: t("confirmation.abort"), hint: t("confirmation.abortHint") }
|
|
1272
|
+
]);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/context/capture-handler.ts
|
|
1276
|
+
import { z as z4 } from "zod";
|
|
1277
|
+
var captureValueSchema = z4.object({
|
|
1278
|
+
value: z4.string().nullable().describe("\u62BD\u51FA\u3057\u305F\u5024\u3002\u898B\u3064\u304B\u3089\u306A\u3044\u5834\u5408\u306Fnull")
|
|
1279
|
+
});
|
|
1280
|
+
async function executeCapture(browser, capture, store) {
|
|
1281
|
+
switch (capture.strategy) {
|
|
1282
|
+
case "snapshot":
|
|
1283
|
+
return captureFromSnapshot(browser, capture);
|
|
1284
|
+
case "url":
|
|
1285
|
+
return captureFromUrl(browser, capture);
|
|
1286
|
+
case "ai":
|
|
1287
|
+
return captureWithAI(browser, capture);
|
|
1288
|
+
case "expression":
|
|
1289
|
+
return captureFromExpression(capture, store);
|
|
1290
|
+
case "evaluate":
|
|
1291
|
+
return captureWithEvaluate(browser, capture);
|
|
1292
|
+
default:
|
|
1293
|
+
log.warn(`Unknown capture strategy: ${capture.strategy}`);
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async function captureFromSnapshot(browser, capture) {
|
|
1298
|
+
if (!capture.pattern) {
|
|
1299
|
+
log.warn(`Capture "${capture.name}": snapshot strategy requires pattern`);
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
const snapshot = await browser.snapshot();
|
|
1303
|
+
let re;
|
|
1304
|
+
try {
|
|
1305
|
+
re = new RegExp(capture.pattern);
|
|
1306
|
+
} catch {
|
|
1307
|
+
log.warn(`Capture "${capture.name}": invalid regex pattern: ${capture.pattern}`);
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
const match = re.exec(snapshot);
|
|
1311
|
+
if (!match) {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
const group = capture.group ?? 1;
|
|
1315
|
+
return match[group] ?? match[0] ?? null;
|
|
1316
|
+
}
|
|
1317
|
+
async function captureFromUrl(browser, capture) {
|
|
1318
|
+
if (!capture.pattern) {
|
|
1319
|
+
log.warn(`Capture "${capture.name}": url strategy requires pattern`);
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
const currentUrl = await browser.url();
|
|
1323
|
+
let re;
|
|
1324
|
+
try {
|
|
1325
|
+
re = new RegExp(capture.pattern);
|
|
1326
|
+
} catch {
|
|
1327
|
+
log.warn(`Capture "${capture.name}": invalid regex pattern: ${capture.pattern}`);
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
const match = re.exec(currentUrl);
|
|
1331
|
+
if (!match) {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
const group = capture.group ?? 1;
|
|
1335
|
+
return match[group] ?? match[0] ?? null;
|
|
1336
|
+
}
|
|
1337
|
+
var CAPTURE_SYSTEM_PROMPT = "\u4EE5\u4E0B\u306E\u30DA\u30FC\u30B8\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u304B\u3089\u60C5\u5831\u3092\u62BD\u51FA\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u62BD\u51FA\u5BFE\u8C61\u306E\u8AAC\u660E\u306B\u57FA\u3065\u3044\u3066\u3001\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u5185\u304B\u3089\u8A72\u5F53\u3059\u308B\u5024\u3092\u7279\u5B9A\u3057\u3066\u8FD4\u3057\u3066\u304F\u3060\u3055\u3044\u3002";
|
|
1338
|
+
async function captureWithAI(browser, capture) {
|
|
1339
|
+
if (!capture.prompt) {
|
|
1340
|
+
log.warn(`Capture "${capture.name}": ai strategy requires prompt`);
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
const snapshot = filterSnapshot(await browser.snapshot());
|
|
1344
|
+
const currentUrl = await browser.url();
|
|
1345
|
+
const userPrompt = [
|
|
1346
|
+
`## \u62BD\u51FA\u5BFE\u8C61`,
|
|
1347
|
+
capture.prompt,
|
|
1348
|
+
"",
|
|
1349
|
+
`## \u73FE\u5728\u306EURL`,
|
|
1350
|
+
currentUrl,
|
|
1351
|
+
"",
|
|
1352
|
+
`## \u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8`,
|
|
1353
|
+
snapshot
|
|
1354
|
+
].join("\n");
|
|
1355
|
+
try {
|
|
1356
|
+
const { object } = await withAIRetry(
|
|
1357
|
+
() => trackedGenerateObject("extraction", {
|
|
1358
|
+
model: getModel("extraction"),
|
|
1359
|
+
messages: [
|
|
1360
|
+
{
|
|
1361
|
+
role: "system",
|
|
1362
|
+
content: CAPTURE_SYSTEM_PROMPT,
|
|
1363
|
+
providerOptions: {
|
|
1364
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
{ role: "user", content: userPrompt }
|
|
1368
|
+
],
|
|
1369
|
+
schema: captureValueSchema,
|
|
1370
|
+
temperature: 0
|
|
1371
|
+
}),
|
|
1372
|
+
{ label: "capture-ai" }
|
|
1373
|
+
);
|
|
1374
|
+
return object.value ?? null;
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
log.warn(
|
|
1377
|
+
`Capture "${capture.name}" AI extraction failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1378
|
+
);
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
async function captureWithEvaluate(browser, capture) {
|
|
1383
|
+
if (!capture.expression) {
|
|
1384
|
+
log.warn(`Capture "${capture.name}": evaluate strategy requires expression (JavaScript code)`);
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
const result = await browser.evaluate(capture.expression);
|
|
1389
|
+
if (result === null || result === void 0) {
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
log.warn(
|
|
1395
|
+
`Capture "${capture.name}" evaluate failed: ${error instanceof Error ? error.message : String(error)}`
|
|
1396
|
+
);
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function captureFromExpression(capture, store) {
|
|
1401
|
+
if (!capture.expression) {
|
|
1402
|
+
log.warn(`Capture "${capture.name}": expression strategy requires expression`);
|
|
1403
|
+
return null;
|
|
1404
|
+
}
|
|
1405
|
+
const result = store.resolveTemplate(capture.expression);
|
|
1406
|
+
if (/\{\{\w+\}\}/.test(result)) {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
return result;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/context/expression-evaluator.ts
|
|
1413
|
+
var COMPARISON_OPERATORS = ["===", "!==", "==", "!=", "<=", ">=", "<", ">"];
|
|
1414
|
+
function evaluateCondition(expression) {
|
|
1415
|
+
const trimmed = expression.trim();
|
|
1416
|
+
if (!trimmed) {
|
|
1417
|
+
return { value: false, error: "Empty expression" };
|
|
1418
|
+
}
|
|
1419
|
+
if (trimmed.startsWith("!") && !trimmed.startsWith("!=")) {
|
|
1420
|
+
const inner = trimmed.slice(1).trim();
|
|
1421
|
+
const operand2 = parseOperand(inner);
|
|
1422
|
+
if (operand2 === void 0) {
|
|
1423
|
+
return { value: false, error: `Cannot parse operand: ${inner}` };
|
|
1424
|
+
}
|
|
1425
|
+
return { value: !isTruthy(operand2) };
|
|
1426
|
+
}
|
|
1427
|
+
const comparison = findComparison(trimmed);
|
|
1428
|
+
if (comparison) {
|
|
1429
|
+
const { left, operator, right } = comparison;
|
|
1430
|
+
const leftVal = parseOperand(left);
|
|
1431
|
+
const rightVal = parseOperand(right);
|
|
1432
|
+
if (leftVal === void 0) {
|
|
1433
|
+
return { value: false, error: `Cannot parse left operand: ${left}` };
|
|
1434
|
+
}
|
|
1435
|
+
if (rightVal === void 0) {
|
|
1436
|
+
return { value: false, error: `Cannot parse right operand: ${right}` };
|
|
1437
|
+
}
|
|
1438
|
+
return { value: compare(leftVal, rightVal, operator) };
|
|
1439
|
+
}
|
|
1440
|
+
const operand = parseOperand(trimmed);
|
|
1441
|
+
if (operand === void 0) {
|
|
1442
|
+
return { value: false, error: `Cannot parse expression: ${trimmed}` };
|
|
1443
|
+
}
|
|
1444
|
+
return { value: isTruthy(operand) };
|
|
1445
|
+
}
|
|
1446
|
+
function findComparison(expr) {
|
|
1447
|
+
for (const op of COMPARISON_OPERATORS) {
|
|
1448
|
+
const idx = findOperatorOutsideStrings(expr, op);
|
|
1449
|
+
if (idx !== -1) {
|
|
1450
|
+
return {
|
|
1451
|
+
left: expr.slice(0, idx).trim(),
|
|
1452
|
+
operator: op,
|
|
1453
|
+
right: expr.slice(idx + op.length).trim()
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
function findOperatorOutsideStrings(expr, op) {
|
|
1460
|
+
let inSingle = false;
|
|
1461
|
+
let inDouble = false;
|
|
1462
|
+
for (let i = 0; i < expr.length; i++) {
|
|
1463
|
+
const ch = expr[i];
|
|
1464
|
+
if (ch === "'" && !inDouble && (i === 0 || expr[i - 1] !== "\\")) {
|
|
1465
|
+
inSingle = !inSingle;
|
|
1466
|
+
} else if (ch === '"' && !inSingle && (i === 0 || expr[i - 1] !== "\\")) {
|
|
1467
|
+
inDouble = !inDouble;
|
|
1468
|
+
} else if (!inSingle && !inDouble) {
|
|
1469
|
+
if (expr.slice(i, i + op.length) === op) {
|
|
1470
|
+
if (op === "<" && expr[i + 1] === "=") continue;
|
|
1471
|
+
if (op === ">" && expr[i + 1] === "=") continue;
|
|
1472
|
+
if (op === "=" && op.length === 1) continue;
|
|
1473
|
+
if (op === "==" && expr[i + 2] === "=") continue;
|
|
1474
|
+
if (op === "!=" && expr[i + 2] === "=") continue;
|
|
1475
|
+
return i;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return -1;
|
|
1480
|
+
}
|
|
1481
|
+
function parseOperand(raw) {
|
|
1482
|
+
const trimmed = raw.trim();
|
|
1483
|
+
if (!trimmed) return void 0;
|
|
1484
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
1485
|
+
return trimmed.slice(1, -1).replace(/\\'/g, "'").replace(/\\"/g, '"');
|
|
1486
|
+
}
|
|
1487
|
+
if (trimmed === "true") return true;
|
|
1488
|
+
if (trimmed === "false") return false;
|
|
1489
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
1490
|
+
return Number(trimmed);
|
|
1491
|
+
}
|
|
1492
|
+
return trimmed;
|
|
1493
|
+
}
|
|
1494
|
+
function isTruthy(val) {
|
|
1495
|
+
if (typeof val === "boolean") return val;
|
|
1496
|
+
if (typeof val === "number") return val !== 0;
|
|
1497
|
+
if (typeof val === "string") return val !== "" && val !== "false" && val !== "0";
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
function compare(left, right, op) {
|
|
1501
|
+
switch (op) {
|
|
1502
|
+
case "===":
|
|
1503
|
+
return left === right;
|
|
1504
|
+
case "!==":
|
|
1505
|
+
return left !== right;
|
|
1506
|
+
case "==":
|
|
1507
|
+
return String(left) === String(right);
|
|
1508
|
+
case "!=":
|
|
1509
|
+
return String(left) !== String(right);
|
|
1510
|
+
case "<":
|
|
1511
|
+
return toNumber(left) < toNumber(right);
|
|
1512
|
+
case ">":
|
|
1513
|
+
return toNumber(left) > toNumber(right);
|
|
1514
|
+
case "<=":
|
|
1515
|
+
return toNumber(left) <= toNumber(right);
|
|
1516
|
+
case ">=":
|
|
1517
|
+
return toNumber(left) >= toNumber(right);
|
|
1518
|
+
default:
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function toNumber(val) {
|
|
1523
|
+
if (typeof val === "number") return val;
|
|
1524
|
+
if (typeof val === "boolean") return val ? 1 : 0;
|
|
1525
|
+
const n = Number(val);
|
|
1526
|
+
return Number.isNaN(n) ? 0 : n;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/context/snapshot-cache.ts
|
|
1530
|
+
import { createHash as createHash4 } from "crypto";
|
|
1531
|
+
var SnapshotCache = class {
|
|
1532
|
+
constructor(maxEntries = 5) {
|
|
1533
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
1534
|
+
this.maxEntries = maxEntries;
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* フィルタ済みスナップショットを返す(キャッシュがあればそれを利用)。
|
|
1538
|
+
*
|
|
1539
|
+
* opts が異なる場合は別キーとして扱う(filledSelectors のサイズをキーに含める)。
|
|
1540
|
+
*/
|
|
1541
|
+
getFiltered(raw, opts) {
|
|
1542
|
+
const key = this.buildKey(raw, opts);
|
|
1543
|
+
const existing = this.cache.get(key);
|
|
1544
|
+
if (existing) {
|
|
1545
|
+
this.cache.delete(key);
|
|
1546
|
+
this.cache.set(key, existing);
|
|
1547
|
+
return existing.filtered;
|
|
1548
|
+
}
|
|
1549
|
+
const filtered = filterSnapshot(raw, opts);
|
|
1550
|
+
const tokenEstimate = Math.ceil(filtered.length / 4);
|
|
1551
|
+
this.evictIfNeeded();
|
|
1552
|
+
this.cache.set(key, {
|
|
1553
|
+
rawHash: this.hashSnapshot(raw),
|
|
1554
|
+
filtered,
|
|
1555
|
+
tokenEstimate,
|
|
1556
|
+
createdAt: Date.now()
|
|
1557
|
+
});
|
|
1558
|
+
return filtered;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* 2つのスナップショットが同一内容かどうかをハッシュで高速判定する。
|
|
1562
|
+
*/
|
|
1563
|
+
hasChanged(rawA, rawB) {
|
|
1564
|
+
return this.hashSnapshot(rawA) !== this.hashSnapshot(rawB);
|
|
1565
|
+
}
|
|
1566
|
+
/** キャッシュをクリアする */
|
|
1567
|
+
clear() {
|
|
1568
|
+
this.cache.clear();
|
|
1569
|
+
}
|
|
1570
|
+
/** 現在のキャッシュエントリ数 */
|
|
1571
|
+
get size() {
|
|
1572
|
+
return this.cache.size;
|
|
1573
|
+
}
|
|
1574
|
+
buildKey(raw, opts) {
|
|
1575
|
+
const rawHash = this.hashSnapshot(raw);
|
|
1576
|
+
const filledCount = opts?.filledSelectors?.size ?? 0;
|
|
1577
|
+
return `${rawHash}:${filledCount}`;
|
|
1578
|
+
}
|
|
1579
|
+
hashSnapshot(raw) {
|
|
1580
|
+
return createHash4("sha256").update(raw).digest("hex").slice(0, 16);
|
|
1581
|
+
}
|
|
1582
|
+
evictIfNeeded() {
|
|
1583
|
+
while (this.cache.size >= this.maxEntries) {
|
|
1584
|
+
const oldestKey = this.cache.keys().next().value;
|
|
1585
|
+
if (oldestKey !== void 0) {
|
|
1586
|
+
this.cache.delete(oldestKey);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
// src/context/context-chunker.ts
|
|
1593
|
+
var ContextChunker = class {
|
|
1594
|
+
constructor(markdown) {
|
|
1595
|
+
/** 直近の selectRelevant() で選択されたチャンク数 */
|
|
1596
|
+
this._lastSelectedCount = 0;
|
|
1597
|
+
this.fullText = markdown;
|
|
1598
|
+
this.chunks = parseMarkdownIntoChunks(markdown);
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* ステップ説明 + URL に関連するチャンクを選択して返す。
|
|
1602
|
+
* キーワードオーバーラップスコアリングによる選択(AI 不要)。
|
|
1603
|
+
*
|
|
1604
|
+
* @param stepDescription ステップの説明テキスト
|
|
1605
|
+
* @param url 現在のページURL
|
|
1606
|
+
* @param maxTokens 最大トークン数(デフォルト: 2000)
|
|
1607
|
+
* @returns 関連チャンクを結合したテキスト。関連チャンクがなければ空文字。
|
|
1608
|
+
*/
|
|
1609
|
+
selectRelevant(stepDescription, url, maxTokens = 2e3) {
|
|
1610
|
+
if (this.chunks.length === 0) {
|
|
1611
|
+
this._lastSelectedCount = 0;
|
|
1612
|
+
return this.fullText;
|
|
1613
|
+
}
|
|
1614
|
+
const queryTerms = extractKeywords(`${stepDescription} ${url}`);
|
|
1615
|
+
if (queryTerms.length === 0) {
|
|
1616
|
+
this._lastSelectedCount = 0;
|
|
1617
|
+
return this.fullText;
|
|
1618
|
+
}
|
|
1619
|
+
const scored = this.chunks.map((chunk) => ({
|
|
1620
|
+
chunk,
|
|
1621
|
+
score: keywordOverlapScore(queryTerms, chunk.keywords)
|
|
1622
|
+
})).filter((s) => s.score > 0).sort((a, b) => b.score - a.score);
|
|
1623
|
+
if (scored.length === 0) {
|
|
1624
|
+
this._lastSelectedCount = 0;
|
|
1625
|
+
const fullTokens = Math.ceil(this.fullText.length / 4);
|
|
1626
|
+
if (fullTokens <= maxTokens) return this.fullText;
|
|
1627
|
+
return this.fullText.slice(0, maxTokens * 4) + "\n[... truncated]";
|
|
1628
|
+
}
|
|
1629
|
+
let tokens = 0;
|
|
1630
|
+
const selected = [];
|
|
1631
|
+
for (const { chunk } of scored) {
|
|
1632
|
+
if (tokens + chunk.tokenEstimate > maxTokens) {
|
|
1633
|
+
continue;
|
|
1634
|
+
}
|
|
1635
|
+
selected.push(chunk);
|
|
1636
|
+
tokens += chunk.tokenEstimate;
|
|
1637
|
+
}
|
|
1638
|
+
if (selected.length === 0 && scored.length > 0) {
|
|
1639
|
+
this._lastSelectedCount = 1;
|
|
1640
|
+
const best = scored[0].chunk;
|
|
1641
|
+
const maxChars = maxTokens * 4;
|
|
1642
|
+
return `### ${best.heading}
|
|
1643
|
+
${best.content.slice(0, maxChars)}
|
|
1644
|
+
[... truncated]`;
|
|
1645
|
+
}
|
|
1646
|
+
this._lastSelectedCount = selected.length;
|
|
1647
|
+
return selected.map((c) => `### ${c.heading}
|
|
1648
|
+
${c.content}`).join("\n\n");
|
|
1649
|
+
}
|
|
1650
|
+
/** 全文を返す(フォールバック用) */
|
|
1651
|
+
getFull() {
|
|
1652
|
+
return this.fullText;
|
|
1653
|
+
}
|
|
1654
|
+
/** チャンク数を返す(テスト・デバッグ用) */
|
|
1655
|
+
get chunkCount() {
|
|
1656
|
+
return this.chunks.length;
|
|
1657
|
+
}
|
|
1658
|
+
/** 直近の selectRelevant() で選択されたチャンク数 */
|
|
1659
|
+
get lastSelectedCount() {
|
|
1660
|
+
return this._lastSelectedCount;
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
function parseMarkdownIntoChunks(markdown) {
|
|
1664
|
+
const lines = markdown.split("\n");
|
|
1665
|
+
const chunks = [];
|
|
1666
|
+
let currentHeading = "";
|
|
1667
|
+
let currentContent = [];
|
|
1668
|
+
for (const line of lines) {
|
|
1669
|
+
const headingMatch = line.match(/^#{1,3}\s+(.+)/);
|
|
1670
|
+
if (headingMatch) {
|
|
1671
|
+
if (currentContent.length > 0 || currentHeading) {
|
|
1672
|
+
const content2 = currentContent.join("\n").trim();
|
|
1673
|
+
if (content2) {
|
|
1674
|
+
chunks.push(buildChunk(currentHeading || "untitled", content2));
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
currentHeading = headingMatch[1].trim();
|
|
1678
|
+
currentContent = [];
|
|
1679
|
+
} else {
|
|
1680
|
+
currentContent.push(line);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const content = currentContent.join("\n").trim();
|
|
1684
|
+
if (content) {
|
|
1685
|
+
chunks.push(buildChunk(currentHeading || "untitled", content));
|
|
1686
|
+
}
|
|
1687
|
+
return chunks;
|
|
1688
|
+
}
|
|
1689
|
+
function buildChunk(heading, content) {
|
|
1690
|
+
const fullText = `${heading} ${content}`;
|
|
1691
|
+
return {
|
|
1692
|
+
heading,
|
|
1693
|
+
content,
|
|
1694
|
+
tokenEstimate: Math.ceil(fullText.length / 4),
|
|
1695
|
+
keywords: extractKeywords(fullText)
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function extractKeywords(text) {
|
|
1699
|
+
const normalized = text.toLowerCase();
|
|
1700
|
+
const words = /* @__PURE__ */ new Set();
|
|
1701
|
+
const urlPaths = normalized.match(/\/[a-z0-9_-]+/g);
|
|
1702
|
+
if (urlPaths) {
|
|
1703
|
+
for (const path of urlPaths) {
|
|
1704
|
+
words.add(path.replace("/", ""));
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
const englishWords = normalized.match(/[a-z][a-z0-9_-]{2,}/g);
|
|
1708
|
+
if (englishWords) {
|
|
1709
|
+
for (const w of englishWords) {
|
|
1710
|
+
if (!STOP_WORDS.has(w)) {
|
|
1711
|
+
words.add(w);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const japaneseWords = text.match(/[\u30A0-\u30FF]{2,}|[\u4E00-\u9FFF]{2,}/g);
|
|
1716
|
+
if (japaneseWords) {
|
|
1717
|
+
for (const w of japaneseWords) {
|
|
1718
|
+
words.add(w);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return [...words];
|
|
1722
|
+
}
|
|
1723
|
+
function keywordOverlapScore(queryTerms, chunkKeywords) {
|
|
1724
|
+
if (queryTerms.length === 0 || chunkKeywords.length === 0) return 0;
|
|
1725
|
+
let matchCount = 0;
|
|
1726
|
+
for (const qt of queryTerms) {
|
|
1727
|
+
for (const ck of chunkKeywords) {
|
|
1728
|
+
if (ck.includes(qt) || qt.includes(ck)) {
|
|
1729
|
+
matchCount++;
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return matchCount / queryTerms.length;
|
|
1735
|
+
}
|
|
1736
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1737
|
+
"the",
|
|
1738
|
+
"and",
|
|
1739
|
+
"for",
|
|
1740
|
+
"are",
|
|
1741
|
+
"but",
|
|
1742
|
+
"not",
|
|
1743
|
+
"you",
|
|
1744
|
+
"all",
|
|
1745
|
+
"can",
|
|
1746
|
+
"has",
|
|
1747
|
+
"her",
|
|
1748
|
+
"was",
|
|
1749
|
+
"one",
|
|
1750
|
+
"our",
|
|
1751
|
+
"out",
|
|
1752
|
+
"day",
|
|
1753
|
+
"had",
|
|
1754
|
+
"hot",
|
|
1755
|
+
"may",
|
|
1756
|
+
"who",
|
|
1757
|
+
"did",
|
|
1758
|
+
"get",
|
|
1759
|
+
"let",
|
|
1760
|
+
"say",
|
|
1761
|
+
"she",
|
|
1762
|
+
"too",
|
|
1763
|
+
"use",
|
|
1764
|
+
"that",
|
|
1765
|
+
"this",
|
|
1766
|
+
"with",
|
|
1767
|
+
"have",
|
|
1768
|
+
"from",
|
|
1769
|
+
"they",
|
|
1770
|
+
"been",
|
|
1771
|
+
"will",
|
|
1772
|
+
"each",
|
|
1773
|
+
"make",
|
|
1774
|
+
"like",
|
|
1775
|
+
"when",
|
|
1776
|
+
"than",
|
|
1777
|
+
"them",
|
|
1778
|
+
"into",
|
|
1779
|
+
"some",
|
|
1780
|
+
"could",
|
|
1781
|
+
"other",
|
|
1782
|
+
"about",
|
|
1783
|
+
"which",
|
|
1784
|
+
"their",
|
|
1785
|
+
"there",
|
|
1786
|
+
"would",
|
|
1787
|
+
"click",
|
|
1788
|
+
"input",
|
|
1789
|
+
"select",
|
|
1790
|
+
"button",
|
|
1791
|
+
"page",
|
|
1792
|
+
"form",
|
|
1793
|
+
"step"
|
|
1794
|
+
]);
|
|
1795
|
+
|
|
1796
|
+
// src/runbook-executor/executor.ts
|
|
1797
|
+
async function applySkillTransform(browser, rawSnapshot, url, skills, stepContext) {
|
|
1798
|
+
if (!skills || skills.length === 0) return null;
|
|
1799
|
+
const locale = getLocale();
|
|
1800
|
+
for (const skill of skills) {
|
|
1801
|
+
const shouldActivate = stepContext && skill.shouldActivateForStep ? skill.shouldActivateForStep(url, stepContext) : skill.shouldActivate(url);
|
|
1802
|
+
if (shouldActivate) {
|
|
1803
|
+
try {
|
|
1804
|
+
return await skill.transformSnapshot(browser, rawSnapshot, { url, locale });
|
|
1805
|
+
} catch {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
function mergeNotesIntoContext(contextMarkdown, notes) {
|
|
1813
|
+
if (!notes) return contextMarkdown;
|
|
1814
|
+
const section = `## Generation Notes (Human Guidance)
|
|
1815
|
+
${notes}`;
|
|
1816
|
+
return `${contextMarkdown}
|
|
1817
|
+
|
|
1818
|
+
${section}`;
|
|
1819
|
+
}
|
|
1820
|
+
function resolveContextMarkdown(config, runbook) {
|
|
1821
|
+
return mergeNotesIntoContext(config.contextMarkdown, runbook.notes);
|
|
1822
|
+
}
|
|
1823
|
+
async function execute(config, runbook, opts) {
|
|
1824
|
+
const totalStart = performance.now();
|
|
1825
|
+
const ownsBrowser = !opts.browser;
|
|
1826
|
+
const browser = opts.browser ?? new AgentBrowser();
|
|
1827
|
+
const results = [];
|
|
1828
|
+
let aborted = false;
|
|
1829
|
+
let recordedVideoPaths;
|
|
1830
|
+
const logger = opts.logger ?? new NoopLogger();
|
|
1831
|
+
const createSpinner = opts.createSpinner ?? (() => new NoopSpinner());
|
|
1832
|
+
const debugLogger = createDebugLogger({
|
|
1833
|
+
filePath: config.debugLogPath,
|
|
1834
|
+
console: config.debugConsole
|
|
1835
|
+
});
|
|
1836
|
+
const snapshotCache = new SnapshotCache();
|
|
1837
|
+
config.contextMarkdown = resolveContextMarkdown(config, runbook);
|
|
1838
|
+
const contextChunker = new ContextChunker(config.contextMarkdown);
|
|
1839
|
+
const failureRegistry = new FailureRegistry();
|
|
1840
|
+
const pauseMs = config.stepDelay ?? runbook.settings.pauseBetweenSteps;
|
|
1841
|
+
if (config.screenshotDir) {
|
|
1842
|
+
await mkdir2(config.screenshotDir, { recursive: true });
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
if (ownsBrowser) {
|
|
1846
|
+
const startUrl = opts.store.resolveTemplate(runbook.metadata.startUrl);
|
|
1847
|
+
const openSpinner = createSpinner();
|
|
1848
|
+
openSpinner.start(tf("executor.openingUrl", { url: startUrl }));
|
|
1849
|
+
await browser.open(startUrl, {
|
|
1850
|
+
headless: config.headless,
|
|
1851
|
+
stealth: config.stealth,
|
|
1852
|
+
proxy: config.proxy
|
|
1853
|
+
});
|
|
1854
|
+
await browser.waitForDOMStability(3e3);
|
|
1855
|
+
openSpinner.stop(t("executor.browserReady"));
|
|
1856
|
+
if (config.videoDir) {
|
|
1857
|
+
try {
|
|
1858
|
+
await browser.startRecording(config.videoDir);
|
|
1859
|
+
logger.info(tf("executor.recordingStarted", { path: config.videoDir }));
|
|
1860
|
+
} catch (e) {
|
|
1861
|
+
logger.warn(tf("executor.recordingStartFailed", { error: e instanceof Error ? e.message : String(e) }));
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
for (const step of runbook.steps) {
|
|
1866
|
+
if (aborted) break;
|
|
1867
|
+
if (opts.abortSignal?.aborted) {
|
|
1868
|
+
aborted = true;
|
|
1869
|
+
logger.warn(t("executor.jobAbortedTimeout"));
|
|
1870
|
+
break;
|
|
1871
|
+
}
|
|
1872
|
+
logger.step(`Step ${step.ordinal}/${runbook.steps.length}: ${step.description}`);
|
|
1873
|
+
await opts.onStepStart?.(step.ordinal, step.description, step.action.type).catch(() => {
|
|
1874
|
+
});
|
|
1875
|
+
const start = Date.now();
|
|
1876
|
+
if (step.condition) {
|
|
1877
|
+
const resolved = opts.store.resolveTemplate(step.condition);
|
|
1878
|
+
const evalResult = evaluateCondition(resolved);
|
|
1879
|
+
debugLogger.log({
|
|
1880
|
+
phase: "executor",
|
|
1881
|
+
event: "condition_eval",
|
|
1882
|
+
step: step.ordinal,
|
|
1883
|
+
data: { condition: step.condition, resolved, result: evalResult.value, error: evalResult.error }
|
|
1884
|
+
});
|
|
1885
|
+
if (!evalResult.value) {
|
|
1886
|
+
logger.warn(tf("executor.conditionSkipping", { condition: step.condition }));
|
|
1887
|
+
const skipDuration = Date.now() - start;
|
|
1888
|
+
results.push({
|
|
1889
|
+
ordinal: step.ordinal,
|
|
1890
|
+
description: step.description,
|
|
1891
|
+
actionType: step.action.type,
|
|
1892
|
+
status: "skipped",
|
|
1893
|
+
durationMs: skipDuration,
|
|
1894
|
+
conditionSkipped: true
|
|
1895
|
+
});
|
|
1896
|
+
await opts.onStepComplete?.(step.ordinal, "skipped", skipDuration, step.description, step.action.type).catch(() => {
|
|
1897
|
+
});
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
if (step.branches) {
|
|
1902
|
+
const branchResult = await executeBranchStep(
|
|
1903
|
+
browser,
|
|
1904
|
+
step,
|
|
1905
|
+
runbook,
|
|
1906
|
+
config,
|
|
1907
|
+
opts,
|
|
1908
|
+
debugLogger,
|
|
1909
|
+
pauseMs,
|
|
1910
|
+
logger,
|
|
1911
|
+
snapshotCache,
|
|
1912
|
+
contextChunker,
|
|
1913
|
+
failureRegistry
|
|
1914
|
+
);
|
|
1915
|
+
results.push(branchResult);
|
|
1916
|
+
if (branchResult.status === "failed" && runbook.settings.stopOnError) {
|
|
1917
|
+
logger.error(t("executor.stopOnErrorAborting"));
|
|
1918
|
+
aborted = true;
|
|
1919
|
+
}
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (step.loop && step.steps) {
|
|
1923
|
+
const loopResult = await executeLoopStep(
|
|
1924
|
+
browser,
|
|
1925
|
+
step,
|
|
1926
|
+
runbook,
|
|
1927
|
+
config,
|
|
1928
|
+
opts,
|
|
1929
|
+
debugLogger,
|
|
1930
|
+
pauseMs,
|
|
1931
|
+
logger,
|
|
1932
|
+
snapshotCache,
|
|
1933
|
+
contextChunker,
|
|
1934
|
+
failureRegistry
|
|
1935
|
+
);
|
|
1936
|
+
results.push(loopResult);
|
|
1937
|
+
if (loopResult.status === "failed" && runbook.settings.stopOnError) {
|
|
1938
|
+
logger.error(t("executor.stopOnErrorAborting"));
|
|
1939
|
+
aborted = true;
|
|
1940
|
+
}
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
if (needsConfirmation(step, config.skipConfirmation) && opts.confirmationProvider) {
|
|
1944
|
+
logger.info(tf("executor.approvalWaiting", { ordinal: step.ordinal, riskLevel: step.riskLevel }));
|
|
1945
|
+
debugLogger.log({
|
|
1946
|
+
phase: "executor",
|
|
1947
|
+
event: "approval_pending",
|
|
1948
|
+
step: step.ordinal,
|
|
1949
|
+
data: { riskLevel: step.riskLevel, description: step.description }
|
|
1950
|
+
});
|
|
1951
|
+
const result = await opts.confirmationProvider.confirm(step, step.ordinal, {
|
|
1952
|
+
goal: runbook.metadata.goal
|
|
1953
|
+
});
|
|
1954
|
+
debugLogger.log({
|
|
1955
|
+
phase: "executor",
|
|
1956
|
+
event: "approval_received",
|
|
1957
|
+
step: step.ordinal,
|
|
1958
|
+
data: { result }
|
|
1959
|
+
});
|
|
1960
|
+
if (result === "abort") {
|
|
1961
|
+
logger.error(tf("executor.approvalAborted", { ordinal: step.ordinal }));
|
|
1962
|
+
aborted = true;
|
|
1963
|
+
results.push({
|
|
1964
|
+
ordinal: step.ordinal,
|
|
1965
|
+
description: step.description,
|
|
1966
|
+
actionType: step.action.type,
|
|
1967
|
+
status: "skipped",
|
|
1968
|
+
durationMs: Date.now() - start,
|
|
1969
|
+
error: t("executor.userAborted")
|
|
1970
|
+
});
|
|
1971
|
+
await opts.onStepComplete?.(step.ordinal, "skipped", Date.now() - start, step.description, step.action.type).catch(() => {
|
|
1972
|
+
});
|
|
1973
|
+
break;
|
|
1974
|
+
}
|
|
1975
|
+
if (result === "skip") {
|
|
1976
|
+
logger.warn(tf("executor.approvalSkipped", { ordinal: step.ordinal }));
|
|
1977
|
+
results.push({
|
|
1978
|
+
ordinal: step.ordinal,
|
|
1979
|
+
description: step.description,
|
|
1980
|
+
actionType: step.action.type,
|
|
1981
|
+
status: "skipped",
|
|
1982
|
+
durationMs: Date.now() - start
|
|
1983
|
+
});
|
|
1984
|
+
await opts.onStepComplete?.(step.ordinal, "skipped", Date.now() - start, step.description, step.action.type).catch(() => {
|
|
1985
|
+
});
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
logger.success(tf("executor.approvalApproved", { ordinal: step.ordinal }));
|
|
1989
|
+
}
|
|
1990
|
+
try {
|
|
1991
|
+
const stepResult = await executeStep(
|
|
1992
|
+
browser,
|
|
1993
|
+
step,
|
|
1994
|
+
runbook,
|
|
1995
|
+
config,
|
|
1996
|
+
opts,
|
|
1997
|
+
debugLogger,
|
|
1998
|
+
logger,
|
|
1999
|
+
snapshotCache,
|
|
2000
|
+
contextChunker,
|
|
2001
|
+
failureRegistry
|
|
2002
|
+
);
|
|
2003
|
+
results.push({ ...stepResult, ordinal: step.ordinal, description: step.description, actionType: step.action.type });
|
|
2004
|
+
opts.store.updateWorkingMemory(
|
|
2005
|
+
step.ordinal,
|
|
2006
|
+
runbook.steps.length,
|
|
2007
|
+
stepResult.status === "success",
|
|
2008
|
+
step.url,
|
|
2009
|
+
step.action.type,
|
|
2010
|
+
step.description
|
|
2011
|
+
);
|
|
2012
|
+
if (stepResult.extractedData) {
|
|
2013
|
+
opts.store.set("extractedData", stepResult.extractedData);
|
|
2014
|
+
}
|
|
2015
|
+
if (stepResult.status === "failed" && runbook.settings.stopOnError) {
|
|
2016
|
+
logger.error(t("executor.stopOnErrorAborting"));
|
|
2017
|
+
aborted = true;
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
if (step.captures && step.captures.length > 0) {
|
|
2021
|
+
await executeCapturePhase(browser, step, opts.store, results, runbook, debugLogger, logger);
|
|
2022
|
+
if (results[results.length - 1].status === "failed" && runbook.settings.stopOnError) {
|
|
2023
|
+
aborted = true;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
if (step.memoryOperations && opts.dataStore) {
|
|
2027
|
+
await executeMemoryOperations(step, opts.store, opts.dataStore, debugLogger, logger);
|
|
2028
|
+
}
|
|
2029
|
+
if (config.screenshotDir) {
|
|
2030
|
+
const filename = `step-${String(step.ordinal).padStart(3, "0")}.png`;
|
|
2031
|
+
try {
|
|
2032
|
+
await browser.screenshot(join3(config.screenshotDir, filename));
|
|
2033
|
+
} catch {
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
const lastResult = results[results.length - 1];
|
|
2037
|
+
await opts.onStepComplete?.(step.ordinal, lastResult.status, lastResult.durationMs, step.description, step.action.type).catch(() => {
|
|
2038
|
+
});
|
|
2039
|
+
if (pauseMs > 0) {
|
|
2040
|
+
await new Promise((r) => setTimeout(r, pauseMs));
|
|
2041
|
+
}
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
const rawErrorMsg = error instanceof Error ? error.message : String(error);
|
|
2044
|
+
const errorMsg = sanitizeBrowserError(rawErrorMsg);
|
|
2045
|
+
logger.error(errorMsg);
|
|
2046
|
+
debugLogger.log({
|
|
2047
|
+
phase: "executor",
|
|
2048
|
+
event: "step_error",
|
|
2049
|
+
step: step.ordinal,
|
|
2050
|
+
data: { error: rawErrorMsg }
|
|
2051
|
+
});
|
|
2052
|
+
results.push({
|
|
2053
|
+
ordinal: step.ordinal,
|
|
2054
|
+
description: step.description,
|
|
2055
|
+
actionType: step.action.type,
|
|
2056
|
+
status: "failed",
|
|
2057
|
+
durationMs: Date.now() - start,
|
|
2058
|
+
error: errorMsg
|
|
2059
|
+
});
|
|
2060
|
+
await opts.onStepFailed?.(step.ordinal, errorMsg).catch(() => {
|
|
2061
|
+
});
|
|
2062
|
+
if (runbook.settings.stopOnError) {
|
|
2063
|
+
aborted = true;
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
} finally {
|
|
2069
|
+
if (ownsBrowser && browser.isRecording()) {
|
|
2070
|
+
try {
|
|
2071
|
+
const result = await browser.stopRecording();
|
|
2072
|
+
if (result.paths.length > 0) {
|
|
2073
|
+
recordedVideoPaths = result.paths;
|
|
2074
|
+
for (const p of result.paths) {
|
|
2075
|
+
logger.info(tf("executor.recordingComplete", { path: p }));
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
} catch (e) {
|
|
2079
|
+
logger.warn(tf("executor.recordingStopFailed", { error: e instanceof Error ? e.message : String(e) }));
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
if (ownsBrowser) {
|
|
2083
|
+
try {
|
|
2084
|
+
await browser.close();
|
|
2085
|
+
} catch {
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
const metricsSummary = getActiveMetricsCollector().getSummary();
|
|
2090
|
+
if (metricsSummary.totalCalls > 0) {
|
|
2091
|
+
debugLogger.log({
|
|
2092
|
+
phase: "executor",
|
|
2093
|
+
event: "ai_metrics_summary",
|
|
2094
|
+
data: metricsSummary
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
await debugLogger.flush();
|
|
2098
|
+
const succeeded = results.filter((r) => r.status === "success").length;
|
|
2099
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
2100
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
2101
|
+
const memoryCollections = opts.dataStore ? Object.fromEntries(
|
|
2102
|
+
opts.dataStore.listCollections().map((c) => [c, opts.dataStore.count(c)])
|
|
2103
|
+
) : void 0;
|
|
2104
|
+
const downloadedFiles = opts.downloadManager ? opts.downloadManager.getAllPaths() : void 0;
|
|
2105
|
+
return {
|
|
2106
|
+
runbookTitle: runbook.title,
|
|
2107
|
+
startUrl: runbook.metadata.startUrl,
|
|
2108
|
+
totalSteps: runbook.steps.length,
|
|
2109
|
+
executed: succeeded + failed,
|
|
2110
|
+
succeeded,
|
|
2111
|
+
failed,
|
|
2112
|
+
skipped,
|
|
2113
|
+
aborted,
|
|
2114
|
+
steps: results,
|
|
2115
|
+
totalDurationMs: Math.round(performance.now() - totalStart),
|
|
2116
|
+
...memoryCollections && Object.keys(memoryCollections).length > 0 ? { memoryCollections } : {},
|
|
2117
|
+
...downloadedFiles && downloadedFiles.length > 0 ? { downloadedFiles } : {},
|
|
2118
|
+
...recordedVideoPaths && recordedVideoPaths.length > 0 ? { videoPaths: recordedVideoPaths } : {}
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
async function executeCapturePhase(browser, step, store, results, runbook, debugLogger, logger) {
|
|
2122
|
+
if (!step.captures || step.captures.length === 0) return;
|
|
2123
|
+
const capturedValues = {};
|
|
2124
|
+
for (const capture of step.captures) {
|
|
2125
|
+
const value = await executeCapture(browser, capture, store);
|
|
2126
|
+
if (value !== null) {
|
|
2127
|
+
store.set(capture.name, value);
|
|
2128
|
+
const maskedValue = store.isSensitive(capture.name) ? "****" : value;
|
|
2129
|
+
capturedValues[capture.name] = maskedValue;
|
|
2130
|
+
logger.success(`Captured: ${capture.name} = "${maskedValue}"`);
|
|
2131
|
+
debugLogger.log({
|
|
2132
|
+
phase: "executor",
|
|
2133
|
+
event: "capture",
|
|
2134
|
+
step: step.ordinal,
|
|
2135
|
+
data: { name: capture.name, strategy: capture.strategy, value: maskedValue }
|
|
2136
|
+
});
|
|
2137
|
+
} else if (capture.required) {
|
|
2138
|
+
logger.error(`Required capture "${capture.name}" failed`);
|
|
2139
|
+
results[results.length - 1].status = "failed";
|
|
2140
|
+
results[results.length - 1].error = `Required capture "${capture.name}" failed`;
|
|
2141
|
+
} else {
|
|
2142
|
+
logger.warn(`Optional capture "${capture.name}" returned no value`);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (Object.keys(capturedValues).length > 0) {
|
|
2146
|
+
results[results.length - 1].capturedValues = capturedValues;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
async function executeLoopStep(browser, step, runbook, config, opts, debugLogger, pauseMs, logger, snapshotCache, contextChunker, failureRegistry) {
|
|
2150
|
+
const loop = step.loop;
|
|
2151
|
+
if (loop.forEach) {
|
|
2152
|
+
return executeForEachLoop(
|
|
2153
|
+
browser,
|
|
2154
|
+
step,
|
|
2155
|
+
runbook,
|
|
2156
|
+
config,
|
|
2157
|
+
opts,
|
|
2158
|
+
debugLogger,
|
|
2159
|
+
pauseMs,
|
|
2160
|
+
logger,
|
|
2161
|
+
snapshotCache,
|
|
2162
|
+
contextChunker,
|
|
2163
|
+
failureRegistry
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
const start = Date.now();
|
|
2167
|
+
const store = opts.store;
|
|
2168
|
+
const subSteps = step.steps;
|
|
2169
|
+
const maxIter = loop.maxIterations ?? 10;
|
|
2170
|
+
const counterVar = loop.counterVariable ?? "__loopIndex";
|
|
2171
|
+
const allIterResults = [];
|
|
2172
|
+
let iterations = 0;
|
|
2173
|
+
let loopFailed = false;
|
|
2174
|
+
debugLogger.log({
|
|
2175
|
+
phase: "executor",
|
|
2176
|
+
event: "loop_start",
|
|
2177
|
+
step: step.ordinal,
|
|
2178
|
+
data: { condition: loop.condition, maxIterations: maxIter, counterVariable: counterVar }
|
|
2179
|
+
});
|
|
2180
|
+
for (let i = 0; i < maxIter; i++) {
|
|
2181
|
+
if (opts.abortSignal?.aborted) {
|
|
2182
|
+
loopFailed = true;
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
2185
|
+
const resolved = store.resolveTemplate(loop.condition);
|
|
2186
|
+
const evalResult = evaluateCondition(resolved);
|
|
2187
|
+
debugLogger.log({
|
|
2188
|
+
phase: "executor",
|
|
2189
|
+
event: "loop_condition",
|
|
2190
|
+
step: step.ordinal,
|
|
2191
|
+
data: { iteration: i, condition: loop.condition, resolved, result: evalResult.value }
|
|
2192
|
+
});
|
|
2193
|
+
if (!evalResult.value) {
|
|
2194
|
+
logger.info(`Loop condition false at iteration ${i}, exiting loop`);
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
store.set(counterVar, String(i));
|
|
2198
|
+
iterations++;
|
|
2199
|
+
logger.step(`Loop ${step.ordinal} iteration ${i}/${maxIter}`);
|
|
2200
|
+
const { results: iterResults, failed } = await executeSubSteps(
|
|
2201
|
+
browser,
|
|
2202
|
+
subSteps,
|
|
2203
|
+
runbook,
|
|
2204
|
+
config,
|
|
2205
|
+
opts,
|
|
2206
|
+
debugLogger,
|
|
2207
|
+
pauseMs,
|
|
2208
|
+
logger,
|
|
2209
|
+
snapshotCache,
|
|
2210
|
+
contextChunker,
|
|
2211
|
+
failureRegistry
|
|
2212
|
+
);
|
|
2213
|
+
allIterResults.push(iterResults);
|
|
2214
|
+
if (failed) {
|
|
2215
|
+
loopFailed = true;
|
|
2216
|
+
break;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
debugLogger.log({
|
|
2220
|
+
phase: "executor",
|
|
2221
|
+
event: "loop_end",
|
|
2222
|
+
step: step.ordinal,
|
|
2223
|
+
data: { iterations, failed: loopFailed }
|
|
2224
|
+
});
|
|
2225
|
+
return {
|
|
2226
|
+
ordinal: step.ordinal,
|
|
2227
|
+
description: step.description,
|
|
2228
|
+
actionType: "loop",
|
|
2229
|
+
status: loopFailed ? "failed" : "success",
|
|
2230
|
+
durationMs: Date.now() - start,
|
|
2231
|
+
loopIterations: iterations,
|
|
2232
|
+
subStepResults: allIterResults
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
async function executeForEachLoop(browser, step, runbook, config, opts, debugLogger, pauseMs, logger, snapshotCache, contextChunker, failureRegistry) {
|
|
2236
|
+
const start = Date.now();
|
|
2237
|
+
const store = opts.store;
|
|
2238
|
+
const loop = step.loop;
|
|
2239
|
+
const subSteps = step.steps;
|
|
2240
|
+
const maxIter = loop.maxIterations ?? 100;
|
|
2241
|
+
const itemVar = loop.itemVariable ?? "__item";
|
|
2242
|
+
const indexVar = loop.indexVariable ?? "__index";
|
|
2243
|
+
const rawForEach = store.resolveTemplate(loop.forEach);
|
|
2244
|
+
let items;
|
|
2245
|
+
if (rawForEach.startsWith("collection:")) {
|
|
2246
|
+
const collectionName = rawForEach.slice("collection:".length);
|
|
2247
|
+
if (!opts.dataStore) {
|
|
2248
|
+
return {
|
|
2249
|
+
ordinal: step.ordinal,
|
|
2250
|
+
description: step.description,
|
|
2251
|
+
actionType: "forEach",
|
|
2252
|
+
status: "failed",
|
|
2253
|
+
durationMs: Date.now() - start,
|
|
2254
|
+
error: `forEach collection: requires dataStore (collection: ${collectionName})`
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
items = opts.dataStore.getAll(collectionName);
|
|
2258
|
+
} else {
|
|
2259
|
+
try {
|
|
2260
|
+
const parsed = JSON.parse(rawForEach);
|
|
2261
|
+
if (!Array.isArray(parsed)) {
|
|
2262
|
+
return {
|
|
2263
|
+
ordinal: step.ordinal,
|
|
2264
|
+
description: step.description,
|
|
2265
|
+
actionType: "forEach",
|
|
2266
|
+
status: "failed",
|
|
2267
|
+
durationMs: Date.now() - start,
|
|
2268
|
+
error: `forEach value must be a JSON array, got ${typeof parsed}`
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
items = parsed;
|
|
2272
|
+
} catch {
|
|
2273
|
+
return {
|
|
2274
|
+
ordinal: step.ordinal,
|
|
2275
|
+
description: step.description,
|
|
2276
|
+
actionType: "forEach",
|
|
2277
|
+
status: "failed",
|
|
2278
|
+
durationMs: Date.now() - start,
|
|
2279
|
+
error: `forEach value is not valid JSON: ${rawForEach.slice(0, 100)}`
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
const effectiveItems = items.slice(0, maxIter);
|
|
2284
|
+
debugLogger.log({
|
|
2285
|
+
phase: "executor",
|
|
2286
|
+
event: "forEach_start",
|
|
2287
|
+
step: step.ordinal,
|
|
2288
|
+
data: {
|
|
2289
|
+
forEach: loop.forEach,
|
|
2290
|
+
itemCount: items.length,
|
|
2291
|
+
effectiveCount: effectiveItems.length,
|
|
2292
|
+
maxIterations: maxIter,
|
|
2293
|
+
itemVariable: itemVar,
|
|
2294
|
+
indexVariable: indexVar
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
const allIterResults = [];
|
|
2298
|
+
let loopFailed = false;
|
|
2299
|
+
for (let i = 0; i < effectiveItems.length; i++) {
|
|
2300
|
+
if (opts.abortSignal?.aborted) {
|
|
2301
|
+
loopFailed = true;
|
|
2302
|
+
break;
|
|
2303
|
+
}
|
|
2304
|
+
store.setForEachItem(itemVar, effectiveItems[i], indexVar, i);
|
|
2305
|
+
logger.step(`forEach ${step.ordinal} [${i + 1}/${effectiveItems.length}]`);
|
|
2306
|
+
const { results: iterResults, failed } = await executeSubSteps(
|
|
2307
|
+
browser,
|
|
2308
|
+
subSteps,
|
|
2309
|
+
runbook,
|
|
2310
|
+
config,
|
|
2311
|
+
opts,
|
|
2312
|
+
debugLogger,
|
|
2313
|
+
pauseMs,
|
|
2314
|
+
logger,
|
|
2315
|
+
snapshotCache,
|
|
2316
|
+
contextChunker,
|
|
2317
|
+
failureRegistry
|
|
2318
|
+
);
|
|
2319
|
+
allIterResults.push(iterResults);
|
|
2320
|
+
if (failed) {
|
|
2321
|
+
loopFailed = true;
|
|
2322
|
+
break;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
store.clearForEachItem(itemVar, indexVar);
|
|
2326
|
+
debugLogger.log({
|
|
2327
|
+
phase: "executor",
|
|
2328
|
+
event: "forEach_end",
|
|
2329
|
+
step: step.ordinal,
|
|
2330
|
+
data: { iterations: allIterResults.length, failed: loopFailed }
|
|
2331
|
+
});
|
|
2332
|
+
return {
|
|
2333
|
+
ordinal: step.ordinal,
|
|
2334
|
+
description: step.description,
|
|
2335
|
+
actionType: "forEach",
|
|
2336
|
+
status: loopFailed ? "failed" : "success",
|
|
2337
|
+
durationMs: Date.now() - start,
|
|
2338
|
+
loopIterations: allIterResults.length,
|
|
2339
|
+
forEachItemCount: items.length,
|
|
2340
|
+
subStepResults: allIterResults
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
async function executeSubSteps(browser, subSteps, runbook, config, opts, debugLogger, pauseMs, logger, snapshotCache, contextChunker, failureRegistry) {
|
|
2344
|
+
const store = opts.store;
|
|
2345
|
+
const results = [];
|
|
2346
|
+
let failed = false;
|
|
2347
|
+
for (const subStep of subSteps) {
|
|
2348
|
+
if (subStep.condition) {
|
|
2349
|
+
const subResolved = store.resolveTemplate(subStep.condition);
|
|
2350
|
+
const subEval = evaluateCondition(subResolved);
|
|
2351
|
+
if (!subEval.value) {
|
|
2352
|
+
logger.warn(` Sub-step ${subStep.ordinal} condition false, skipping`);
|
|
2353
|
+
results.push({
|
|
2354
|
+
ordinal: subStep.ordinal,
|
|
2355
|
+
description: subStep.description,
|
|
2356
|
+
actionType: subStep.action.type,
|
|
2357
|
+
status: "skipped",
|
|
2358
|
+
durationMs: 0,
|
|
2359
|
+
conditionSkipped: true
|
|
2360
|
+
});
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (subStep.branches) {
|
|
2365
|
+
const branchResult = await executeBranchStep(
|
|
2366
|
+
browser,
|
|
2367
|
+
subStep,
|
|
2368
|
+
runbook,
|
|
2369
|
+
config,
|
|
2370
|
+
opts,
|
|
2371
|
+
debugLogger,
|
|
2372
|
+
pauseMs,
|
|
2373
|
+
logger,
|
|
2374
|
+
snapshotCache,
|
|
2375
|
+
contextChunker,
|
|
2376
|
+
failureRegistry
|
|
2377
|
+
);
|
|
2378
|
+
results.push(branchResult);
|
|
2379
|
+
if (branchResult.status === "failed" && runbook.settings.stopOnError) {
|
|
2380
|
+
failed = true;
|
|
2381
|
+
break;
|
|
2382
|
+
}
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
if (subStep.loop && subStep.steps) {
|
|
2386
|
+
const loopResult = await executeLoopStep(
|
|
2387
|
+
browser,
|
|
2388
|
+
subStep,
|
|
2389
|
+
runbook,
|
|
2390
|
+
config,
|
|
2391
|
+
opts,
|
|
2392
|
+
debugLogger,
|
|
2393
|
+
pauseMs,
|
|
2394
|
+
logger,
|
|
2395
|
+
snapshotCache,
|
|
2396
|
+
contextChunker,
|
|
2397
|
+
failureRegistry
|
|
2398
|
+
);
|
|
2399
|
+
results.push(loopResult);
|
|
2400
|
+
if (loopResult.status === "failed" && runbook.settings.stopOnError) {
|
|
2401
|
+
failed = true;
|
|
2402
|
+
break;
|
|
2403
|
+
}
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
await opts.onStepStart?.(subStep.ordinal, subStep.description, subStep.action.type).catch(() => {
|
|
2407
|
+
});
|
|
2408
|
+
try {
|
|
2409
|
+
const subResult = await executeStep(
|
|
2410
|
+
browser,
|
|
2411
|
+
subStep,
|
|
2412
|
+
runbook,
|
|
2413
|
+
config,
|
|
2414
|
+
opts,
|
|
2415
|
+
debugLogger,
|
|
2416
|
+
logger,
|
|
2417
|
+
snapshotCache,
|
|
2418
|
+
contextChunker,
|
|
2419
|
+
failureRegistry
|
|
2420
|
+
);
|
|
2421
|
+
results.push({
|
|
2422
|
+
...subResult,
|
|
2423
|
+
ordinal: subStep.ordinal,
|
|
2424
|
+
description: subStep.description,
|
|
2425
|
+
actionType: subStep.action.type
|
|
2426
|
+
});
|
|
2427
|
+
store.updateWorkingMemory(
|
|
2428
|
+
subStep.ordinal,
|
|
2429
|
+
runbook.steps.length,
|
|
2430
|
+
subResult.status === "success",
|
|
2431
|
+
subStep.url,
|
|
2432
|
+
subStep.action.type,
|
|
2433
|
+
subStep.description
|
|
2434
|
+
);
|
|
2435
|
+
if (subResult.extractedData) {
|
|
2436
|
+
store.set("extractedData", subResult.extractedData);
|
|
2437
|
+
}
|
|
2438
|
+
if (subResult.status === "failed" && runbook.settings.stopOnError) {
|
|
2439
|
+
failed = true;
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
2442
|
+
if (subStep.captures && subStep.captures.length > 0) {
|
|
2443
|
+
await executeCapturePhase(
|
|
2444
|
+
browser,
|
|
2445
|
+
subStep,
|
|
2446
|
+
store,
|
|
2447
|
+
results,
|
|
2448
|
+
runbook,
|
|
2449
|
+
debugLogger,
|
|
2450
|
+
logger
|
|
2451
|
+
);
|
|
2452
|
+
if (results[results.length - 1].status === "failed" && runbook.settings.stopOnError) {
|
|
2453
|
+
failed = true;
|
|
2454
|
+
break;
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
if (subStep.memoryOperations && opts.dataStore) {
|
|
2458
|
+
await executeMemoryOperations(subStep, store, opts.dataStore, debugLogger, logger);
|
|
2459
|
+
}
|
|
2460
|
+
const lastSubResult = results[results.length - 1];
|
|
2461
|
+
await opts.onStepComplete?.(subStep.ordinal, lastSubResult.status, lastSubResult.durationMs, subStep.description, subStep.action.type).catch(() => {
|
|
2462
|
+
});
|
|
2463
|
+
if (pauseMs > 0) {
|
|
2464
|
+
await new Promise((r) => setTimeout(r, pauseMs));
|
|
2465
|
+
}
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2468
|
+
logger.error(errorMsg);
|
|
2469
|
+
results.push({
|
|
2470
|
+
ordinal: subStep.ordinal,
|
|
2471
|
+
description: subStep.description,
|
|
2472
|
+
actionType: subStep.action.type,
|
|
2473
|
+
status: "failed",
|
|
2474
|
+
durationMs: 0,
|
|
2475
|
+
error: errorMsg
|
|
2476
|
+
});
|
|
2477
|
+
await opts.onStepFailed?.(subStep.ordinal, errorMsg).catch(() => {
|
|
2478
|
+
});
|
|
2479
|
+
if (runbook.settings.stopOnError) {
|
|
2480
|
+
failed = true;
|
|
2481
|
+
break;
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
return { results, failed };
|
|
2486
|
+
}
|
|
2487
|
+
async function executeBranchStep(browser, step, runbook, config, opts, debugLogger, pauseMs, logger, snapshotCache, contextChunker, failureRegistry) {
|
|
2488
|
+
const start = Date.now();
|
|
2489
|
+
const store = opts.store;
|
|
2490
|
+
const branches = step.branches;
|
|
2491
|
+
const resolvedValue = store.resolveTemplate(branches.value);
|
|
2492
|
+
debugLogger.log({
|
|
2493
|
+
phase: "executor",
|
|
2494
|
+
event: "branch_start",
|
|
2495
|
+
step: step.ordinal,
|
|
2496
|
+
data: { value: branches.value, resolved: resolvedValue, caseCount: branches.cases.length }
|
|
2497
|
+
});
|
|
2498
|
+
let matchedSteps = null;
|
|
2499
|
+
let matchLabel = "";
|
|
2500
|
+
for (const branchCase of branches.cases) {
|
|
2501
|
+
const resolvedMatch = store.resolveTemplate(branchCase.match);
|
|
2502
|
+
if (resolvedValue === resolvedMatch) {
|
|
2503
|
+
matchedSteps = branchCase.steps;
|
|
2504
|
+
matchLabel = branchCase.match;
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (!matchedSteps && branches.default) {
|
|
2509
|
+
matchedSteps = branches.default.steps;
|
|
2510
|
+
matchLabel = "default";
|
|
2511
|
+
}
|
|
2512
|
+
if (!matchedSteps) {
|
|
2513
|
+
logger.warn(`Branch: no matching case for "${resolvedValue}", skipping`);
|
|
2514
|
+
debugLogger.log({
|
|
2515
|
+
phase: "executor",
|
|
2516
|
+
event: "branch_no_match",
|
|
2517
|
+
step: step.ordinal,
|
|
2518
|
+
data: { value: resolvedValue }
|
|
2519
|
+
});
|
|
2520
|
+
return {
|
|
2521
|
+
ordinal: step.ordinal,
|
|
2522
|
+
description: step.description,
|
|
2523
|
+
actionType: "branch",
|
|
2524
|
+
status: "skipped",
|
|
2525
|
+
durationMs: Date.now() - start,
|
|
2526
|
+
branchMatch: void 0
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
logger.step(`Branch: matched "${matchLabel}" (value="${resolvedValue}")`);
|
|
2530
|
+
debugLogger.log({
|
|
2531
|
+
phase: "executor",
|
|
2532
|
+
event: "branch_matched",
|
|
2533
|
+
step: step.ordinal,
|
|
2534
|
+
data: { value: resolvedValue, match: matchLabel, subStepCount: matchedSteps.length }
|
|
2535
|
+
});
|
|
2536
|
+
const { results: subResults, failed } = await executeSubSteps(
|
|
2537
|
+
browser,
|
|
2538
|
+
matchedSteps,
|
|
2539
|
+
runbook,
|
|
2540
|
+
config,
|
|
2541
|
+
opts,
|
|
2542
|
+
debugLogger,
|
|
2543
|
+
pauseMs,
|
|
2544
|
+
logger,
|
|
2545
|
+
snapshotCache,
|
|
2546
|
+
contextChunker,
|
|
2547
|
+
failureRegistry
|
|
2548
|
+
);
|
|
2549
|
+
debugLogger.log({
|
|
2550
|
+
phase: "executor",
|
|
2551
|
+
event: "branch_end",
|
|
2552
|
+
step: step.ordinal,
|
|
2553
|
+
data: { match: matchLabel, failed }
|
|
2554
|
+
});
|
|
2555
|
+
return {
|
|
2556
|
+
ordinal: step.ordinal,
|
|
2557
|
+
description: step.description,
|
|
2558
|
+
actionType: "branch",
|
|
2559
|
+
status: failed ? "failed" : "success",
|
|
2560
|
+
durationMs: Date.now() - start,
|
|
2561
|
+
branchMatch: matchLabel,
|
|
2562
|
+
subStepResults: [subResults]
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
async function executeStep(browser, step, runbook, config, opts, debugLogger, logger, snapshotCache, contextChunker, failureRegistry) {
|
|
2566
|
+
const start = Date.now();
|
|
2567
|
+
if (opts.skills?.length && step.url) {
|
|
2568
|
+
const stepContext = {
|
|
2569
|
+
actionType: step.action.type,
|
|
2570
|
+
memoryOperations: step.memoryOperations
|
|
2571
|
+
};
|
|
2572
|
+
const hasActiveSkill = opts.skills.some(
|
|
2573
|
+
(s) => s.shouldActivateForStep ? s.shouldActivateForStep(step.url, stepContext) : s.shouldActivate(step.url)
|
|
2574
|
+
);
|
|
2575
|
+
if (hasActiveSkill) {
|
|
2576
|
+
try {
|
|
2577
|
+
const snap = await browser.snapshot();
|
|
2578
|
+
const skillResult = await applySkillTransform(browser, snap, step.url, opts.skills, stepContext);
|
|
2579
|
+
if (skillResult?.extractedData) {
|
|
2580
|
+
opts.store.set("extractedData", skillResult.extractedData);
|
|
2581
|
+
}
|
|
2582
|
+
} catch {
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
if (step.action.type === "navigate") {
|
|
2587
|
+
const rawUrl = step.action.url ?? step.url;
|
|
2588
|
+
const url = opts.store.resolveTemplate(rawUrl);
|
|
2589
|
+
logger.step(`Navigate: ${opts.store.maskSensitive(url)}`);
|
|
2590
|
+
const maxNavRetries = 2;
|
|
2591
|
+
for (let navAttempt = 0; ; navAttempt++) {
|
|
2592
|
+
await browser.navigate(url);
|
|
2593
|
+
const pageCount = await browser.pageCount();
|
|
2594
|
+
await browser.waitForPossibleNavigation(await browser.url(), pageCount);
|
|
2595
|
+
const currentUrl = await browser.url();
|
|
2596
|
+
if (!currentUrl.startsWith("chrome-error://")) break;
|
|
2597
|
+
if (navAttempt >= maxNavRetries) {
|
|
2598
|
+
logger.warn(tf("executor.chromeErrorDetected", { url }));
|
|
2599
|
+
throw new Error(`Navigation failed: page landed on chrome-error:// (target: ${url})`);
|
|
2600
|
+
}
|
|
2601
|
+
logger.warn(tf("executor.chromeErrorDetected", { url }) + ` \u2014 retrying (${navAttempt + 1}/${maxNavRetries})`);
|
|
2602
|
+
await sleep(2e3 * (navAttempt + 1));
|
|
2603
|
+
}
|
|
2604
|
+
return { status: "success", durationMs: Date.now() - start };
|
|
2605
|
+
}
|
|
2606
|
+
if (step.action.type === "scroll") {
|
|
2607
|
+
logger.step("Scroll: down");
|
|
2608
|
+
await browser.scroll("down");
|
|
2609
|
+
return { status: "success", durationMs: Date.now() - start };
|
|
2610
|
+
}
|
|
2611
|
+
if (step.action.type === "wait") {
|
|
2612
|
+
const rawMs = step.action.value ? opts.store.resolveTemplate(step.action.value) : String(runbook.settings.defaultTimeout);
|
|
2613
|
+
const ms = Number(rawMs);
|
|
2614
|
+
logger.step(`Wait: ${ms}ms`);
|
|
2615
|
+
await browser.wait(ms);
|
|
2616
|
+
return { status: "success", durationMs: Date.now() - start };
|
|
2617
|
+
}
|
|
2618
|
+
if (step.action.type === "extract") {
|
|
2619
|
+
const script = opts.store.resolveTemplate(step.action.script ?? step.action.value ?? "");
|
|
2620
|
+
if (!script) {
|
|
2621
|
+
return { status: "failed", durationMs: Date.now() - start, error: "extract requires script or value" };
|
|
2622
|
+
}
|
|
2623
|
+
logger.step(`Extract: executing script`);
|
|
2624
|
+
debugLogger.log({
|
|
2625
|
+
phase: "executor",
|
|
2626
|
+
event: "extract",
|
|
2627
|
+
step: step.ordinal,
|
|
2628
|
+
data: { script: script.slice(0, 200) }
|
|
2629
|
+
});
|
|
2630
|
+
const result = await browser.evaluate(script);
|
|
2631
|
+
const isArray = Array.isArray(result);
|
|
2632
|
+
const stringifyApplied = typeof result !== "string" && result !== null && result !== void 0;
|
|
2633
|
+
const resultStr = result === null || result === void 0 ? "" : typeof result === "string" ? result : JSON.stringify(result);
|
|
2634
|
+
debugLogger.log({
|
|
2635
|
+
phase: "executor",
|
|
2636
|
+
event: "extract_result",
|
|
2637
|
+
step: step.ordinal,
|
|
2638
|
+
data: {
|
|
2639
|
+
resultType: result === null ? "null" : typeof result,
|
|
2640
|
+
isArray,
|
|
2641
|
+
arrayLength: isArray ? result.length : void 0,
|
|
2642
|
+
resultPreview: resultStr.slice(0, 200),
|
|
2643
|
+
storedLength: resultStr.length,
|
|
2644
|
+
stringifyApplied
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
if (resultStr === "") {
|
|
2648
|
+
logger.warn("Extract returned empty result");
|
|
2649
|
+
debugLogger.log({
|
|
2650
|
+
phase: "executor",
|
|
2651
|
+
event: "extract_empty_result",
|
|
2652
|
+
step: step.ordinal,
|
|
2653
|
+
data: { script: script.slice(0, 200) }
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
return { status: "success", durationMs: Date.now() - start, extractedData: resultStr };
|
|
2657
|
+
}
|
|
2658
|
+
if (step.action.type === "export") {
|
|
2659
|
+
if (!opts.dataStore) {
|
|
2660
|
+
return { status: "failed", durationMs: Date.now() - start, error: "export requires dataStore" };
|
|
2661
|
+
}
|
|
2662
|
+
const collection = opts.store.resolveTemplate(step.action.exportCollection ?? "default");
|
|
2663
|
+
const format = step.action.exportFormat ?? "csv";
|
|
2664
|
+
const outputDir = config.outputDir ?? "/tmp";
|
|
2665
|
+
const exportPath = opts.store.resolveTemplate(
|
|
2666
|
+
step.action.exportPath ?? `${outputDir}/${collection}.${format}`
|
|
2667
|
+
);
|
|
2668
|
+
logger.step(`Export: ${collection} \u2192 ${exportPath} (${format})`);
|
|
2669
|
+
const exportItems = opts.dataStore.getAll(collection);
|
|
2670
|
+
const sampleKeys = exportItems.length > 0 ? JSON.stringify(Object.keys(exportItems[0])) : "N/A";
|
|
2671
|
+
debugLogger.log({
|
|
2672
|
+
phase: "executor",
|
|
2673
|
+
event: "export",
|
|
2674
|
+
step: step.ordinal,
|
|
2675
|
+
data: {
|
|
2676
|
+
collection,
|
|
2677
|
+
itemCount: exportItems.length,
|
|
2678
|
+
format,
|
|
2679
|
+
path: exportPath,
|
|
2680
|
+
sampleKeys
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
await opts.dataStore.writeToFile(collection, exportPath, format);
|
|
2684
|
+
if (opts.downloadManager) {
|
|
2685
|
+
opts.downloadManager.addDownload({
|
|
2686
|
+
path: exportPath,
|
|
2687
|
+
filename: exportPath.split("/").pop() ?? "unknown",
|
|
2688
|
+
stepOrdinal: step.ordinal,
|
|
2689
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
return { status: "success", durationMs: Date.now() - start, exportedFile: exportPath };
|
|
2693
|
+
}
|
|
2694
|
+
if (step.action.type === "download" && !step.action.selector) {
|
|
2695
|
+
const downloadPath = opts.store.resolveTemplate(
|
|
2696
|
+
step.action.downloadPath ?? `/tmp/download-${Date.now()}.bin`
|
|
2697
|
+
);
|
|
2698
|
+
logger.step(`Download: waiting for download \u2192 ${downloadPath}`);
|
|
2699
|
+
await browser.waitForDownload(downloadPath);
|
|
2700
|
+
if (opts.downloadManager) {
|
|
2701
|
+
opts.downloadManager.addDownload({
|
|
2702
|
+
path: downloadPath,
|
|
2703
|
+
filename: downloadPath.split("/").pop() ?? "unknown",
|
|
2704
|
+
stepOrdinal: step.ordinal,
|
|
2705
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
return { status: "success", durationMs: Date.now() - start, downloadedFile: downloadPath };
|
|
2709
|
+
}
|
|
2710
|
+
if (step.action.type === "memory") {
|
|
2711
|
+
logger.step(`Memory operation: ${step.description}`);
|
|
2712
|
+
return { status: "success", durationMs: Date.now() - start };
|
|
2713
|
+
}
|
|
2714
|
+
if (step.action.type === "key") {
|
|
2715
|
+
const keys = opts.store.resolveTemplate(step.action.keys ?? step.action.value ?? "");
|
|
2716
|
+
if (!keys) {
|
|
2717
|
+
return { status: "failed", durationMs: Date.now() - start, error: "key action requires keys or value" };
|
|
2718
|
+
}
|
|
2719
|
+
logger.step(`Key: ${keys}`);
|
|
2720
|
+
await browser.pressKeys(keys);
|
|
2721
|
+
return { status: "success", durationMs: Date.now() - start };
|
|
2722
|
+
}
|
|
2723
|
+
if (step.memoryOperations && step.memoryOperations.length > 0 && !step.action.selector) {
|
|
2724
|
+
logger.warn(
|
|
2725
|
+
`Step has memoryOperations but action type "${step.action.type}" with no selector \u2014 treating as memory-only`
|
|
2726
|
+
);
|
|
2727
|
+
debugLogger.log({
|
|
2728
|
+
phase: "executor",
|
|
2729
|
+
event: "memory_fallback",
|
|
2730
|
+
step: step.ordinal,
|
|
2731
|
+
data: { originalActionType: step.action.type, memoryOpsCount: step.memoryOperations.length }
|
|
2732
|
+
});
|
|
2733
|
+
return { status: "success", durationMs: Date.now() - start };
|
|
2734
|
+
}
|
|
2735
|
+
if (!step.action.selector) {
|
|
2736
|
+
return {
|
|
2737
|
+
status: "failed",
|
|
2738
|
+
durationMs: Date.now() - start,
|
|
2739
|
+
error: "No selector defined for this step",
|
|
2740
|
+
diagnostics: {
|
|
2741
|
+
stepAction: {
|
|
2742
|
+
type: step.action.type,
|
|
2743
|
+
value: step.action.value,
|
|
2744
|
+
url: step.action.url
|
|
2745
|
+
},
|
|
2746
|
+
stepUrl: step.url
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
const defaultStrategy = {
|
|
2751
|
+
maxRetries: 3,
|
|
2752
|
+
changeTimeouts: [8e3, 1e4],
|
|
2753
|
+
finalRetryStabilizeMs: 3e3,
|
|
2754
|
+
domStabilityMs: 5e3
|
|
2755
|
+
};
|
|
2756
|
+
const strategy = config.executionStrategy ?? defaultStrategy;
|
|
2757
|
+
const { maxRetries } = strategy;
|
|
2758
|
+
let aiResult = null;
|
|
2759
|
+
let lastSnapshot = "";
|
|
2760
|
+
let lastRefs;
|
|
2761
|
+
let prevSnapshot = "";
|
|
2762
|
+
let lastAiResponseText = "";
|
|
2763
|
+
let lastAiPrompt = "";
|
|
2764
|
+
let attempts = 0;
|
|
2765
|
+
const failureHistory = [];
|
|
2766
|
+
const retryDetails = [];
|
|
2767
|
+
let cachedRefUsed = false;
|
|
2768
|
+
let deterministicResolveResult = "";
|
|
2769
|
+
let agentFallbackResult;
|
|
2770
|
+
let visionFallbackResult;
|
|
2771
|
+
let resolveMethod = null;
|
|
2772
|
+
let deterministicMatchType;
|
|
2773
|
+
let deterministicConfidence;
|
|
2774
|
+
let failureHintsProvided = false;
|
|
2775
|
+
if (opts.selectorCache && step.action.selector) {
|
|
2776
|
+
const cacheKey = buildCacheKey(step.ordinal, step.url, step.action.selector);
|
|
2777
|
+
const cached = lookupCache(opts.selectorCache, cacheKey);
|
|
2778
|
+
if (cached) {
|
|
2779
|
+
cachedRefUsed = true;
|
|
2780
|
+
const snapshotResult = await browser.snapshotWithRefs();
|
|
2781
|
+
const snapshot = snapshotResult.tree;
|
|
2782
|
+
lastSnapshot = snapshot;
|
|
2783
|
+
lastRefs = snapshotResult.refs;
|
|
2784
|
+
prevSnapshot = snapshot;
|
|
2785
|
+
if (findElementInSnapshot(snapshot, cached.ref) !== null) {
|
|
2786
|
+
updateCache(opts.selectorCache, cacheKey, cached.ref, buildSnapshotHash(snapshot));
|
|
2787
|
+
aiResult = { ref: cached.ref, aiResponseText: "(cache hit)", reasoning: "cache hit" };
|
|
2788
|
+
resolveMethod = "cache";
|
|
2789
|
+
debugLogger.log({
|
|
2790
|
+
phase: "executor",
|
|
2791
|
+
event: "selector_cache_hit",
|
|
2792
|
+
step: step.ordinal,
|
|
2793
|
+
data: { cacheKey, ref: cached.ref, hitCount: cached.hitCount + 1 }
|
|
2794
|
+
});
|
|
2795
|
+
logger.success(`Cache hit: @${cached.ref}`);
|
|
2796
|
+
} else {
|
|
2797
|
+
invalidateCacheEntry(opts.selectorCache, cacheKey);
|
|
2798
|
+
debugLogger.log({
|
|
2799
|
+
phase: "executor",
|
|
2800
|
+
event: "selector_cache_stale",
|
|
2801
|
+
step: step.ordinal,
|
|
2802
|
+
data: { cacheKey, ref: cached.ref }
|
|
2803
|
+
});
|
|
2804
|
+
logger.warn(`Cache stale: @${cached.ref} not found in snapshot, falling back to AI`);
|
|
2805
|
+
cachedRefUsed = false;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
if (!aiResult && step.action.selector) {
|
|
2810
|
+
if (!lastSnapshot) {
|
|
2811
|
+
const currentUrl = await browser.url();
|
|
2812
|
+
if (currentUrl.startsWith("chrome-error://")) {
|
|
2813
|
+
logger.warn(tf("executor.chromeErrorDetected", { url: step.url }));
|
|
2814
|
+
await browser.navigate(step.url);
|
|
2815
|
+
await browser.waitForDOMStability(3e3);
|
|
2816
|
+
}
|
|
2817
|
+
await browser.waitForDOMStability(strategy.domStabilityMs);
|
|
2818
|
+
const snapshotResult = await browser.snapshotWithRefs();
|
|
2819
|
+
lastSnapshot = snapshotResult.tree;
|
|
2820
|
+
lastRefs = snapshotResult.refs;
|
|
2821
|
+
prevSnapshot = lastSnapshot;
|
|
2822
|
+
debugLogger.log({
|
|
2823
|
+
phase: "executor",
|
|
2824
|
+
event: "initial_snapshot",
|
|
2825
|
+
step: step.ordinal,
|
|
2826
|
+
data: {
|
|
2827
|
+
url: await browser.url(),
|
|
2828
|
+
elementCount: countSnapshotElements(lastSnapshot),
|
|
2829
|
+
snapshotLength: lastSnapshot.length
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
if (countSnapshotElements(lastSnapshot) === 0) {
|
|
2833
|
+
debugLogger.log({
|
|
2834
|
+
phase: "executor",
|
|
2835
|
+
event: "empty_snapshot_recovery",
|
|
2836
|
+
step: step.ordinal,
|
|
2837
|
+
data: { url: await browser.url(), attempt: "initial" }
|
|
2838
|
+
});
|
|
2839
|
+
logger.warn("Empty snapshot detected, waiting for DOM stability...");
|
|
2840
|
+
await sleep(1e3);
|
|
2841
|
+
await browser.waitForDOMStability(strategy.domStabilityMs);
|
|
2842
|
+
const retryResult = await browser.snapshotWithRefs();
|
|
2843
|
+
lastSnapshot = retryResult.tree;
|
|
2844
|
+
lastRefs = retryResult.refs;
|
|
2845
|
+
prevSnapshot = lastSnapshot;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
const deterministicResult = resolveDeterministic(lastSnapshot, step.action.selector, lastRefs);
|
|
2849
|
+
if (deterministicResult.ref) {
|
|
2850
|
+
aiResult = {
|
|
2851
|
+
ref: deterministicResult.ref,
|
|
2852
|
+
aiResponseText: `(deterministic: ${deterministicResult.matchType}, confidence=${deterministicResult.confidence})`,
|
|
2853
|
+
reasoning: `deterministic match: ${deterministicResult.matchType}`
|
|
2854
|
+
};
|
|
2855
|
+
resolveMethod = "deterministic";
|
|
2856
|
+
deterministicMatchType = deterministicResult.matchType ?? void 0;
|
|
2857
|
+
deterministicConfidence = deterministicResult.confidence;
|
|
2858
|
+
debugLogger.log({
|
|
2859
|
+
phase: "executor",
|
|
2860
|
+
event: "deterministic_resolve",
|
|
2861
|
+
step: step.ordinal,
|
|
2862
|
+
data: {
|
|
2863
|
+
ref: deterministicResult.ref,
|
|
2864
|
+
matchType: deterministicResult.matchType,
|
|
2865
|
+
confidence: deterministicResult.confidence,
|
|
2866
|
+
selector: step.action.selector
|
|
2867
|
+
}
|
|
2868
|
+
});
|
|
2869
|
+
deterministicResolveResult = `hit: @${deterministicResult.ref} (${deterministicResult.matchType}, confidence=${deterministicResult.confidence})`;
|
|
2870
|
+
logger.success(`Deterministic resolve: @${deterministicResult.ref} (${deterministicResult.matchType}, confidence=${deterministicResult.confidence})`);
|
|
2871
|
+
} else {
|
|
2872
|
+
deterministicResolveResult = "miss: no match";
|
|
2873
|
+
debugLogger.log({
|
|
2874
|
+
phase: "executor",
|
|
2875
|
+
event: "deterministic_resolve_miss",
|
|
2876
|
+
step: step.ordinal,
|
|
2877
|
+
data: { selector: step.action.selector }
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
for (let attempt = 0; attempt <= maxRetries && !aiResult; attempt++) {
|
|
2882
|
+
if (attempt > 0) {
|
|
2883
|
+
logger.warn(`Selector not found, retrying (${attempt}/${maxRetries})...`);
|
|
2884
|
+
if (attempt <= strategy.changeTimeouts.length) {
|
|
2885
|
+
const timeout = strategy.changeTimeouts[attempt - 1];
|
|
2886
|
+
logger.info(`Waiting up to ${timeout / 1e3}s for page transition...`);
|
|
2887
|
+
const changeResult = await browser.waitForSnapshotChange(lastSnapshot, timeout);
|
|
2888
|
+
if (changeResult.changed) {
|
|
2889
|
+
logger.success("Page transition detected");
|
|
2890
|
+
lastSnapshot = changeResult.snapshot;
|
|
2891
|
+
} else {
|
|
2892
|
+
debugLogger.log({
|
|
2893
|
+
phase: "executor",
|
|
2894
|
+
event: "snapshot_unchanged",
|
|
2895
|
+
step: step.ordinal,
|
|
2896
|
+
data: { attempt, strategy: "waitForSnapshotChange", timeoutMs: timeout }
|
|
2897
|
+
});
|
|
2898
|
+
if (countSnapshotElements(lastSnapshot) === 0) {
|
|
2899
|
+
logger.warn("No page transition detected but snapshot is empty, attempting recovery...");
|
|
2900
|
+
} else {
|
|
2901
|
+
logger.warn("No page transition detected, skipping AI call");
|
|
2902
|
+
const reason2 = `Attempt ${attempt}: waited ${timeout / 1e3}s for page transition, no change`;
|
|
2903
|
+
failureHistory.push(reason2);
|
|
2904
|
+
retryDetails.push({
|
|
2905
|
+
attempt,
|
|
2906
|
+
snapshotChanged: false,
|
|
2907
|
+
snapshotElementCount: countSnapshotElements(lastSnapshot),
|
|
2908
|
+
failureReason: reason2
|
|
2909
|
+
});
|
|
2910
|
+
prevSnapshot = lastSnapshot;
|
|
2911
|
+
attempts = attempt + 1;
|
|
2912
|
+
continue;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
} else {
|
|
2916
|
+
await sleep(strategy.finalRetryStabilizeMs);
|
|
2917
|
+
await browser.waitForDOMStability(strategy.domStabilityMs);
|
|
2918
|
+
lastSnapshot = await browser.snapshot();
|
|
2919
|
+
}
|
|
2920
|
+
} else {
|
|
2921
|
+
if (!lastSnapshot) {
|
|
2922
|
+
lastSnapshot = await browser.snapshot();
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
if (countSnapshotElements(lastSnapshot) === 0) {
|
|
2926
|
+
debugLogger.log({
|
|
2927
|
+
phase: "executor",
|
|
2928
|
+
event: "empty_snapshot_recovery",
|
|
2929
|
+
step: step.ordinal,
|
|
2930
|
+
data: { url: await browser.url(), attempt }
|
|
2931
|
+
});
|
|
2932
|
+
logger.warn(`Snapshot empty (attempt ${attempt}), forcing DOM stability wait...`);
|
|
2933
|
+
await sleep(1e3);
|
|
2934
|
+
await browser.waitForDOMStability(strategy.domStabilityMs);
|
|
2935
|
+
const freshResult = await browser.snapshotWithRefs();
|
|
2936
|
+
lastSnapshot = freshResult.tree;
|
|
2937
|
+
lastRefs = freshResult.refs;
|
|
2938
|
+
}
|
|
2939
|
+
const snapshotChanged = attempt === 0 || lastSnapshot !== prevSnapshot;
|
|
2940
|
+
debugLogger.log({
|
|
2941
|
+
phase: "executor",
|
|
2942
|
+
event: attempt > 0 ? "selector_retry" : "snapshot",
|
|
2943
|
+
step: step.ordinal,
|
|
2944
|
+
data: {
|
|
2945
|
+
url: step.url,
|
|
2946
|
+
snapshot: lastSnapshot,
|
|
2947
|
+
attempt,
|
|
2948
|
+
...attempt > 0 ? { snapshotChanged } : {}
|
|
2949
|
+
}
|
|
2950
|
+
});
|
|
2951
|
+
const failureHints = failureRegistry && step.action.selector ? failureRegistry.getRelevantHints(step.url, step.action.selector) : void 0;
|
|
2952
|
+
if (failureHints) failureHintsProvided = true;
|
|
2953
|
+
const retryContext = attempt > 0 ? {
|
|
2954
|
+
attempt,
|
|
2955
|
+
previousAiResponse: lastAiResponseText || void 0,
|
|
2956
|
+
snapshotChanged,
|
|
2957
|
+
failureHistory,
|
|
2958
|
+
failureHints
|
|
2959
|
+
} : void 0;
|
|
2960
|
+
const skillResult = await applySkillTransform(browser, lastSnapshot, step.url, opts.skills);
|
|
2961
|
+
if (skillResult?.extractedData) {
|
|
2962
|
+
opts.store.set("extractedData", skillResult.extractedData);
|
|
2963
|
+
}
|
|
2964
|
+
const filteredSnap = skillResult?.snapshot ?? (snapshotCache ? snapshotCache.getFiltered(lastSnapshot) : lastSnapshot);
|
|
2965
|
+
const relevantContext = contextChunker ? contextChunker.selectRelevant(step.description, step.url) : config.contextMarkdown;
|
|
2966
|
+
const detailed = await resolveWithAIDetailed(
|
|
2967
|
+
filteredSnap,
|
|
2968
|
+
step.action.selector,
|
|
2969
|
+
step.description,
|
|
2970
|
+
step.url,
|
|
2971
|
+
relevantContext,
|
|
2972
|
+
debugLogger,
|
|
2973
|
+
retryContext,
|
|
2974
|
+
opts.store.getWorkingMemorySummary(),
|
|
2975
|
+
opts.aiProvider
|
|
2976
|
+
);
|
|
2977
|
+
attempts = attempt + 1;
|
|
2978
|
+
lastAiResponseText = detailed.aiResponseText;
|
|
2979
|
+
lastAiPrompt = detailed.prompt ?? "";
|
|
2980
|
+
prevSnapshot = lastSnapshot;
|
|
2981
|
+
if (detailed.ref) {
|
|
2982
|
+
aiResult = detailed;
|
|
2983
|
+
resolveMethod = "ai";
|
|
2984
|
+
break;
|
|
2985
|
+
}
|
|
2986
|
+
const reason = `Attempt ${attempt + 1}: AI returned empty ref`;
|
|
2987
|
+
failureHistory.push(reason);
|
|
2988
|
+
retryDetails.push({
|
|
2989
|
+
attempt,
|
|
2990
|
+
snapshotChanged,
|
|
2991
|
+
snapshotElementCount: countSnapshotElements(lastSnapshot),
|
|
2992
|
+
failureReason: reason,
|
|
2993
|
+
aiReasoning: detailed.reasoning || void 0,
|
|
2994
|
+
aiPrompt: detailed.prompt?.slice(0, 2e3),
|
|
2995
|
+
aiResponse: detailed.aiResponseText?.slice(0, 1e3),
|
|
2996
|
+
snapshotPreview: lastSnapshot.slice(0, 2e3)
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
if (!aiResult) {
|
|
3000
|
+
debugLogger.log({
|
|
3001
|
+
phase: "executor",
|
|
3002
|
+
event: "recovery_attempt",
|
|
3003
|
+
step: step.ordinal,
|
|
3004
|
+
data: { strategy: "scroll_down" }
|
|
3005
|
+
});
|
|
3006
|
+
logger.warn("All retries failed, attempting scroll recovery...");
|
|
3007
|
+
try {
|
|
3008
|
+
await browser.scroll("down", 500);
|
|
3009
|
+
await browser.waitForDOMStability(3e3);
|
|
3010
|
+
const scrolledSnapshot = await browser.snapshot();
|
|
3011
|
+
if (scrolledSnapshot !== lastSnapshot) {
|
|
3012
|
+
const scrollSkillResult = await applySkillTransform(browser, scrolledSnapshot, step.url, opts.skills);
|
|
3013
|
+
if (scrollSkillResult?.extractedData) {
|
|
3014
|
+
opts.store.set("extractedData", scrollSkillResult.extractedData);
|
|
3015
|
+
}
|
|
3016
|
+
const filteredScrollSnap = scrollSkillResult?.snapshot ?? (snapshotCache ? snapshotCache.getFiltered(scrolledSnapshot) : scrolledSnapshot);
|
|
3017
|
+
const relevantCtx = contextChunker ? contextChunker.selectRelevant(step.description, step.url) : config.contextMarkdown;
|
|
3018
|
+
const recoveryResult = await resolveWithAIDetailed(
|
|
3019
|
+
filteredScrollSnap,
|
|
3020
|
+
step.action.selector,
|
|
3021
|
+
step.description,
|
|
3022
|
+
step.url,
|
|
3023
|
+
relevantCtx,
|
|
3024
|
+
debugLogger,
|
|
3025
|
+
{
|
|
3026
|
+
attempt: attempts,
|
|
3027
|
+
snapshotChanged: true,
|
|
3028
|
+
failureHistory: [...failureHistory, "Scroll recovery attempt"],
|
|
3029
|
+
failureHints: failureRegistry && step.action.selector ? failureRegistry.getRelevantHints(step.url, step.action.selector) : void 0
|
|
3030
|
+
},
|
|
3031
|
+
opts.store.getWorkingMemorySummary(),
|
|
3032
|
+
opts.aiProvider
|
|
3033
|
+
);
|
|
3034
|
+
attempts++;
|
|
3035
|
+
lastAiResponseText = recoveryResult.aiResponseText;
|
|
3036
|
+
lastAiPrompt = recoveryResult.prompt ?? "";
|
|
3037
|
+
lastSnapshot = scrolledSnapshot;
|
|
3038
|
+
if (recoveryResult.ref) {
|
|
3039
|
+
aiResult = recoveryResult;
|
|
3040
|
+
resolveMethod = "scroll";
|
|
3041
|
+
logger.success("Scroll recovery succeeded");
|
|
3042
|
+
if (failureRegistry && step.action.selector) {
|
|
3043
|
+
failureRegistry.record(step.url, step.action.selector, "element_not_found", step.ordinal, { method: "scroll" });
|
|
3044
|
+
}
|
|
3045
|
+
} else {
|
|
3046
|
+
retryDetails.push({
|
|
3047
|
+
attempt: attempts - 1,
|
|
3048
|
+
snapshotChanged: true,
|
|
3049
|
+
snapshotElementCount: countSnapshotElements(scrolledSnapshot),
|
|
3050
|
+
failureReason: "Scroll recovery: AI returned empty ref",
|
|
3051
|
+
aiReasoning: recoveryResult.reasoning || void 0,
|
|
3052
|
+
aiPrompt: recoveryResult.prompt?.slice(0, 2e3),
|
|
3053
|
+
aiResponse: recoveryResult.aiResponseText?.slice(0, 1e3),
|
|
3054
|
+
snapshotPreview: scrolledSnapshot.slice(0, 2e3)
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
} catch {
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (!aiResult && config.enableVisionFallback) {
|
|
3062
|
+
logger.warn("Attempting vision fallback (screenshot-based)...");
|
|
3063
|
+
try {
|
|
3064
|
+
const { imageBuffer, annotations } = await browser.screenshotWithAnnotations();
|
|
3065
|
+
const visionResult = await resolveWithVision(
|
|
3066
|
+
imageBuffer,
|
|
3067
|
+
annotations,
|
|
3068
|
+
step.action.selector,
|
|
3069
|
+
step.description,
|
|
3070
|
+
step.url,
|
|
3071
|
+
failureHistory,
|
|
3072
|
+
debugLogger,
|
|
3073
|
+
opts.aiProvider
|
|
3074
|
+
);
|
|
3075
|
+
if (visionResult?.ref) {
|
|
3076
|
+
aiResult = {
|
|
3077
|
+
ref: visionResult.ref,
|
|
3078
|
+
aiResponseText: `(vision: confidence=${visionResult.confidence})`,
|
|
3079
|
+
reasoning: visionResult.reasoning
|
|
3080
|
+
};
|
|
3081
|
+
resolveMethod = "vision";
|
|
3082
|
+
visionFallbackResult = {
|
|
3083
|
+
annotationCount: annotations.length,
|
|
3084
|
+
reasoning: visionResult.reasoning,
|
|
3085
|
+
success: true
|
|
3086
|
+
};
|
|
3087
|
+
logger.success(`Vision fallback succeeded: @${visionResult.ref}`);
|
|
3088
|
+
if (failureRegistry && step.action.selector) {
|
|
3089
|
+
failureRegistry.record(step.url, step.action.selector, "element_not_found", step.ordinal, { method: "vision" });
|
|
3090
|
+
}
|
|
3091
|
+
} else {
|
|
3092
|
+
visionFallbackResult = {
|
|
3093
|
+
annotationCount: annotations.length,
|
|
3094
|
+
reasoning: visionResult?.reasoning ?? "no match",
|
|
3095
|
+
success: false
|
|
3096
|
+
};
|
|
3097
|
+
failureHistory.push("Vision fallback: no matching element");
|
|
3098
|
+
}
|
|
3099
|
+
} catch (error) {
|
|
3100
|
+
failureHistory.push(`Vision fallback error: ${error instanceof Error ? error.message : String(error)}`);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
if (!aiResult && config.enableAgentFallback) {
|
|
3104
|
+
debugLogger.log({
|
|
3105
|
+
phase: "executor",
|
|
3106
|
+
event: "agent_fallback_start",
|
|
3107
|
+
step: step.ordinal,
|
|
3108
|
+
data: { failureHistory }
|
|
3109
|
+
});
|
|
3110
|
+
logger.warn("Attempting agent fallback...");
|
|
3111
|
+
const fallbackContext = contextChunker ? contextChunker.selectRelevant(step.description, step.url) : config.contextMarkdown;
|
|
3112
|
+
const fallbackResult = await runGoalAgent(
|
|
3113
|
+
browser,
|
|
3114
|
+
{ description: step.description, action: step.action },
|
|
3115
|
+
failureHistory,
|
|
3116
|
+
fallbackContext,
|
|
3117
|
+
debugLogger,
|
|
3118
|
+
opts.aiProvider
|
|
3119
|
+
);
|
|
3120
|
+
debugLogger.log({
|
|
3121
|
+
phase: "executor",
|
|
3122
|
+
event: "agent_fallback_result",
|
|
3123
|
+
step: step.ordinal,
|
|
3124
|
+
data: fallbackResult
|
|
3125
|
+
});
|
|
3126
|
+
agentFallbackResult = {
|
|
3127
|
+
strategy: fallbackResult.alternativeStrategy,
|
|
3128
|
+
analysis: fallbackResult.analysis,
|
|
3129
|
+
reasoning: fallbackResult.reasoning,
|
|
3130
|
+
success: fallbackResult.success
|
|
3131
|
+
};
|
|
3132
|
+
if (fallbackResult.success && fallbackResult.alternativeRef) {
|
|
3133
|
+
aiResult = {
|
|
3134
|
+
ref: fallbackResult.alternativeRef,
|
|
3135
|
+
aiResponseText: JSON.stringify(fallbackResult),
|
|
3136
|
+
reasoning: fallbackResult.reasoning
|
|
3137
|
+
};
|
|
3138
|
+
resolveMethod = "agent";
|
|
3139
|
+
logger.success(`Agent fallback succeeded: @${fallbackResult.alternativeRef} (${fallbackResult.alternativeStrategy})`);
|
|
3140
|
+
if (failureRegistry && step.action.selector) {
|
|
3141
|
+
failureRegistry.record(step.url, step.action.selector, "element_not_found", step.ordinal, { method: `agent:${fallbackResult.alternativeStrategy}` });
|
|
3142
|
+
}
|
|
3143
|
+
} else {
|
|
3144
|
+
logger.warn(`Agent fallback: ${fallbackResult.alternativeStrategy} \u2014 ${fallbackResult.analysis}`);
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
const retryCount = attempts > 1 ? attempts - 1 : 0;
|
|
3148
|
+
if (!aiResult) {
|
|
3149
|
+
const elementCount = countSnapshotElements(lastSnapshot);
|
|
3150
|
+
const selectorInfo = JSON.stringify(step.action.selector);
|
|
3151
|
+
const aiExcerpt = lastAiResponseText.slice(0, 200);
|
|
3152
|
+
const failureCategory = classifyFailure({
|
|
3153
|
+
error: `Could not resolve selector after ${attempts} attempts`,
|
|
3154
|
+
selectorResolved: false,
|
|
3155
|
+
actionExecuted: false,
|
|
3156
|
+
retryDetails,
|
|
3157
|
+
cachedRefUsed
|
|
3158
|
+
});
|
|
3159
|
+
if (failureRegistry && step.action.selector) {
|
|
3160
|
+
failureRegistry.record(step.url, step.action.selector, failureCategory, step.ordinal);
|
|
3161
|
+
}
|
|
3162
|
+
return {
|
|
3163
|
+
status: "failed",
|
|
3164
|
+
durationMs: Date.now() - start,
|
|
3165
|
+
error: `Could not resolve selector after ${attempts} attempts (elements: ${elementCount}, selector: ${selectorInfo}, AI: "${aiExcerpt}")`,
|
|
3166
|
+
retryCount,
|
|
3167
|
+
retryDetails,
|
|
3168
|
+
failureCategory,
|
|
3169
|
+
diagnostics: {
|
|
3170
|
+
lastSnapshotPreview: lastSnapshot.slice(0, 2e3),
|
|
3171
|
+
failureHistory,
|
|
3172
|
+
lastAiResponseText,
|
|
3173
|
+
recoveryHint: getRecoveryHint(failureCategory),
|
|
3174
|
+
deterministicResolveResult: deterministicResolveResult || void 0,
|
|
3175
|
+
visionFallbackResult,
|
|
3176
|
+
agentFallbackResult,
|
|
3177
|
+
executionStrategy: strategy,
|
|
3178
|
+
stepAction: {
|
|
3179
|
+
type: step.action.type,
|
|
3180
|
+
selector: step.action.selector,
|
|
3181
|
+
value: step.action.value,
|
|
3182
|
+
url: step.action.url
|
|
3183
|
+
},
|
|
3184
|
+
stepUrl: step.url
|
|
3185
|
+
},
|
|
3186
|
+
// テレメトリフィールド
|
|
3187
|
+
resolveMethod,
|
|
3188
|
+
deterministicMatchType,
|
|
3189
|
+
deterministicConfidence,
|
|
3190
|
+
contextChunksUsed: contextChunker?.lastSelectedCount ?? 0,
|
|
3191
|
+
failureHintsProvided,
|
|
3192
|
+
workingMemoryTokens: opts.store.getWorkingMemorySummary() ? Math.ceil(opts.store.getWorkingMemorySummary().length / 4) : 0,
|
|
3193
|
+
snapshotElementCount: elementCount
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
debugLogger.log({
|
|
3197
|
+
phase: "executor",
|
|
3198
|
+
event: "selector_resolve",
|
|
3199
|
+
step: step.ordinal,
|
|
3200
|
+
data: { selector: step.action.selector, resolvedRef: aiResult.ref, url: step.url, attempts, resolveMethod }
|
|
3201
|
+
});
|
|
3202
|
+
if (opts.selectorCache && step.action.selector) {
|
|
3203
|
+
const cacheKey = buildCacheKey(step.ordinal, step.url, step.action.selector);
|
|
3204
|
+
updateCache(opts.selectorCache, cacheKey, aiResult.ref, buildSnapshotHash(lastSnapshot));
|
|
3205
|
+
}
|
|
3206
|
+
if (resolveMethod !== "cache" && resolveMethod !== "deterministic") {
|
|
3207
|
+
logger.success(`AI resolved: @${aiResult.ref}${attempts > 1 ? ` (after ${attempts} attempts)` : ""}`);
|
|
3208
|
+
}
|
|
3209
|
+
const ref = `@${aiResult.ref}`;
|
|
3210
|
+
const value = resolveValue(step, opts.store);
|
|
3211
|
+
const optionText = step.action.optionText ? opts.store.resolveTemplate(step.action.optionText) : void 0;
|
|
3212
|
+
if (!lastRefs) {
|
|
3213
|
+
const freshResult = await browser.snapshotWithRefs();
|
|
3214
|
+
lastSnapshot = freshResult.tree;
|
|
3215
|
+
lastRefs = freshResult.refs;
|
|
3216
|
+
}
|
|
3217
|
+
let validation = validateAction(lastSnapshot, aiResult.ref, step.action.type, value, lastRefs);
|
|
3218
|
+
if (validation.warnings.length > 0) {
|
|
3219
|
+
for (const w of validation.warnings) {
|
|
3220
|
+
logger.warn(`Validation warning: ${w.message}`);
|
|
3221
|
+
}
|
|
3222
|
+
debugLogger.log({
|
|
3223
|
+
phase: "executor",
|
|
3224
|
+
event: "validation_warnings",
|
|
3225
|
+
step: step.ordinal,
|
|
3226
|
+
data: { warnings: validation.warnings }
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
if (!validation.valid) {
|
|
3230
|
+
const hasTypeMismatch = validation.errors.some((e) => e.code === "type_mismatch");
|
|
3231
|
+
if (hasTypeMismatch) {
|
|
3232
|
+
debugLogger.log({
|
|
3233
|
+
phase: "executor",
|
|
3234
|
+
event: "validation_retry",
|
|
3235
|
+
step: step.ordinal,
|
|
3236
|
+
data: { errors: validation.errors, ref: aiResult.ref, actionType: step.action.type }
|
|
3237
|
+
});
|
|
3238
|
+
logger.warn("Validation failed with type_mismatch, retrying with fresh snapshot...");
|
|
3239
|
+
await browser.waitForDOMStability(2e3);
|
|
3240
|
+
const freshResult = await browser.snapshotWithRefs();
|
|
3241
|
+
if (freshResult.refs[aiResult.ref]) {
|
|
3242
|
+
lastSnapshot = freshResult.tree;
|
|
3243
|
+
lastRefs = freshResult.refs;
|
|
3244
|
+
validation = validateAction(lastSnapshot, aiResult.ref, step.action.type, value, lastRefs);
|
|
3245
|
+
if (validation.warnings.length > 0) {
|
|
3246
|
+
for (const w of validation.warnings) {
|
|
3247
|
+
logger.warn(`Validation warning (retry): ${w.message}`);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (!validation.valid) {
|
|
3253
|
+
const errorMessages = validation.errors.map((e) => e.message).join("; ");
|
|
3254
|
+
debugLogger.log({
|
|
3255
|
+
phase: "executor",
|
|
3256
|
+
event: "validation_failed",
|
|
3257
|
+
step: step.ordinal,
|
|
3258
|
+
data: { errors: validation.errors, ref: aiResult.ref, actionType: step.action.type }
|
|
3259
|
+
});
|
|
3260
|
+
return {
|
|
3261
|
+
status: "failed",
|
|
3262
|
+
durationMs: Date.now() - start,
|
|
3263
|
+
error: `Pre-execution validation failed: ${errorMessages}`,
|
|
3264
|
+
retryCount,
|
|
3265
|
+
retryDetails: retryDetails.length > 0 ? retryDetails : void 0,
|
|
3266
|
+
diagnostics: {
|
|
3267
|
+
lastSnapshotPreview: lastSnapshot.slice(0, 2e3),
|
|
3268
|
+
failureHistory,
|
|
3269
|
+
lastAiResponseText,
|
|
3270
|
+
validationErrors: validation.errors.map((e) => `[${e.code}] ${e.message}`),
|
|
3271
|
+
validationWarnings: validation.warnings.map((w) => `[${w.code}] ${w.message}`),
|
|
3272
|
+
executionStrategy: strategy,
|
|
3273
|
+
stepAction: {
|
|
3274
|
+
type: step.action.type,
|
|
3275
|
+
selector: step.action.selector,
|
|
3276
|
+
value: step.action.value,
|
|
3277
|
+
url: step.action.url
|
|
3278
|
+
},
|
|
3279
|
+
stepUrl: step.url
|
|
3280
|
+
},
|
|
3281
|
+
// テレメトリフィールド
|
|
3282
|
+
resolveMethod,
|
|
3283
|
+
deterministicMatchType,
|
|
3284
|
+
deterministicConfidence,
|
|
3285
|
+
contextChunksUsed: contextChunker?.lastSelectedCount ?? 0,
|
|
3286
|
+
failureHintsProvided,
|
|
3287
|
+
snapshotElementCount: countSnapshotElements(lastSnapshot)
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
logger.success("Validation retry succeeded");
|
|
3291
|
+
}
|
|
3292
|
+
const displayValue = value ? opts.store.maskSensitive(value) : value;
|
|
3293
|
+
const urlBefore = await browser.url();
|
|
3294
|
+
const pageCountBefore = await browser.pageCount();
|
|
3295
|
+
switch (step.action.type) {
|
|
3296
|
+
case "click":
|
|
3297
|
+
logger.step(`Click: ${ref}`);
|
|
3298
|
+
await browser.click(ref);
|
|
3299
|
+
break;
|
|
3300
|
+
case "input":
|
|
3301
|
+
logger.step(`Input: ${ref} = "${displayValue ?? ""}"`);
|
|
3302
|
+
await browser.fill(ref, value ?? "");
|
|
3303
|
+
break;
|
|
3304
|
+
case "select": {
|
|
3305
|
+
const displayOptionText = optionText ? opts.store.maskSensitive(optionText) : void 0;
|
|
3306
|
+
logger.step(`Select: ${ref} = "${displayOptionText ?? displayValue ?? ""}"`);
|
|
3307
|
+
await browser.select(ref, optionText ?? value ?? "");
|
|
3308
|
+
break;
|
|
3309
|
+
}
|
|
3310
|
+
case "hover":
|
|
3311
|
+
logger.step(`Hover (click fallback): ${ref}`);
|
|
3312
|
+
await browser.click(ref);
|
|
3313
|
+
break;
|
|
3314
|
+
case "download": {
|
|
3315
|
+
const downloadPath = opts.store.resolveTemplate(
|
|
3316
|
+
step.action.downloadPath ?? `/tmp/download-${Date.now()}.bin`
|
|
3317
|
+
);
|
|
3318
|
+
logger.step(`Download: ${ref} \u2192 ${downloadPath}`);
|
|
3319
|
+
await browser.download(ref, downloadPath);
|
|
3320
|
+
if (opts.downloadManager) {
|
|
3321
|
+
opts.downloadManager.addDownload({
|
|
3322
|
+
path: downloadPath,
|
|
3323
|
+
filename: downloadPath.split("/").pop() ?? "unknown",
|
|
3324
|
+
stepOrdinal: step.ordinal,
|
|
3325
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
return {
|
|
3329
|
+
status: "success",
|
|
3330
|
+
durationMs: Date.now() - start,
|
|
3331
|
+
downloadedFile: downloadPath,
|
|
3332
|
+
retryCount,
|
|
3333
|
+
retryDetails: retryDetails.length > 0 ? retryDetails : void 0
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (step.action.type === "click") {
|
|
3338
|
+
await browser.waitForPossibleNavigation(urlBefore, pageCountBefore);
|
|
3339
|
+
const maxClickRetries = 2;
|
|
3340
|
+
let clickRetryUrl = await browser.url();
|
|
3341
|
+
for (let clickAttempt = 0; clickRetryUrl.startsWith("chrome-error://"); clickAttempt++) {
|
|
3342
|
+
if (clickAttempt >= maxClickRetries) {
|
|
3343
|
+
logger.warn(tf("executor.chromeErrorDetected", { url: urlBefore }));
|
|
3344
|
+
throw new Error(`Navigation failed: page landed on chrome-error:// (from: ${urlBefore})`);
|
|
3345
|
+
}
|
|
3346
|
+
logger.warn(tf("executor.chromeErrorDetected", { url: urlBefore }) + ` \u2014 retrying click (${clickAttempt + 1}/${maxClickRetries})`);
|
|
3347
|
+
await browser.navigate(urlBefore);
|
|
3348
|
+
await browser.waitForDOMStability(3e3);
|
|
3349
|
+
await sleep(2e3 * (clickAttempt + 1));
|
|
3350
|
+
await browser.click(ref);
|
|
3351
|
+
await browser.waitForPossibleNavigation(urlBefore, await browser.pageCount());
|
|
3352
|
+
clickRetryUrl = await browser.url();
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
const durationMs = Date.now() - start;
|
|
3356
|
+
debugLogger.log({
|
|
3357
|
+
phase: "executor",
|
|
3358
|
+
event: "action",
|
|
3359
|
+
step: step.ordinal,
|
|
3360
|
+
data: {
|
|
3361
|
+
type: step.action.type,
|
|
3362
|
+
ref,
|
|
3363
|
+
value: displayValue,
|
|
3364
|
+
status: "success",
|
|
3365
|
+
durationMs
|
|
3366
|
+
}
|
|
3367
|
+
});
|
|
3368
|
+
const workingMemorySummary = opts.store.getWorkingMemorySummary();
|
|
3369
|
+
return {
|
|
3370
|
+
status: "success",
|
|
3371
|
+
durationMs,
|
|
3372
|
+
retryCount,
|
|
3373
|
+
retryDetails: retryDetails.length > 0 ? retryDetails : void 0,
|
|
3374
|
+
// テレメトリフィールド
|
|
3375
|
+
resolveMethod,
|
|
3376
|
+
deterministicMatchType,
|
|
3377
|
+
deterministicConfidence,
|
|
3378
|
+
contextChunksUsed: contextChunker?.lastSelectedCount ?? 0,
|
|
3379
|
+
failureHintsProvided,
|
|
3380
|
+
workingMemoryTokens: workingMemorySummary ? Math.ceil(workingMemorySummary.length / 4) : 0,
|
|
3381
|
+
snapshotElementCount: lastSnapshot ? countSnapshotElements(lastSnapshot) : 0
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
async function executeMemoryOperations(step, store, dataStore, debugLogger, logger) {
|
|
3385
|
+
if (!step.memoryOperations) return;
|
|
3386
|
+
for (const op of step.memoryOperations) {
|
|
3387
|
+
if (op.type === "append") {
|
|
3388
|
+
if (!op.source) {
|
|
3389
|
+
logger.warn(`Memory append: source is required`);
|
|
3390
|
+
continue;
|
|
3391
|
+
}
|
|
3392
|
+
const rawValue = store.get(op.source);
|
|
3393
|
+
if (!rawValue) {
|
|
3394
|
+
logger.warn(`Memory append: variable "${op.source}" not found in store`);
|
|
3395
|
+
continue;
|
|
3396
|
+
}
|
|
3397
|
+
parseAndAppendToMemory(rawValue, {
|
|
3398
|
+
dataStore,
|
|
3399
|
+
debugLogger,
|
|
3400
|
+
phase: "executor",
|
|
3401
|
+
step: step.ordinal,
|
|
3402
|
+
collection: op.collection
|
|
3403
|
+
});
|
|
3404
|
+
} else if (op.type === "aggregate") {
|
|
3405
|
+
if (!op.field || !op.operation || !op.outputVariable) {
|
|
3406
|
+
logger.warn(`Memory aggregate: field, operation, and outputVariable are required`);
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
const result = dataStore.aggregate({
|
|
3410
|
+
collection: op.collection,
|
|
3411
|
+
field: op.field,
|
|
3412
|
+
operation: op.operation
|
|
3413
|
+
});
|
|
3414
|
+
store.set(op.outputVariable, result);
|
|
3415
|
+
logger.success(`Memory: ${op.operation}(${op.collection}.${op.field}) = "${result}" \u2192 {{${op.outputVariable}}}`);
|
|
3416
|
+
debugLogger.log({
|
|
3417
|
+
phase: "executor",
|
|
3418
|
+
event: "memory_aggregate",
|
|
3419
|
+
step: step.ordinal,
|
|
3420
|
+
data: { collection: op.collection, field: op.field, operation: op.operation, result, outputVariable: op.outputVariable }
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
function resolveValue(step, store) {
|
|
3426
|
+
const rawValue = step.action.value;
|
|
3427
|
+
if (rawValue === void 0) return void 0;
|
|
3428
|
+
return store.resolveTemplate(rawValue);
|
|
3429
|
+
}
|
|
3430
|
+
function countSnapshotElements(snapshot) {
|
|
3431
|
+
return countElements(snapshot);
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
// src/runbook-executor/input-collector.ts
|
|
3435
|
+
import { z as z5 } from "zod";
|
|
3436
|
+
var contextValuesSchema = z5.object({
|
|
3437
|
+
values: z5.record(z5.string(), z5.string()).describe("\u5909\u6570\u540D\u2192\u62BD\u51FA\u5024\u306E\u30DE\u30C3\u30D7")
|
|
3438
|
+
});
|
|
3439
|
+
var CONTEXT_EXTRACTION_SYSTEM_PROMPT = [
|
|
3440
|
+
"\u4EE5\u4E0B\u306E\u5909\u6570\u30EA\u30B9\u30C8\u3068 context markdown \u304C\u4E0E\u3048\u3089\u308C\u307E\u3059\u3002",
|
|
3441
|
+
"context \u304B\u3089\u5404\u5909\u6570\u306B\u5BFE\u5FDC\u3059\u308B\u5024\u3092\u62BD\u51FA\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
3442
|
+
"\u5024\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u5834\u5408\u306F\u305D\u306E\u30AD\u30FC\u3092\u542B\u3081\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002"
|
|
3443
|
+
].join("\n");
|
|
3444
|
+
async function extractValuesFromContext(inputs, contextMarkdown, aiProvider) {
|
|
3445
|
+
const inputDescriptions = inputs.map((i) => `- name: "${i.name}", description: "${i.description}"`).join("\n");
|
|
3446
|
+
const userPrompt = [
|
|
3447
|
+
"## \u5909\u6570",
|
|
3448
|
+
inputDescriptions,
|
|
3449
|
+
"",
|
|
3450
|
+
"## Context",
|
|
3451
|
+
contextMarkdown
|
|
3452
|
+
].join("\n");
|
|
3453
|
+
try {
|
|
3454
|
+
const model = aiProvider ? aiProvider.getModel("extraction") : getModel("extraction");
|
|
3455
|
+
const { object } = await withAIRetry(
|
|
3456
|
+
() => trackedGenerateObject("extraction", {
|
|
3457
|
+
model,
|
|
3458
|
+
messages: [
|
|
3459
|
+
{
|
|
3460
|
+
role: "system",
|
|
3461
|
+
content: CONTEXT_EXTRACTION_SYSTEM_PROMPT,
|
|
3462
|
+
providerOptions: {
|
|
3463
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
3464
|
+
}
|
|
3465
|
+
},
|
|
3466
|
+
{ role: "user", content: userPrompt }
|
|
3467
|
+
],
|
|
3468
|
+
schema: contextValuesSchema,
|
|
3469
|
+
temperature: 0
|
|
3470
|
+
}),
|
|
3471
|
+
{ label: "input-collect" }
|
|
3472
|
+
);
|
|
3473
|
+
return object.values;
|
|
3474
|
+
} catch (error) {
|
|
3475
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3476
|
+
console.warn(`[input-collector] Context extraction failed: ${msg}`);
|
|
3477
|
+
return {};
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
// src/runbook-executor/execution-planner.ts
|
|
3482
|
+
function mergeVariablesAndSecrets(variables, secrets) {
|
|
3483
|
+
const mergedValues = { ...variables };
|
|
3484
|
+
const sensitiveKeys = /* @__PURE__ */ new Set();
|
|
3485
|
+
if (secrets) {
|
|
3486
|
+
for (const [k, v] of Object.entries(secrets.values)) {
|
|
3487
|
+
mergedValues[k] = v;
|
|
3488
|
+
sensitiveKeys.add(k);
|
|
3489
|
+
}
|
|
3490
|
+
for (const k of secrets.keys) sensitiveKeys.add(k);
|
|
3491
|
+
}
|
|
3492
|
+
return { mergedValues, sensitiveKeys };
|
|
3493
|
+
}
|
|
3494
|
+
async function resolveVariablesCore(runbook, input) {
|
|
3495
|
+
const variables = runbook.variables ?? {};
|
|
3496
|
+
const values = /* @__PURE__ */ new Map();
|
|
3497
|
+
const sensitiveKeys = /* @__PURE__ */ new Set();
|
|
3498
|
+
const unresolvedDataVars = [];
|
|
3499
|
+
const captureVars = [];
|
|
3500
|
+
function collectCaptureVars(steps) {
|
|
3501
|
+
for (const step of steps) {
|
|
3502
|
+
if (step.captures) {
|
|
3503
|
+
for (const c of step.captures) {
|
|
3504
|
+
captureVars.push(c.name);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
if (step.steps) {
|
|
3508
|
+
collectCaptureVars(step.steps);
|
|
3509
|
+
}
|
|
3510
|
+
if (step.branches) {
|
|
3511
|
+
for (const branchCase of step.branches.cases) {
|
|
3512
|
+
collectCaptureVars(branchCase.steps);
|
|
3513
|
+
}
|
|
3514
|
+
if (step.branches.default) {
|
|
3515
|
+
collectCaptureVars(step.branches.default.steps);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
collectCaptureVars(runbook.steps);
|
|
3521
|
+
for (const [name, def] of Object.entries(variables)) {
|
|
3522
|
+
if (def.sensitive) {
|
|
3523
|
+
sensitiveKeys.add(name);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
const secretsKeys = /* @__PURE__ */ new Set();
|
|
3527
|
+
if (input.secrets) {
|
|
3528
|
+
for (const [key, val] of Object.entries(input.secrets.values)) {
|
|
3529
|
+
values.set(key, val);
|
|
3530
|
+
sensitiveKeys.add(key);
|
|
3531
|
+
secretsKeys.add(key);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
for (const [name, def] of Object.entries(variables)) {
|
|
3535
|
+
if (def.source === "data" && !values.has(name)) {
|
|
3536
|
+
unresolvedDataVars.push(name);
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
const unresolvedVars = Object.entries(variables).filter(
|
|
3540
|
+
([name]) => !values.has(name) && !unresolvedDataVars.includes(name)
|
|
3541
|
+
);
|
|
3542
|
+
const contextExtractedKeys = /* @__PURE__ */ new Set();
|
|
3543
|
+
if (unresolvedVars.length > 0) {
|
|
3544
|
+
const extracted = await extractValuesFromContext(
|
|
3545
|
+
unresolvedVars.map(([name, def]) => ({
|
|
3546
|
+
name,
|
|
3547
|
+
description: def.description ?? name,
|
|
3548
|
+
required: def.required !== false
|
|
3549
|
+
})),
|
|
3550
|
+
input.contextMarkdown,
|
|
3551
|
+
input.aiProvider
|
|
3552
|
+
);
|
|
3553
|
+
for (const [name, val] of Object.entries(extracted)) {
|
|
3554
|
+
if (!values.has(name) && val) {
|
|
3555
|
+
values.set(name, val);
|
|
3556
|
+
contextExtractedKeys.add(name);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
if (!input.skipEnv) {
|
|
3561
|
+
for (const [name, def] of Object.entries(variables)) {
|
|
3562
|
+
if (def.source === "env" && !values.has(name) && def.envKey) {
|
|
3563
|
+
const envVal = process.env[def.envKey];
|
|
3564
|
+
if (envVal !== void 0) {
|
|
3565
|
+
values.set(name, envVal);
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
for (const [name, def] of Object.entries(variables)) {
|
|
3571
|
+
if (def.source === "expression" && !values.has(name) && def.expression) {
|
|
3572
|
+
const resolved = resolveTemplate(def.expression, values);
|
|
3573
|
+
if (!/\{\{[\w.]+\}\}/.test(resolved)) {
|
|
3574
|
+
values.set(name, resolved);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
for (const [name, def] of Object.entries(variables)) {
|
|
3579
|
+
if (!values.has(name) && !unresolvedDataVars.includes(name) && def.value !== void 0) {
|
|
3580
|
+
values.set(name, def.value);
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
return {
|
|
3584
|
+
values,
|
|
3585
|
+
sensitiveKeys,
|
|
3586
|
+
unresolvedDataVars,
|
|
3587
|
+
captureVars,
|
|
3588
|
+
contextExtractedKeys,
|
|
3589
|
+
secretsKeys
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
async function planExecution(runbook, config) {
|
|
3593
|
+
const variables = runbook.variables ?? {};
|
|
3594
|
+
const hasContextVars = Object.entries(variables).some(
|
|
3595
|
+
([name]) => !config.secrets?.keys.has(name) && variables[name]?.source !== "data"
|
|
3596
|
+
);
|
|
3597
|
+
if (hasContextVars) {
|
|
3598
|
+
log.step(t("planner.extractingFromContext"));
|
|
3599
|
+
}
|
|
3600
|
+
const coreResult = await resolveVariablesCore(runbook, {
|
|
3601
|
+
secrets: config.secrets,
|
|
3602
|
+
contextMarkdown: config.contextMarkdown,
|
|
3603
|
+
skipEnv: false
|
|
3604
|
+
});
|
|
3605
|
+
if (hasContextVars) {
|
|
3606
|
+
log.info(tf("planner.extractedFromContext", { count: coreResult.contextExtractedKeys.size }));
|
|
3607
|
+
}
|
|
3608
|
+
const captureVarNames = new Set(coreResult.captureVars);
|
|
3609
|
+
const stillUnresolved = Object.entries(variables).filter(
|
|
3610
|
+
([name, def]) => !coreResult.values.has(name) && !coreResult.unresolvedDataVars.includes(name) && !captureVarNames.has(name) && def.required !== false
|
|
3611
|
+
);
|
|
3612
|
+
if (stillUnresolved.length > 0) {
|
|
3613
|
+
log.warn(tf("planner.unresolvedRequired", { count: stillUnresolved.length }));
|
|
3614
|
+
for (const [name, def] of stillUnresolved) {
|
|
3615
|
+
const answer = await promptText(def.description ?? name, {
|
|
3616
|
+
placeholder: t("planner.placeholder"),
|
|
3617
|
+
validate: (v) => !v?.trim() ? tf("planner.variableRequired", { name }) : void 0
|
|
3618
|
+
});
|
|
3619
|
+
coreResult.values.set(name, answer.trim());
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
return {
|
|
3623
|
+
resolvedVariables: {
|
|
3624
|
+
values: coreResult.values,
|
|
3625
|
+
sensitiveKeys: coreResult.sensitiveKeys
|
|
3626
|
+
},
|
|
3627
|
+
unresolvedDataVars: coreResult.unresolvedDataVars,
|
|
3628
|
+
captureVars: coreResult.captureVars,
|
|
3629
|
+
dataFilePath: config.dataFilePath,
|
|
3630
|
+
contextExtractedKeys: coreResult.contextExtractedKeys,
|
|
3631
|
+
secretsKeys: coreResult.secretsKeys
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
function displayPlan(plan, runbook, dataRowCount) {
|
|
3635
|
+
const lines = [];
|
|
3636
|
+
lines.push(t("planner.preVariables"));
|
|
3637
|
+
if (plan.resolvedVariables.values.size === 0 && plan.unresolvedDataVars.length === 0) {
|
|
3638
|
+
lines.push(" " + t("common.none"));
|
|
3639
|
+
} else {
|
|
3640
|
+
for (const [name, value] of plan.resolvedVariables.values) {
|
|
3641
|
+
const display = plan.resolvedVariables.sensitiveKeys.has(name) ? `"${value.slice(0, 3)}****"` : `"${value}"`;
|
|
3642
|
+
let source = "";
|
|
3643
|
+
if (plan.secretsKeys.has(name)) {
|
|
3644
|
+
source = t("planner.fromSecrets");
|
|
3645
|
+
} else if (plan.contextExtractedKeys.has(name)) {
|
|
3646
|
+
source = t("planner.fromContext");
|
|
3647
|
+
}
|
|
3648
|
+
lines.push(` ${name} \u2192 ${display}${source}`);
|
|
3649
|
+
}
|
|
3650
|
+
for (const name of plan.unresolvedDataVars) {
|
|
3651
|
+
lines.push(` ${name} \u2192 ${t("planner.fromDataSource")}`);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
if (plan.captureVars.length > 0) {
|
|
3655
|
+
let displayCaptures2 = function(steps, prefix) {
|
|
3656
|
+
for (const step of steps) {
|
|
3657
|
+
if (step.captures) {
|
|
3658
|
+
for (const c of step.captures) {
|
|
3659
|
+
lines.push(` ${c.name} \u2190 ${prefix}Step ${step.ordinal} (${c.strategy})`);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
if (step.steps) {
|
|
3663
|
+
displayCaptures2(step.steps, `${prefix}Loop ${step.ordinal} > `);
|
|
3664
|
+
}
|
|
3665
|
+
if (step.branches) {
|
|
3666
|
+
for (const branchCase of step.branches.cases) {
|
|
3667
|
+
displayCaptures2(branchCase.steps, `${prefix}Branch ${step.ordinal}/${branchCase.match} > `);
|
|
3668
|
+
}
|
|
3669
|
+
if (step.branches.default) {
|
|
3670
|
+
displayCaptures2(step.branches.default.steps, `${prefix}Branch ${step.ordinal}/default > `);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
3675
|
+
var displayCaptures = displayCaptures2;
|
|
3676
|
+
lines.push("");
|
|
3677
|
+
lines.push(t("planner.runtimeCaptures"));
|
|
3678
|
+
displayCaptures2(runbook.steps, "");
|
|
3679
|
+
}
|
|
3680
|
+
const conditionSteps = runbook.steps.filter((s) => s.condition);
|
|
3681
|
+
const loopSteps = runbook.steps.filter((s) => s.loop && s.steps);
|
|
3682
|
+
const branchSteps = runbook.steps.filter((s) => s.branches);
|
|
3683
|
+
if (conditionSteps.length > 0 || loopSteps.length > 0 || branchSteps.length > 0) {
|
|
3684
|
+
lines.push("");
|
|
3685
|
+
lines.push(t("planner.controlFlow"));
|
|
3686
|
+
for (const step of conditionSteps) {
|
|
3687
|
+
lines.push(tf("planner.conditionLabel", { ordinal: step.ordinal, condition: step.condition ?? "" }));
|
|
3688
|
+
}
|
|
3689
|
+
for (const step of loopSteps) {
|
|
3690
|
+
if (step.loop.forEach) {
|
|
3691
|
+
const maxIter = step.loop.maxIterations ?? 100;
|
|
3692
|
+
lines.push(
|
|
3693
|
+
tf("planner.forEachLabel", { ordinal: step.ordinal, max: maxIter, subSteps: step.steps.length, forEach: step.loop.forEach })
|
|
3694
|
+
);
|
|
3695
|
+
} else {
|
|
3696
|
+
const maxIter = step.loop.maxIterations ?? 10;
|
|
3697
|
+
lines.push(
|
|
3698
|
+
tf("planner.loopLabel", { ordinal: step.ordinal, max: maxIter, subSteps: step.steps.length, condition: step.loop.condition ?? "" })
|
|
3699
|
+
);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
for (const step of branchSteps) {
|
|
3703
|
+
const caseLabels = step.branches.cases.map((c) => c.match).join(", ");
|
|
3704
|
+
lines.push(
|
|
3705
|
+
tf("planner.branchLabel", { ordinal: step.ordinal, value: step.branches.value, cases: caseLabels })
|
|
3706
|
+
);
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
if (plan.dataFilePath && dataRowCount !== void 0) {
|
|
3710
|
+
lines.push("");
|
|
3711
|
+
lines.push(tf("planner.batchMode", { count: dataRowCount, path: plan.dataFilePath }));
|
|
3712
|
+
}
|
|
3713
|
+
lines.push(tf("planner.stepCount", { count: runbook.steps.length }));
|
|
3714
|
+
note(lines.join("\n"), t("planner.executionPlan"));
|
|
3715
|
+
}
|
|
3716
|
+
async function confirmPlan() {
|
|
3717
|
+
return promptConfirm(t("planner.confirmStart"), true);
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
// src/context/runtime-store.ts
|
|
3721
|
+
var MAX_RECENT_SUMMARIES = 5;
|
|
3722
|
+
var RuntimeStore = class {
|
|
3723
|
+
constructor() {
|
|
3724
|
+
this.store = /* @__PURE__ */ new Map();
|
|
3725
|
+
this.sensitiveKeys = /* @__PURE__ */ new Set();
|
|
3726
|
+
this.wm = {
|
|
3727
|
+
currentPhase: "initial",
|
|
3728
|
+
completedPhases: [],
|
|
3729
|
+
recentStepSummaries: [],
|
|
3730
|
+
currentPageContext: "",
|
|
3731
|
+
successStreak: 0,
|
|
3732
|
+
failureCount: 0,
|
|
3733
|
+
totalSteps: 0,
|
|
3734
|
+
lastUrl: "",
|
|
3735
|
+
lastActionType: "",
|
|
3736
|
+
actionTypeRun: 0
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
/** 事前解決済み変数をシードする */
|
|
3740
|
+
seed(values, sensitive) {
|
|
3741
|
+
for (const [k, v] of values) {
|
|
3742
|
+
this.store.set(k, v);
|
|
3743
|
+
}
|
|
3744
|
+
for (const k of sensitive) {
|
|
3745
|
+
this.sensitiveKeys.add(k);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
get(name) {
|
|
3749
|
+
return this.store.get(name);
|
|
3750
|
+
}
|
|
3751
|
+
set(name, value) {
|
|
3752
|
+
this.store.set(name, value);
|
|
3753
|
+
}
|
|
3754
|
+
has(name) {
|
|
3755
|
+
return this.store.has(name);
|
|
3756
|
+
}
|
|
3757
|
+
isSensitive(name) {
|
|
3758
|
+
return this.sensitiveKeys.has(name);
|
|
3759
|
+
}
|
|
3760
|
+
/** sensitive として登録されたキー名一覧を返す */
|
|
3761
|
+
getSensitiveKeys() {
|
|
3762
|
+
return Array.from(this.sensitiveKeys);
|
|
3763
|
+
}
|
|
3764
|
+
/** store の値で {{varName}} を展開する */
|
|
3765
|
+
resolveTemplate(template) {
|
|
3766
|
+
return resolveTemplate(template, this.store);
|
|
3767
|
+
}
|
|
3768
|
+
/** ログ出力用: sensitive な値をマスクした文字列を返す */
|
|
3769
|
+
maskSensitive(value) {
|
|
3770
|
+
let masked = value;
|
|
3771
|
+
for (const key of this.sensitiveKeys) {
|
|
3772
|
+
const val = this.store.get(key);
|
|
3773
|
+
if (val && masked.includes(val)) {
|
|
3774
|
+
masked = masked.replaceAll(val, "****");
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
return masked;
|
|
3778
|
+
}
|
|
3779
|
+
/** 全変数のスナップショット(デバッグ用) */
|
|
3780
|
+
entries() {
|
|
3781
|
+
return Array.from(this.store.entries());
|
|
3782
|
+
}
|
|
3783
|
+
// ─────────────── forEach ヘルパー ───────────────
|
|
3784
|
+
/**
|
|
3785
|
+
* forEach ループの各アイテムを変数としてセットする。
|
|
3786
|
+
* オブジェクト → varName = JSON文字列, varName.key = value にフラット化(1階層のみ)
|
|
3787
|
+
* プリミティブ → varName = String(item)
|
|
3788
|
+
*/
|
|
3789
|
+
setForEachItem(varName, item, indexVar, index) {
|
|
3790
|
+
if (indexVar !== void 0 && index !== void 0) {
|
|
3791
|
+
this.store.set(indexVar, String(index));
|
|
3792
|
+
}
|
|
3793
|
+
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
3794
|
+
this.store.set(varName, JSON.stringify(item));
|
|
3795
|
+
for (const [key, value] of Object.entries(item)) {
|
|
3796
|
+
this.store.set(`${varName}.${key}`, String(value ?? ""));
|
|
3797
|
+
}
|
|
3798
|
+
} else {
|
|
3799
|
+
this.store.set(varName, String(item ?? ""));
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
/**
|
|
3803
|
+
* forEach ループのアイテム変数をクリーンアップする。
|
|
3804
|
+
* varName と全 varName.* キーを削除。
|
|
3805
|
+
*/
|
|
3806
|
+
clearForEachItem(varName, indexVar) {
|
|
3807
|
+
this.store.delete(varName);
|
|
3808
|
+
if (indexVar) {
|
|
3809
|
+
this.store.delete(indexVar);
|
|
3810
|
+
}
|
|
3811
|
+
const prefix = `${varName}.`;
|
|
3812
|
+
for (const key of Array.from(this.store.keys())) {
|
|
3813
|
+
if (key.startsWith(prefix)) {
|
|
3814
|
+
this.store.delete(key);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
// ─────────────── Working Memory ───────────────
|
|
3819
|
+
/**
|
|
3820
|
+
* ステップ完了後に Working Memory を更新する。
|
|
3821
|
+
* フェーズ自動検出 + ステップ履歴の記録。
|
|
3822
|
+
*/
|
|
3823
|
+
updateWorkingMemory(stepOrdinal, totalStepCount, success, url, actionType, stepDescription) {
|
|
3824
|
+
this.wm.totalSteps = totalStepCount;
|
|
3825
|
+
if (success) {
|
|
3826
|
+
this.wm.successStreak++;
|
|
3827
|
+
this.wm.failureCount = 0;
|
|
3828
|
+
} else {
|
|
3829
|
+
this.wm.successStreak = 0;
|
|
3830
|
+
this.wm.failureCount++;
|
|
3831
|
+
}
|
|
3832
|
+
const summary = `Step ${stepOrdinal}: ${stepDescription} \u2192 ${success ? "OK" : "FAIL"}`;
|
|
3833
|
+
this.wm.recentStepSummaries.push(summary);
|
|
3834
|
+
if (this.wm.recentStepSummaries.length > MAX_RECENT_SUMMARIES) {
|
|
3835
|
+
this.wm.recentStepSummaries.shift();
|
|
3836
|
+
}
|
|
3837
|
+
this.wm.currentPageContext = url;
|
|
3838
|
+
this.detectPhaseTransition(stepOrdinal, url, actionType);
|
|
3839
|
+
this.wm.lastUrl = url;
|
|
3840
|
+
this.wm.lastActionType = actionType;
|
|
3841
|
+
}
|
|
3842
|
+
/**
|
|
3843
|
+
* AI プロンプト注入用の Working Memory サマリーを返す。
|
|
3844
|
+
* 100〜200 トークンの簡潔な構造化テキスト。
|
|
3845
|
+
* Working Memory がまだ蓄積されていない場合は undefined を返す。
|
|
3846
|
+
*/
|
|
3847
|
+
getWorkingMemorySummary() {
|
|
3848
|
+
if (this.wm.recentStepSummaries.length === 0) return void 0;
|
|
3849
|
+
const parts = [];
|
|
3850
|
+
if (this.wm.completedPhases.length > 0) {
|
|
3851
|
+
const phaseHistory = this.wm.completedPhases.map((p) => `${p.name}(steps ${p.startStep}-${p.endStep ?? "?"})`).join(" \u2192 ");
|
|
3852
|
+
parts.push(`Phases: ${phaseHistory} \u2192 ${this.wm.currentPhase}(current)`);
|
|
3853
|
+
} else {
|
|
3854
|
+
parts.push(`Phase: ${this.wm.currentPhase}`);
|
|
3855
|
+
}
|
|
3856
|
+
const recent = this.wm.recentStepSummaries.slice(-3).join("; ");
|
|
3857
|
+
parts.push(`Recent: ${recent}`);
|
|
3858
|
+
const statusParts = [];
|
|
3859
|
+
if (this.wm.successStreak > 0) {
|
|
3860
|
+
statusParts.push(`${this.wm.successStreak} consecutive successes`);
|
|
3861
|
+
}
|
|
3862
|
+
if (this.wm.failureCount > 0) {
|
|
3863
|
+
statusParts.push(`${this.wm.failureCount} consecutive failures`);
|
|
3864
|
+
}
|
|
3865
|
+
if (statusParts.length > 0) {
|
|
3866
|
+
parts.push(`Status: ${statusParts.join(", ")}`);
|
|
3867
|
+
}
|
|
3868
|
+
parts.push(`URL: ${this.wm.currentPageContext}`);
|
|
3869
|
+
return parts.join("\n");
|
|
3870
|
+
}
|
|
3871
|
+
/**
|
|
3872
|
+
* URL パターン変化 + アクションタイプのクラスタリングでフェーズ遷移を検出。
|
|
3873
|
+
* AI 呼び出し不要のヒューリスティック。
|
|
3874
|
+
*/
|
|
3875
|
+
detectPhaseTransition(stepOrdinal, url, actionType) {
|
|
3876
|
+
const urlPath = extractUrlPath(url);
|
|
3877
|
+
const lastUrlPath = extractUrlPath(this.wm.lastUrl);
|
|
3878
|
+
const pathChanged = urlPath !== lastUrlPath && this.wm.lastUrl !== "";
|
|
3879
|
+
if (actionType === this.wm.lastActionType) {
|
|
3880
|
+
this.wm.actionTypeRun++;
|
|
3881
|
+
} else {
|
|
3882
|
+
this.wm.actionTypeRun = 1;
|
|
3883
|
+
}
|
|
3884
|
+
const newPhase = detectPhaseFromContext(urlPath, actionType, this.wm.actionTypeRun);
|
|
3885
|
+
if (newPhase !== this.wm.currentPhase && (pathChanged || this.wm.actionTypeRun >= 3)) {
|
|
3886
|
+
if (this.wm.currentPhase !== "initial") {
|
|
3887
|
+
const lastPhase = this.wm.completedPhases[this.wm.completedPhases.length - 1];
|
|
3888
|
+
const startStep = lastPhase ? (lastPhase.endStep ?? 0) + 1 : 1;
|
|
3889
|
+
this.wm.completedPhases.push({
|
|
3890
|
+
name: this.wm.currentPhase,
|
|
3891
|
+
startStep,
|
|
3892
|
+
endStep: stepOrdinal - 1,
|
|
3893
|
+
summary: `${this.wm.currentPhase} completed`
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3896
|
+
this.wm.currentPhase = newPhase;
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
};
|
|
3900
|
+
function extractUrlPath(url) {
|
|
3901
|
+
try {
|
|
3902
|
+
const parsed = new URL(url);
|
|
3903
|
+
return parsed.pathname;
|
|
3904
|
+
} catch {
|
|
3905
|
+
return url;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
function detectPhaseFromContext(urlPath, actionType, actionTypeRun) {
|
|
3909
|
+
if (/\/(login|signin|auth)/i.test(urlPath)) return "authentication";
|
|
3910
|
+
if (/\/(dashboard|home|overview)/i.test(urlPath)) return "navigation";
|
|
3911
|
+
if (/\/(settings|config|preferences)/i.test(urlPath)) return "configuration";
|
|
3912
|
+
if (actionType === "input" && actionTypeRun >= 2) return "form_filling";
|
|
3913
|
+
if (actionType === "extract" && actionTypeRun >= 2) return "data_extraction";
|
|
3914
|
+
if (actionType === "click" && actionTypeRun >= 3) return "navigation";
|
|
3915
|
+
if (actionType === "download") return "data_export";
|
|
3916
|
+
return "general";
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
// src/messaging/chat-bot.ts
|
|
3920
|
+
import { Chat } from "chat";
|
|
3921
|
+
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
3922
|
+
import { createTeamsAdapter } from "@chat-adapter/teams";
|
|
3923
|
+
import { createDiscordAdapter } from "@chat-adapter/discord";
|
|
3924
|
+
import { createMemoryState } from "@chat-adapter/state-memory";
|
|
3925
|
+
import { Hono } from "hono";
|
|
3926
|
+
import { serve } from "@hono/node-server";
|
|
3927
|
+
var ADAPTER_NAME = {
|
|
3928
|
+
slack: "slack",
|
|
3929
|
+
teams: "teams",
|
|
3930
|
+
discord: "discord"
|
|
3931
|
+
};
|
|
3932
|
+
var SharedChatBot = class {
|
|
3933
|
+
constructor(config) {
|
|
3934
|
+
this.bot = null;
|
|
3935
|
+
this.server = null;
|
|
3936
|
+
this.channel = null;
|
|
3937
|
+
this.initialized = false;
|
|
3938
|
+
this.config = config;
|
|
3939
|
+
}
|
|
3940
|
+
/**
|
|
3941
|
+
* 遅延初期化: Chat SDK インスタンスと Hono webhook サーバーを起動。
|
|
3942
|
+
* 複数回呼ばれても冪等。
|
|
3943
|
+
*/
|
|
3944
|
+
async ensureInitialized() {
|
|
3945
|
+
if (this.initialized) return;
|
|
3946
|
+
this.initialized = true;
|
|
3947
|
+
const adapter = this.createAdapter();
|
|
3948
|
+
this.bot = new Chat({
|
|
3949
|
+
userName: "refrain",
|
|
3950
|
+
adapters: { [ADAPTER_NAME[this.config.platform]]: adapter },
|
|
3951
|
+
state: createMemoryState(),
|
|
3952
|
+
logger: "silent"
|
|
3953
|
+
});
|
|
3954
|
+
if (!this.config.skipWebhookServer) {
|
|
3955
|
+
this.startWebhookServer();
|
|
3956
|
+
}
|
|
3957
|
+
await this.bot.initialize();
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* プロアクティブメッセージ用チャンネルを取得。
|
|
3961
|
+
* chat.channel("slack:C12345") 形式で指定。
|
|
3962
|
+
*/
|
|
3963
|
+
getChannel() {
|
|
3964
|
+
if (!this.bot) throw new Error("SharedChatBot not initialized. Call ensureInitialized() first.");
|
|
3965
|
+
if (!this.channel) {
|
|
3966
|
+
const adapterName = ADAPTER_NAME[this.config.platform];
|
|
3967
|
+
this.channel = this.bot.channel(`${adapterName}:${this.config.channelId}`);
|
|
3968
|
+
}
|
|
3969
|
+
return this.channel;
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* ボタンクリック(Action)ハンドラを登録。
|
|
3973
|
+
* 承認カードのボタン ID に対応するハンドラを登録する。
|
|
3974
|
+
*/
|
|
3975
|
+
onAction(actionIds, handler) {
|
|
3976
|
+
if (!this.bot) throw new Error("SharedChatBot not initialized. Call ensureInitialized() first.");
|
|
3977
|
+
this.bot.onAction(actionIds, (event) => handler({ actionId: event.actionId, value: event.value }));
|
|
3978
|
+
}
|
|
3979
|
+
/**
|
|
3980
|
+
* Chat SDK + webhook サーバーをシャットダウン。
|
|
3981
|
+
*/
|
|
3982
|
+
async dispose() {
|
|
3983
|
+
if (this.bot) {
|
|
3984
|
+
await this.bot.shutdown();
|
|
3985
|
+
this.bot = null;
|
|
3986
|
+
}
|
|
3987
|
+
if (this.server) {
|
|
3988
|
+
this.server.close();
|
|
3989
|
+
this.server = null;
|
|
3990
|
+
}
|
|
3991
|
+
this.channel = null;
|
|
3992
|
+
this.initialized = false;
|
|
3993
|
+
}
|
|
3994
|
+
createAdapter() {
|
|
3995
|
+
const creds = this.config.credentials;
|
|
3996
|
+
switch (this.config.platform) {
|
|
3997
|
+
case "slack":
|
|
3998
|
+
return creds ? createSlackAdapter({
|
|
3999
|
+
botToken: creds.SLACK_BOT_TOKEN,
|
|
4000
|
+
signingSecret: creds.SLACK_SIGNING_SECRET
|
|
4001
|
+
}) : createSlackAdapter();
|
|
4002
|
+
case "teams":
|
|
4003
|
+
return creds ? createTeamsAdapter({
|
|
4004
|
+
appId: creds.TEAMS_APP_ID,
|
|
4005
|
+
appPassword: creds.TEAMS_APP_PASSWORD,
|
|
4006
|
+
appTenantId: creds.TEAMS_APP_TENANT_ID
|
|
4007
|
+
}) : createTeamsAdapter();
|
|
4008
|
+
case "discord":
|
|
4009
|
+
return creds ? createDiscordAdapter({
|
|
4010
|
+
botToken: creds.DISCORD_BOT_TOKEN,
|
|
4011
|
+
publicKey: creds.DISCORD_PUBLIC_KEY,
|
|
4012
|
+
applicationId: creds.DISCORD_APPLICATION_ID
|
|
4013
|
+
}) : createDiscordAdapter();
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
startWebhookServer() {
|
|
4017
|
+
const app = new Hono();
|
|
4018
|
+
const platform = this.config.platform;
|
|
4019
|
+
app.post(`/webhooks/${platform}`, async (c) => {
|
|
4020
|
+
if (!this.bot) return c.text("Bot not initialized", 500);
|
|
4021
|
+
const webhookHandler = this.bot.webhooks[ADAPTER_NAME[platform]];
|
|
4022
|
+
if (!webhookHandler) return c.text("Unknown platform", 400);
|
|
4023
|
+
return webhookHandler(c.req.raw);
|
|
4024
|
+
});
|
|
4025
|
+
this.server = serve({ fetch: app.fetch, port: this.config.callbackPort });
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
|
|
4029
|
+
// src/messaging/config.ts
|
|
4030
|
+
var DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
|
|
4031
|
+
var DEFAULT_CALLBACK_PORT = 3100;
|
|
4032
|
+
function loadChatConfig(platform, overrides) {
|
|
4033
|
+
const channelId = loadChannelId(platform);
|
|
4034
|
+
const callbackPort = overrides?.callbackPort ?? parseIntEnv("CHAT_CALLBACK_PORT") ?? parseIntEnv("SLACK_CALLBACK_PORT") ?? DEFAULT_CALLBACK_PORT;
|
|
4035
|
+
const approvalTimeoutMs = overrides?.approvalTimeoutMs ?? parseIntEnv("CHAT_APPROVAL_TIMEOUT_MS") ?? parseIntEnv("SLACK_APPROVAL_TIMEOUT_MS") ?? DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
4036
|
+
return { platform, channelId, callbackPort, approvalTimeoutMs };
|
|
4037
|
+
}
|
|
4038
|
+
function loadChannelId(platform) {
|
|
4039
|
+
const envMap = {
|
|
4040
|
+
slack: "SLACK_CHANNEL_ID",
|
|
4041
|
+
teams: "TEAMS_CHANNEL_ID",
|
|
4042
|
+
discord: "DISCORD_CHANNEL_ID"
|
|
4043
|
+
};
|
|
4044
|
+
const envKey = envMap[platform];
|
|
4045
|
+
const value = process.env[envKey];
|
|
4046
|
+
if (!value) {
|
|
4047
|
+
throw new Error(`${envKey} environment variable is required for ${platform} messaging`);
|
|
4048
|
+
}
|
|
4049
|
+
return value;
|
|
4050
|
+
}
|
|
4051
|
+
function parseIntEnv(key) {
|
|
4052
|
+
const v = process.env[key];
|
|
4053
|
+
if (!v) return void 0;
|
|
4054
|
+
const n = Number.parseInt(v, 10);
|
|
4055
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
// src/runbook-executor/self-heal-analyzer.ts
|
|
4059
|
+
import { z as z6 } from "zod";
|
|
4060
|
+
var suggestionSchema = z6.array(
|
|
4061
|
+
z6.object({
|
|
4062
|
+
stepOrdinal: z6.number().describe("\u5931\u6557\u3057\u305F\u30B9\u30C6\u30C3\u30D7\u306E ordinal"),
|
|
4063
|
+
stepDescription: z6.string().describe("\u30B9\u30C6\u30C3\u30D7\u306E\u8AAC\u660E"),
|
|
4064
|
+
error: z6.string().describe("\u5931\u6557\u30A8\u30E9\u30FC\u306E\u8981\u7D04"),
|
|
4065
|
+
runbookFix: z6.string().describe(
|
|
4066
|
+
"\u624B\u9806\u66F8\uFF08YAML\uFF09\u3078\u306E\u4FEE\u6B63\u63D0\u6848: \u30BB\u30EC\u30AF\u30BF\u5909\u66F4\u3001\u30B9\u30C6\u30C3\u30D7\u8FFD\u52A0/\u524A\u9664\u3001\u30A2\u30AF\u30B7\u30E7\u30F3\u5024\u306E\u66F4\u65B0\u306A\u3069"
|
|
4067
|
+
),
|
|
4068
|
+
contextFix: z6.string().describe(
|
|
4069
|
+
"\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\uFF08markdown\uFF09\u3078\u306E\u4FEE\u6B63\u63D0\u6848: \u30DA\u30FC\u30B8\u69CB\u9020\u5909\u66F4\u306E\u8A18\u8F09\u3001\u524D\u63D0\u6761\u4EF6\u306E\u8FFD\u52A0\u306A\u3069\u3002context \u672A\u63D0\u4F9B\u6642\u306F\u7A7A\u6587\u5B57"
|
|
4070
|
+
),
|
|
4071
|
+
severity: z6.enum(["error", "warning"]).describe("error: \u5B9F\u884C\u4E0D\u53EF\u3001warning: \u4E0D\u5B89\u5B9A"),
|
|
4072
|
+
failureCategory: z6.enum([
|
|
4073
|
+
"element_not_found",
|
|
4074
|
+
"element_stale",
|
|
4075
|
+
"page_structure_changed",
|
|
4076
|
+
"navigation_timeout",
|
|
4077
|
+
"action_failed",
|
|
4078
|
+
"unknown"
|
|
4079
|
+
]).optional().describe("\u30A8\u30E9\u30FC\u5206\u985E\u30AB\u30C6\u30B4\u30EA"),
|
|
4080
|
+
suggestedStrategy: z6.string().optional().describe("\u30AB\u30C6\u30B4\u30EA\u306B\u57FA\u3065\u304F\u63A8\u5968\u5BFE\u5FDC")
|
|
4081
|
+
})
|
|
4082
|
+
);
|
|
4083
|
+
async function analyzeDebugResult(report, runbook, retryWarningThreshold, contextMarkdown, aiProvider) {
|
|
4084
|
+
const warningSteps = extractWarningSteps(report, retryWarningThreshold);
|
|
4085
|
+
const failedSteps = report.steps.filter((s) => s.status === "failed");
|
|
4086
|
+
if (failedSteps.length === 0 && warningSteps.length === 0) {
|
|
4087
|
+
return { executionReport: report, warningSteps: [], suggestions: [] };
|
|
4088
|
+
}
|
|
4089
|
+
const suggestions = await generateSuggestions(report, runbook, contextMarkdown, aiProvider);
|
|
4090
|
+
return { executionReport: report, warningSteps, suggestions };
|
|
4091
|
+
}
|
|
4092
|
+
function extractWarningSteps(report, threshold) {
|
|
4093
|
+
const warnings = [];
|
|
4094
|
+
for (const step of report.steps) {
|
|
4095
|
+
if (step.retryCount !== void 0 && step.retryCount >= threshold) {
|
|
4096
|
+
warnings.push({
|
|
4097
|
+
ordinal: step.ordinal,
|
|
4098
|
+
description: step.description,
|
|
4099
|
+
retryCount: step.retryCount,
|
|
4100
|
+
threshold
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
return warnings;
|
|
4105
|
+
}
|
|
4106
|
+
async function generateSuggestions(report, runbook, contextMarkdown, aiProvider) {
|
|
4107
|
+
const failedSteps = report.steps.filter((s) => s.status === "failed");
|
|
4108
|
+
if (failedSteps.length === 0) return [];
|
|
4109
|
+
const failedInfo = failedSteps.map((s) => {
|
|
4110
|
+
const retryInfo = s.retryDetails ? s.retryDetails.map(
|
|
4111
|
+
(d) => ` attempt ${d.attempt}: ${d.failureReason} (elements: ${d.snapshotElementCount}, changed: ${d.snapshotChanged}${d.aiReasoning ? `, AI: ${d.aiReasoning.slice(0, 100)}` : ""})`
|
|
4112
|
+
).join("\n") : " \u30EA\u30C8\u30E9\u30A4\u8A73\u7D30\u306A\u3057";
|
|
4113
|
+
const categoryInfo = s.failureCategory ? `
|
|
4114
|
+
\u30A8\u30E9\u30FC\u5206\u985E: ${s.failureCategory}
|
|
4115
|
+
\u63A8\u5968\u5BFE\u5FDC: ${getRecoveryHint(s.failureCategory)}` : "";
|
|
4116
|
+
return `Step #${s.ordinal}: ${s.description}
|
|
4117
|
+
\u30A8\u30E9\u30FC: ${s.error}
|
|
4118
|
+
\u30EA\u30C8\u30E9\u30A4\u56DE\u6570: ${s.retryCount ?? 0}
|
|
4119
|
+
${retryInfo}${categoryInfo}`;
|
|
4120
|
+
});
|
|
4121
|
+
const stepsInfo = runbook.steps.map((s) => {
|
|
4122
|
+
const selector = s.action.selector ? JSON.stringify(s.action.selector) : "\u306A\u3057";
|
|
4123
|
+
const valuePart = s.action.value ? `
|
|
4124
|
+
\u5024: ${s.action.value}` : "";
|
|
4125
|
+
return `Step #${s.ordinal}: ${s.description}
|
|
4126
|
+
\u30A2\u30AF\u30B7\u30E7\u30F3: ${s.action.type}${valuePart}
|
|
4127
|
+
\u30BB\u30EC\u30AF\u30BF: ${selector}
|
|
4128
|
+
URL: ${s.url}`;
|
|
4129
|
+
});
|
|
4130
|
+
const system = getSuggestionSystemPrompt();
|
|
4131
|
+
const userPrompt = buildSuggestionUserPrompt(failedInfo, stepsInfo, runbook, contextMarkdown);
|
|
4132
|
+
try {
|
|
4133
|
+
const model = aiProvider ? aiProvider.getModel("review") : getModel("review");
|
|
4134
|
+
const { object } = await trackedGenerateObject("review", {
|
|
4135
|
+
model,
|
|
4136
|
+
messages: [
|
|
4137
|
+
{
|
|
4138
|
+
role: "system",
|
|
4139
|
+
content: system,
|
|
4140
|
+
providerOptions: {
|
|
4141
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
4142
|
+
}
|
|
4143
|
+
},
|
|
4144
|
+
{ role: "user", content: userPrompt }
|
|
4145
|
+
],
|
|
4146
|
+
schema: suggestionSchema,
|
|
4147
|
+
temperature: 0.2
|
|
4148
|
+
});
|
|
4149
|
+
return object;
|
|
4150
|
+
} catch {
|
|
4151
|
+
return [];
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
// src/messaging/cards.ts
|
|
4156
|
+
import {
|
|
4157
|
+
Card,
|
|
4158
|
+
CardText,
|
|
4159
|
+
Button,
|
|
4160
|
+
Actions,
|
|
4161
|
+
Section,
|
|
4162
|
+
Fields,
|
|
4163
|
+
Field,
|
|
4164
|
+
Divider
|
|
4165
|
+
} from "chat";
|
|
4166
|
+
var ACTION_IDS = {
|
|
4167
|
+
approve: "rfn-approve",
|
|
4168
|
+
skip: "rfn-skip",
|
|
4169
|
+
abort: "rfn-abort"
|
|
4170
|
+
};
|
|
4171
|
+
var RISK_EMOJI = {
|
|
4172
|
+
low: "\u{1F7E2}",
|
|
4173
|
+
// green circle
|
|
4174
|
+
medium: "\u26A0\uFE0F",
|
|
4175
|
+
// warning
|
|
4176
|
+
high: "\u{1F534}"
|
|
4177
|
+
// red circle
|
|
4178
|
+
};
|
|
4179
|
+
function buildApprovalCard(step, context, metadata) {
|
|
4180
|
+
const riskEmoji = RISK_EMOJI[step.riskLevel] ?? "";
|
|
4181
|
+
const goalLine = context?.goal ? `${t("chat.taskLabel")}${context.goal}
|
|
4182
|
+
` : "";
|
|
4183
|
+
const buttonId = (actionId) => metadata ? `${actionId}:${metadata.jobId}:${metadata.stepOrdinal}` : actionId;
|
|
4184
|
+
const buttonValue = metadata ? `${metadata.jobId}:${metadata.stepOrdinal}` : void 0;
|
|
4185
|
+
return Card({
|
|
4186
|
+
title: t("chat.approvalRequest"),
|
|
4187
|
+
children: [
|
|
4188
|
+
Section([
|
|
4189
|
+
CardText(
|
|
4190
|
+
tf("chat.approvalDescription", {
|
|
4191
|
+
goalLine,
|
|
4192
|
+
description: step.description,
|
|
4193
|
+
riskEmoji,
|
|
4194
|
+
riskLevel: step.riskLevel
|
|
4195
|
+
})
|
|
4196
|
+
)
|
|
4197
|
+
]),
|
|
4198
|
+
Actions([
|
|
4199
|
+
Button({ id: buttonId(ACTION_IDS.approve), label: t("chat.approveButton"), style: "primary", value: buttonValue }),
|
|
4200
|
+
Button({ id: buttonId(ACTION_IDS.skip), label: t("chat.skipButton"), value: buttonValue }),
|
|
4201
|
+
Button({ id: buttonId(ACTION_IDS.abort), label: t("chat.abortButton"), style: "danger", value: buttonValue })
|
|
4202
|
+
])
|
|
4203
|
+
]
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
function buildApprovalResultCard(step, resultText) {
|
|
4207
|
+
return Card({
|
|
4208
|
+
children: [
|
|
4209
|
+
Section([
|
|
4210
|
+
CardText(`*${t("chat.stepPrefix")} ${step.ordinal}* ${step.description}
|
|
4211
|
+
${resultText}`)
|
|
4212
|
+
])
|
|
4213
|
+
]
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
function statusEmoji(report) {
|
|
4217
|
+
if ("totalRows" in report) {
|
|
4218
|
+
const b = report;
|
|
4219
|
+
if (b.failed === 0) return "\u2705";
|
|
4220
|
+
if (b.succeeded === 0) return "\u274C";
|
|
4221
|
+
return "\u26A0\uFE0F";
|
|
4222
|
+
}
|
|
4223
|
+
const r = report;
|
|
4224
|
+
if (r.aborted) return "\u{1F6D1}";
|
|
4225
|
+
if (r.failed === 0) return "\u2705";
|
|
4226
|
+
if (r.succeeded === 0) return "\u274C";
|
|
4227
|
+
return "\u26A0\uFE0F";
|
|
4228
|
+
}
|
|
4229
|
+
function buildNotificationCard(report) {
|
|
4230
|
+
const isBatch = "totalRows" in report;
|
|
4231
|
+
return isBatch ? buildBatchCard(report) : buildSingleCard(report);
|
|
4232
|
+
}
|
|
4233
|
+
function buildSingleCard(report) {
|
|
4234
|
+
const emoji = statusEmoji(report);
|
|
4235
|
+
const abortTag = report.aborted ? ` \u{1F6D1} ${t("chat.abortTag")}` : "";
|
|
4236
|
+
const children = [
|
|
4237
|
+
Section([
|
|
4238
|
+
CardText(`*${emoji} ${report.runbookTitle}*`)
|
|
4239
|
+
]),
|
|
4240
|
+
Fields([
|
|
4241
|
+
Field({ label: t("chat.succeededFieldLabel"), value: `\u2705 ${report.succeeded}` }),
|
|
4242
|
+
Field({ label: t("chat.failedFieldLabel"), value: `\u274C ${report.failed}` }),
|
|
4243
|
+
Field({ label: t("chat.skippedFieldLabel"), value: `\u23E9 ${report.skipped}` }),
|
|
4244
|
+
Field({ label: t("chat.durationFieldLabel"), value: `\u23F1 ${formatDuration(report.totalDurationMs)}${abortTag}` })
|
|
4245
|
+
])
|
|
4246
|
+
];
|
|
4247
|
+
const failedSteps = report.steps.filter((s) => s.status === "failed");
|
|
4248
|
+
if (failedSteps.length > 0) {
|
|
4249
|
+
children.push(Divider());
|
|
4250
|
+
children.push(
|
|
4251
|
+
Section([
|
|
4252
|
+
CardText(`*${t("chat.failedLabel")}*
|
|
4253
|
+
${formatFailedSteps(failedSteps)}`)
|
|
4254
|
+
])
|
|
4255
|
+
);
|
|
4256
|
+
}
|
|
4257
|
+
return Card({ children });
|
|
4258
|
+
}
|
|
4259
|
+
function buildBatchCard(report) {
|
|
4260
|
+
const emoji = statusEmoji(report);
|
|
4261
|
+
const children = [
|
|
4262
|
+
Section([
|
|
4263
|
+
CardText(`*${emoji} ${report.runbookTitle} (${t("chat.batchTag")})*`)
|
|
4264
|
+
]),
|
|
4265
|
+
Fields([
|
|
4266
|
+
Field({ label: t("chat.totalFieldLabel"), value: tf("chat.totalRows", { count: report.totalRows }) }),
|
|
4267
|
+
Field({ label: t("chat.succeededFieldLabel"), value: `\u2705 ${report.succeeded}` }),
|
|
4268
|
+
Field({ label: t("chat.failedFieldLabel"), value: `\u274C ${report.failed}` }),
|
|
4269
|
+
Field({ label: t("chat.durationFieldLabel"), value: `\u23F1 ${formatDuration(report.totalDurationMs)}` })
|
|
4270
|
+
])
|
|
4271
|
+
];
|
|
4272
|
+
const failedRows = report.rows.filter((r) => r.report.failed > 0 || r.report.aborted);
|
|
4273
|
+
if (failedRows.length > 0) {
|
|
4274
|
+
const rowLines = failedRows.slice(0, 10).map((row) => {
|
|
4275
|
+
const failed = row.report.steps.filter((s) => s.status === "failed");
|
|
4276
|
+
const stepInfo = failed.map((s) => `#${s.ordinal}`).join(", ");
|
|
4277
|
+
return `- ${row.rowLabel}: ${stepInfo}`;
|
|
4278
|
+
});
|
|
4279
|
+
if (failedRows.length > 10) {
|
|
4280
|
+
rowLines.push(tf("chat.moreRows", { count: failedRows.length - 10 }));
|
|
4281
|
+
}
|
|
4282
|
+
children.push(Divider());
|
|
4283
|
+
children.push(
|
|
4284
|
+
Section([
|
|
4285
|
+
CardText(`*${t("chat.failedRows")}*
|
|
4286
|
+
${rowLines.join("\n")}`)
|
|
4287
|
+
])
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4290
|
+
return Card({ children });
|
|
4291
|
+
}
|
|
4292
|
+
function buildDebugNotificationCard(report) {
|
|
4293
|
+
const r = report.executionReport;
|
|
4294
|
+
const children = [
|
|
4295
|
+
Section([
|
|
4296
|
+
CardText(`*\u{1F50D} ${tf("chat.debugFailure", { title: r.runbookTitle })}*`)
|
|
4297
|
+
]),
|
|
4298
|
+
Fields([
|
|
4299
|
+
Field({ label: t("chat.succeededFieldLabel"), value: `\u2705 ${r.succeeded}` }),
|
|
4300
|
+
Field({ label: t("chat.failedFieldLabel"), value: `\u274C ${r.failed}` }),
|
|
4301
|
+
Field({ label: t("chat.skippedFieldLabel"), value: `\u23E9 ${r.skipped}` }),
|
|
4302
|
+
Field({ label: t("chat.durationFieldLabel"), value: `\u23F1 ${formatDuration(r.totalDurationMs)}` })
|
|
4303
|
+
])
|
|
4304
|
+
];
|
|
4305
|
+
const failedSteps = r.steps.filter((s) => s.status === "failed");
|
|
4306
|
+
if (failedSteps.length > 0) {
|
|
4307
|
+
children.push(Divider());
|
|
4308
|
+
children.push(
|
|
4309
|
+
Section([
|
|
4310
|
+
CardText(`*${t("chat.failedLabel")}*
|
|
4311
|
+
${formatFailedSteps(failedSteps, { limit: 15, errorMaxLen: 80, showRetries: true })}`)
|
|
4312
|
+
])
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
const allSuggestionLines = [];
|
|
4316
|
+
const runbookSuggestions = report.suggestions.filter((s) => s.runbookFix);
|
|
4317
|
+
for (const s of runbookSuggestions.slice(0, 10)) {
|
|
4318
|
+
const prefix = s.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
|
|
4319
|
+
allSuggestionLines.push(`${prefix} #${s.stepOrdinal} [YAML]: ${s.runbookFix.slice(0, 100)}`);
|
|
4320
|
+
}
|
|
4321
|
+
const contextSuggestions = report.suggestions.filter((s) => s.contextFix);
|
|
4322
|
+
for (const s of contextSuggestions.slice(0, 10)) {
|
|
4323
|
+
const prefix = s.severity === "error" ? "\u274C" : "\u26A0\uFE0F";
|
|
4324
|
+
allSuggestionLines.push(`${prefix} #${s.stepOrdinal} [ctx]: ${s.contextFix.slice(0, 100)}`);
|
|
4325
|
+
}
|
|
4326
|
+
const totalSuggestions = runbookSuggestions.length + contextSuggestions.length;
|
|
4327
|
+
if (allSuggestionLines.length > 0 && totalSuggestions > allSuggestionLines.length) {
|
|
4328
|
+
allSuggestionLines.push(tf("chat.moreItems", { count: totalSuggestions - allSuggestionLines.length }));
|
|
4329
|
+
}
|
|
4330
|
+
if (allSuggestionLines.length > 0) {
|
|
4331
|
+
children.push(Divider());
|
|
4332
|
+
children.push(
|
|
4333
|
+
Section([
|
|
4334
|
+
CardText(`*${t("chat.suggestionsLabel")}*
|
|
4335
|
+
${allSuggestionLines.join("\n")}`)
|
|
4336
|
+
])
|
|
4337
|
+
);
|
|
4338
|
+
}
|
|
4339
|
+
if (report.warningSteps.length > 0) {
|
|
4340
|
+
const lines = report.warningSteps.slice(0, 10).map(
|
|
4341
|
+
(w) => `- #${w.ordinal} ${w.description}: retry ${w.retryCount}/${w.threshold}`
|
|
4342
|
+
);
|
|
4343
|
+
if (report.warningSteps.length > 10) {
|
|
4344
|
+
lines.push(tf("chat.moreItems", { count: report.warningSteps.length - 10 }));
|
|
4345
|
+
}
|
|
4346
|
+
children.push(Divider());
|
|
4347
|
+
children.push(
|
|
4348
|
+
Section([
|
|
4349
|
+
CardText(`*${t("chat.retryWarningsLabel")}*
|
|
4350
|
+
${lines.join("\n")}`)
|
|
4351
|
+
])
|
|
4352
|
+
);
|
|
4353
|
+
}
|
|
4354
|
+
return Card({ children });
|
|
4355
|
+
}
|
|
4356
|
+
function buildGenerateNotificationCard(info) {
|
|
4357
|
+
const emoji = info.goalAchieved ? "\u2705" : "\u274C";
|
|
4358
|
+
return Card({
|
|
4359
|
+
children: [
|
|
4360
|
+
Section([
|
|
4361
|
+
CardText(`*${emoji} ${t("chat.generateComplete")}*`)
|
|
4362
|
+
]),
|
|
4363
|
+
Fields([
|
|
4364
|
+
Field({ label: t("chat.goalFieldLabel"), value: info.goal }),
|
|
4365
|
+
Field({ label: t("chat.goalAchievedFieldLabel"), value: info.goalAchieved ? "\u2705" : "\u274C" }),
|
|
4366
|
+
Field({ label: t("chat.stepsFieldLabel"), value: String(info.stepsCount) }),
|
|
4367
|
+
Field({ label: t("chat.yamlFieldLabel"), value: info.yamlGenerated ? "\u2705" : "\u274C" })
|
|
4368
|
+
])
|
|
4369
|
+
]
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
function formatFailedSteps(steps, options) {
|
|
4373
|
+
const { limit = 10, errorMaxLen = 100, showRetries = false } = options ?? {};
|
|
4374
|
+
const lines = steps.slice(0, limit).map((s) => {
|
|
4375
|
+
const errMsg = s.error ? `: ${s.error.slice(0, errorMaxLen)}` : "";
|
|
4376
|
+
const retries = showRetries && s.retryCount ? ` (retry:${s.retryCount})` : "";
|
|
4377
|
+
const categoryTag = s.failureCategory ? ` [${s.failureCategory}]` : "";
|
|
4378
|
+
return `- #${s.ordinal} ${s.description}${errMsg}${retries}${categoryTag}`;
|
|
4379
|
+
});
|
|
4380
|
+
if (steps.length > limit) {
|
|
4381
|
+
lines.push(tf("chat.moreSteps", { count: steps.length - limit }));
|
|
4382
|
+
}
|
|
4383
|
+
return lines.join("\n");
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
export {
|
|
4387
|
+
SELF_HEAL_DEFAULTS,
|
|
4388
|
+
applyExecutorDefaults,
|
|
4389
|
+
detectKeyKind,
|
|
4390
|
+
generateDebugToken,
|
|
4391
|
+
getDebugSigningKey,
|
|
4392
|
+
getDebugPublicKey,
|
|
4393
|
+
COMMUNITY_PLAN,
|
|
4394
|
+
PLAN_PRICING,
|
|
4395
|
+
TIER_TO_PLAN,
|
|
4396
|
+
resolvePlanFromTier,
|
|
4397
|
+
resolvePlan,
|
|
4398
|
+
getApiKey,
|
|
4399
|
+
enforceFeatureGates,
|
|
4400
|
+
validateStepLimit,
|
|
4401
|
+
validateBatchLimit,
|
|
4402
|
+
formatPlanLabel,
|
|
4403
|
+
normalizeUrlPattern,
|
|
4404
|
+
buildSelectorHint,
|
|
4405
|
+
loadCache,
|
|
4406
|
+
saveCache,
|
|
4407
|
+
CliConfirmationProvider,
|
|
4408
|
+
execute,
|
|
4409
|
+
mergeVariablesAndSecrets,
|
|
4410
|
+
resolveVariablesCore,
|
|
4411
|
+
planExecution,
|
|
4412
|
+
displayPlan,
|
|
4413
|
+
confirmPlan,
|
|
4414
|
+
RuntimeStore,
|
|
4415
|
+
SharedChatBot,
|
|
4416
|
+
DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
4417
|
+
DEFAULT_CALLBACK_PORT,
|
|
4418
|
+
loadChatConfig,
|
|
4419
|
+
ACTION_IDS,
|
|
4420
|
+
buildApprovalCard,
|
|
4421
|
+
buildApprovalResultCard,
|
|
4422
|
+
buildNotificationCard,
|
|
4423
|
+
buildDebugNotificationCard,
|
|
4424
|
+
buildGenerateNotificationCard,
|
|
4425
|
+
analyzeDebugResult
|
|
4426
|
+
};
|
|
4427
|
+
//# sourceMappingURL=chunk-H47NWH7N.js.map
|