@optima-chat/browser-cli 0.24.0 → 0.25.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/index.js +123 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -160,10 +160,11 @@ function clearSessionId(onlyIfEquals) {
|
|
|
160
160
|
|
|
161
161
|
// src/api.ts
|
|
162
162
|
var BrowserAPIError = class extends Error {
|
|
163
|
-
constructor(statusCode, errorCode, message) {
|
|
163
|
+
constructor(statusCode, errorCode, message, details) {
|
|
164
164
|
super(message);
|
|
165
165
|
this.statusCode = statusCode;
|
|
166
166
|
this.errorCode = errorCode;
|
|
167
|
+
this.details = details;
|
|
167
168
|
this.name = "BrowserAPIError";
|
|
168
169
|
}
|
|
169
170
|
};
|
|
@@ -173,7 +174,7 @@ function toAPIError(status, statusText, body) {
|
|
|
173
174
|
return new BrowserAPIError(status, "http_error", `HTTP ${status}: ${detail}`);
|
|
174
175
|
}
|
|
175
176
|
if (detail?.message) {
|
|
176
|
-
return new BrowserAPIError(status, detail.error ?? "unknown", detail.message);
|
|
177
|
+
return new BrowserAPIError(status, detail.error ?? "unknown", detail.message, detail);
|
|
177
178
|
}
|
|
178
179
|
return new BrowserAPIError(status, "unknown", `HTTP ${status}: ${statusText}`);
|
|
179
180
|
}
|
|
@@ -199,7 +200,7 @@ function conversationHeader() {
|
|
|
199
200
|
const conv = conversationId();
|
|
200
201
|
return conv ? { "X-Optima-Conversation": conv } : {};
|
|
201
202
|
}
|
|
202
|
-
async function request(path, options = {}) {
|
|
203
|
+
async function request(path, options = {}, originalTaskConversation) {
|
|
203
204
|
const url = `${BROWSER_API_URL}${path}`;
|
|
204
205
|
const auth = authHeader();
|
|
205
206
|
let response;
|
|
@@ -209,7 +210,7 @@ async function request(path, options = {}) {
|
|
|
209
210
|
headers: {
|
|
210
211
|
"Content-Type": "application/json",
|
|
211
212
|
...auth,
|
|
212
|
-
...conversationHeader(),
|
|
213
|
+
...originalTaskConversation === void 0 ? conversationHeader() : originalTaskConversation === null ? {} : { "X-Optima-Conversation": originalTaskConversation },
|
|
213
214
|
...options.headers
|
|
214
215
|
}
|
|
215
216
|
});
|
|
@@ -322,6 +323,12 @@ var BrowserAPI = class {
|
|
|
322
323
|
throw err;
|
|
323
324
|
}
|
|
324
325
|
}
|
|
326
|
+
async handoff(sessionId, timeoutSeconds) {
|
|
327
|
+
return request(`/api/sessions/${sessionId}/handoff`, { method: "POST", body: JSON.stringify({ timeout_seconds: timeoutSeconds }) });
|
|
328
|
+
}
|
|
329
|
+
async resumeHandoff(sessionId) {
|
|
330
|
+
return request(`/api/sessions/${sessionId}/handoff`, { method: "DELETE" });
|
|
331
|
+
}
|
|
325
332
|
async getStatus(sessionId) {
|
|
326
333
|
return request(`/api/sessions/${sessionId}/status`);
|
|
327
334
|
}
|
|
@@ -443,8 +450,32 @@ var BrowserAPI = class {
|
|
|
443
450
|
body: JSON.stringify(payload)
|
|
444
451
|
});
|
|
445
452
|
}
|
|
453
|
+
async settleScriptTaskSession(taskId) {
|
|
454
|
+
const task = await this.getScriptTask(taskId);
|
|
455
|
+
const proof = task.execution_evidence;
|
|
456
|
+
if (task.id !== taskId || !["failed", "cancelled", "needs_fallback", "completed"].includes(task.status) || !proof || proof.protocol !== "script-execution/v1" || proof.task_id !== taskId || proof.worker_ended !== true || !(proof.conversation === null || typeof proof.conversation === "string")) {
|
|
457
|
+
throw new Error("Original task execution/conversation unresolved; refusing session settlement");
|
|
458
|
+
}
|
|
459
|
+
return request(
|
|
460
|
+
`/api/script-tasks/${encodeURIComponent(taskId)}/settle-session`,
|
|
461
|
+
{ method: "POST" },
|
|
462
|
+
proof.conversation
|
|
463
|
+
);
|
|
464
|
+
}
|
|
446
465
|
async getScriptTask(taskId) {
|
|
447
|
-
return request(`/api/script-tasks/${taskId}`);
|
|
466
|
+
return request(`/api/script-tasks/${encodeURIComponent(taskId)}`);
|
|
467
|
+
}
|
|
468
|
+
async sourceCapabilities() {
|
|
469
|
+
return request(`/api/script-source-capabilities`);
|
|
470
|
+
}
|
|
471
|
+
async observeSource(taskId) {
|
|
472
|
+
return request(`/api/script-tasks/${encodeURIComponent(taskId)}/source`);
|
|
473
|
+
}
|
|
474
|
+
async releaseSource(taskId) {
|
|
475
|
+
return request(`/api/script-tasks/${encodeURIComponent(taskId)}/source`, { method: "DELETE" });
|
|
476
|
+
}
|
|
477
|
+
async sourceScreenshot(taskId, observationId) {
|
|
478
|
+
return requestBinary(`/api/script-tasks/${encodeURIComponent(taskId)}/source/observations/${encodeURIComponent(observationId)}/screenshot`);
|
|
448
479
|
}
|
|
449
480
|
async sendFile(src, dest) {
|
|
450
481
|
return request("/api/files/send", {
|
|
@@ -491,6 +522,12 @@ function formatStatus(status) {
|
|
|
491
522
|
lines.push(`Status: ${status.status}`);
|
|
492
523
|
lines.push(`Created: ${status.created_at}`);
|
|
493
524
|
lines.push(`Idle: ${status.idle_seconds}s`);
|
|
525
|
+
if (status.handoff) {
|
|
526
|
+
lines.push(`Handoff: ${status.handoff.state}`);
|
|
527
|
+
lines.push(`Expires: ${status.handoff.expires_at}`);
|
|
528
|
+
lines.push(`Remaining: ${status.handoff.remaining_seconds}s`);
|
|
529
|
+
lines.push(`Recovery: ${status.handoff.recovery}`);
|
|
530
|
+
}
|
|
494
531
|
if (status.execution) {
|
|
495
532
|
const where = status.execution === "sidecar" ? "user's machine" : "server-driven";
|
|
496
533
|
lines.push(`Execution: ${status.execution} (${where})`);
|
|
@@ -572,6 +609,33 @@ function registerLifecycleCommands(program2) {
|
|
|
572
609
|
process.exit(1);
|
|
573
610
|
}
|
|
574
611
|
});
|
|
612
|
+
program2.command("handoff").description("Reserve the current session/tab for human review (bounded, no submission)").option("--timeout-seconds <seconds>", "Retention from now, 60\u20133600 seconds; retries keep the original deadline", "1800").action(async (opts) => {
|
|
613
|
+
try {
|
|
614
|
+
const seconds = Number(opts.timeoutSeconds);
|
|
615
|
+
if (!Number.isInteger(seconds) || seconds < 60 || seconds > 3600) {
|
|
616
|
+
throw new Error("timeout-seconds must be an integer between 60 and 3600");
|
|
617
|
+
}
|
|
618
|
+
const result = await api.handoff(loadSessionId(), seconds);
|
|
619
|
+
console.log(`Session: ${result.session_id}
|
|
620
|
+
Handoff: ${result.handoff.state}
|
|
621
|
+
Expires: ${result.handoff.expires_at}
|
|
622
|
+
Remaining: ${result.handoff.remaining_seconds}s
|
|
623
|
+
Recovery: ${result.handoff.recovery}`);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
console.error(formatError(err));
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
program2.command("handoff-resume").description("Release human retention without navigating, submitting, or closing the tab").action(async () => {
|
|
630
|
+
try {
|
|
631
|
+
const result = await api.resumeHandoff(loadSessionId());
|
|
632
|
+
console.log(`Session: ${result.session_id}
|
|
633
|
+
Handoff released. Inspect the current page and verify submission before restoring or retrying.`);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
console.error(formatError(err));
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
575
639
|
program2.command("status").description("Show session status").action(async () => {
|
|
576
640
|
try {
|
|
577
641
|
const sessionId = loadSessionId();
|
|
@@ -995,7 +1059,12 @@ function formatScriptTask(task) {
|
|
|
995
1059
|
lines.push("");
|
|
996
1060
|
}
|
|
997
1061
|
const sid = task.session_id;
|
|
998
|
-
|
|
1062
|
+
const source = output.source_handoff;
|
|
1063
|
+
if (source) {
|
|
1064
|
+
lines.push(JSON.stringify(source));
|
|
1065
|
+
lines.push(`Submission remains unknown. Read only: browser-cli observe-source ${task.id}`);
|
|
1066
|
+
lines.push(`Release without closing a page: browser-cli release-source ${task.id}`);
|
|
1067
|
+
} else lines.push(
|
|
999
1068
|
sid ? `Session ${sid} may still be open \u2014 continue with browser-cli --session ${sid} dom/click/input, or free it with browser-cli --session ${sid} close` : "Session id not reported \u2014 if one remains, close it by id: browser-cli --session <id> close"
|
|
1000
1069
|
);
|
|
1001
1070
|
} else if (task.status === "failed") {
|
|
@@ -1035,6 +1104,50 @@ function resolveSubmitSource(opts) {
|
|
|
1035
1104
|
throw new Error("\u5FC5\u987B\u7ED9 --code-file \u6216 --code-dir");
|
|
1036
1105
|
}
|
|
1037
1106
|
function registerScriptCommands(program2) {
|
|
1107
|
+
program2.command("observe-source <task-id>").description("Read the exact original task page without navigation or replay; does not renew retention").option("--out <directory>", "Save text/JSON evidence before attempting screenshot download").action(async (taskId, opts) => {
|
|
1108
|
+
try {
|
|
1109
|
+
const capability = await api.sourceCapabilities();
|
|
1110
|
+
if (capability.protocol !== "source-observation/v1" || !capability.task_bound_observation) {
|
|
1111
|
+
throw new Error("source_observation_unsupported: backend capability unavailable; do not launch a replacement page");
|
|
1112
|
+
}
|
|
1113
|
+
const result = await api.observeSource(taskId);
|
|
1114
|
+
if (opts.out) {
|
|
1115
|
+
const directory = resolve(opts.out);
|
|
1116
|
+
mkdirSync2(directory, { recursive: true });
|
|
1117
|
+
const evidence = join3(directory, `${result.observation.id}.json`);
|
|
1118
|
+
writeFileSync3(evidence, JSON.stringify(result, null, 2));
|
|
1119
|
+
if (result.observation.screenshot_path) {
|
|
1120
|
+
try {
|
|
1121
|
+
writeFileSync3(
|
|
1122
|
+
join3(directory, `${result.observation.id}.png`),
|
|
1123
|
+
await api.sourceScreenshot(taskId, result.observation.id)
|
|
1124
|
+
);
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
result.observation.screenshot_error = String(err);
|
|
1127
|
+
writeFileSync3(evidence, JSON.stringify(result, null, 2));
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
const details = err && typeof err === "object" && "details" in err ? err.details : void 0;
|
|
1134
|
+
if (opts.out && details) {
|
|
1135
|
+
const directory = resolve(opts.out);
|
|
1136
|
+
mkdirSync2(directory, { recursive: true });
|
|
1137
|
+
writeFileSync3(join3(directory, "source-refused.json"), JSON.stringify(details, null, 2));
|
|
1138
|
+
}
|
|
1139
|
+
console.error(formatError(err));
|
|
1140
|
+
process.exitCode = 1;
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
program2.command("release-source <task-id>").description("Release a task's bounded source lease; never closes or navigates a page").action(async (taskId) => {
|
|
1144
|
+
try {
|
|
1145
|
+
console.log(JSON.stringify(await api.releaseSource(taskId), null, 2));
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
console.error(formatError(err));
|
|
1148
|
+
process.exitCode = 1;
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1038
1151
|
program2.command("submit-script").description("Submit a script (write to EFS)").requiredOption("--name <name>", "Script name").option("--code-file <path>", "Path to Python script file (\u7B49\u4EF7\u4E8E\u300C\u53EA\u6709 script.py\u300D)").option("--code-dir <dir>", "Path to a skill directory (script.py + _*.py \u4E00\u8D77\u9001)").action(async (opts) => {
|
|
1039
1152
|
try {
|
|
1040
1153
|
const files = resolveSubmitSource(opts);
|
|
@@ -1126,7 +1239,7 @@ ${downloadHint(result)}`);
|
|
|
1126
1239
|
process.exit(1);
|
|
1127
1240
|
}
|
|
1128
1241
|
});
|
|
1129
|
-
program2.command("script-task").argument("<task-id>", "Task ID").description("Check script task status").option("--download [path]", "Save the task's exported file (default: ./<filename>)").option("--json", "Print the raw task (status / error / full output) as JSON").option(
|
|
1242
|
+
program2.command("script-task").argument("<task-id>", "Task ID").description("Check script task status").option("--settle-session", "Read the original task, then settle its ended channel under its server-recorded conversation; keeps the current session and never classifies publication").option("--download [path]", "Save the task's exported file (default: ./<filename>)").option("--json", "Print the raw task (status / error / full output) as JSON").option(
|
|
1130
1243
|
"--wait",
|
|
1131
1244
|
`Block until the task finishes (completed / failed / needs_fallback), at most --timeout seconds. Exit code: 0 completed, 2 failed / needs_fallback, 3 still running (call again), 4 waiting for the store owner (e.g. captcha; tell them, then --wait --through-human), 1 error`
|
|
1132
1245
|
).option("--through-human", "With --wait: keep waiting while the script waits for the store owner (exit 4 otherwise)").option("--timeout <seconds>", `With --wait: max seconds to wait (default ${WAIT_DEFAULT_TIMEOUT_S}, max ${WAIT_MAX_TIMEOUT_S})`).action(async (taskId, opts) => {
|
|
@@ -1136,6 +1249,7 @@ ${downloadHint(result)}`);
|
|
|
1136
1249
|
}
|
|
1137
1250
|
if (opts.timeout !== void 0 && !opts.wait) throw new Error("--timeout \u53EA\u80FD\u548C --wait \u4E00\u8D77\u7528");
|
|
1138
1251
|
if (opts.throughHuman && !opts.wait) throw new Error("--through-human \u53EA\u80FD\u548C --wait \u4E00\u8D77\u7528");
|
|
1252
|
+
if (opts.settleSession && (opts.wait || opts.download !== void 0)) throw new Error("--settle-session cannot combine with --wait/--download");
|
|
1139
1253
|
const timeoutS = opts.timeout === void 0 ? WAIT_DEFAULT_TIMEOUT_S : parseWaitTimeout(opts.timeout);
|
|
1140
1254
|
let task;
|
|
1141
1255
|
if (opts.wait) {
|
|
@@ -1154,12 +1268,12 @@ ${downloadHint(result)}`);
|
|
|
1154
1268
|
return;
|
|
1155
1269
|
}
|
|
1156
1270
|
} else {
|
|
1157
|
-
task = await api.getScriptTask(taskId);
|
|
1271
|
+
task = opts.settleSession ? await api.settleScriptTaskSession(taskId) : await api.getScriptTask(taskId);
|
|
1158
1272
|
}
|
|
1159
1273
|
const stoppedExit = () => {
|
|
1160
1274
|
if (opts.wait && task.status !== "completed") process.exit(EXIT_TASK_STOPPED);
|
|
1161
1275
|
};
|
|
1162
|
-
if (task.status === "needs_fallback" && task.session_id) saveSessionId(task.session_id);
|
|
1276
|
+
if (!opts.settleSession && task.status === "needs_fallback" && task.session_id) saveSessionId(task.session_id);
|
|
1163
1277
|
if (opts.json) {
|
|
1164
1278
|
console.log(JSON.stringify(task, null, 2));
|
|
1165
1279
|
stoppedExit();
|