@optima-chat/browser-cli 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +81 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -116,6 +116,48 @@ var config = loadConfig();
|
|
|
116
116
|
var BROWSER_API_URL = config.apiUrl;
|
|
117
117
|
var AUTH = config.auth;
|
|
118
118
|
|
|
119
|
+
// src/session.ts
|
|
120
|
+
import { readFileSync as readFileSync2, writeFileSync, unlinkSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
121
|
+
import { join as join2 } from "path";
|
|
122
|
+
import { homedir as homedir2 } from "os";
|
|
123
|
+
var OPTIMA_DIR = join2(homedir2(), ".optima");
|
|
124
|
+
function conversationId() {
|
|
125
|
+
const raw = process.env.SESSION_ID;
|
|
126
|
+
return raw ? raw.replace(/[^A-Za-z0-9_-]/g, "_") : null;
|
|
127
|
+
}
|
|
128
|
+
function sessionFile() {
|
|
129
|
+
const conv = conversationId();
|
|
130
|
+
return join2(OPTIMA_DIR, conv ? `browser-session-id.${conv}` : "browser-session-id");
|
|
131
|
+
}
|
|
132
|
+
var _sessionOverride = null;
|
|
133
|
+
function setSessionOverride(id) {
|
|
134
|
+
_sessionOverride = id;
|
|
135
|
+
}
|
|
136
|
+
function saveSessionId(id) {
|
|
137
|
+
mkdirSync(OPTIMA_DIR, { recursive: true });
|
|
138
|
+
writeFileSync(sessionFile(), id, "utf-8");
|
|
139
|
+
}
|
|
140
|
+
function loadSessionId() {
|
|
141
|
+
if (_sessionOverride) {
|
|
142
|
+
return _sessionOverride;
|
|
143
|
+
}
|
|
144
|
+
const file = sessionFile();
|
|
145
|
+
if (!existsSync2(file)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"No active browser session. Run 'browser-cli launch' first. (If the backend says you already have one, close it by id: browser-cli --session <id> close)"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return readFileSync2(file, "utf-8").trim();
|
|
151
|
+
}
|
|
152
|
+
function clearSessionId(onlyIfEquals) {
|
|
153
|
+
const file = sessionFile();
|
|
154
|
+
if (!existsSync2(file)) return;
|
|
155
|
+
if (onlyIfEquals !== void 0 && readFileSync2(file, "utf-8").trim() !== onlyIfEquals) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
unlinkSync(file);
|
|
159
|
+
}
|
|
160
|
+
|
|
119
161
|
// src/api.ts
|
|
120
162
|
var BrowserAPIError = class extends Error {
|
|
121
163
|
constructor(statusCode, errorCode, message) {
|
|
@@ -153,6 +195,10 @@ function withAuthContext(err) {
|
|
|
153
195
|
err.message += `\uFF08\u51ED\u8BC1\u6765\u6E90 ${AUTH.source}${exp}\uFF0Cfp=${fp}\uFF09`;
|
|
154
196
|
return err;
|
|
155
197
|
}
|
|
198
|
+
function conversationHeader() {
|
|
199
|
+
const conv = conversationId();
|
|
200
|
+
return conv ? { "X-Optima-Conversation": conv } : {};
|
|
201
|
+
}
|
|
156
202
|
async function request(path, options = {}) {
|
|
157
203
|
const url = `${BROWSER_API_URL}${path}`;
|
|
158
204
|
const auth = authHeader();
|
|
@@ -163,6 +209,7 @@ async function request(path, options = {}) {
|
|
|
163
209
|
headers: {
|
|
164
210
|
"Content-Type": "application/json",
|
|
165
211
|
...auth,
|
|
212
|
+
...conversationHeader(),
|
|
166
213
|
...options.headers
|
|
167
214
|
}
|
|
168
215
|
});
|
|
@@ -195,7 +242,7 @@ async function requestBinary(path) {
|
|
|
195
242
|
let response;
|
|
196
243
|
try {
|
|
197
244
|
response = await fetch(url, {
|
|
198
|
-
headers: auth
|
|
245
|
+
headers: { ...auth, ...conversationHeader() }
|
|
199
246
|
});
|
|
200
247
|
} catch {
|
|
201
248
|
throw new Error(
|
|
@@ -411,39 +458,6 @@ var BrowserAPI = class {
|
|
|
411
458
|
};
|
|
412
459
|
var api = new BrowserAPI();
|
|
413
460
|
|
|
414
|
-
// src/session.ts
|
|
415
|
-
import { readFileSync as readFileSync2, writeFileSync, unlinkSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
416
|
-
import { join as join2 } from "path";
|
|
417
|
-
import { homedir as homedir2 } from "os";
|
|
418
|
-
var OPTIMA_DIR = join2(homedir2(), ".optima");
|
|
419
|
-
var SESSION_FILE = join2(OPTIMA_DIR, "browser-session-id");
|
|
420
|
-
var _sessionOverride = null;
|
|
421
|
-
function setSessionOverride(id) {
|
|
422
|
-
_sessionOverride = id;
|
|
423
|
-
}
|
|
424
|
-
function saveSessionId(id) {
|
|
425
|
-
mkdirSync(OPTIMA_DIR, { recursive: true });
|
|
426
|
-
writeFileSync(SESSION_FILE, id, "utf-8");
|
|
427
|
-
}
|
|
428
|
-
function loadSessionId() {
|
|
429
|
-
if (_sessionOverride) {
|
|
430
|
-
return _sessionOverride;
|
|
431
|
-
}
|
|
432
|
-
if (!existsSync2(SESSION_FILE)) {
|
|
433
|
-
throw new Error(
|
|
434
|
-
"No active browser session. Run 'browser-cli launch' first. (If the backend says you already have one, close it by id: browser-cli --session <id> close)"
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
return readFileSync2(SESSION_FILE, "utf-8").trim();
|
|
438
|
-
}
|
|
439
|
-
function clearSessionId(onlyIfEquals) {
|
|
440
|
-
if (!existsSync2(SESSION_FILE)) return;
|
|
441
|
-
if (onlyIfEquals !== void 0 && readFileSync2(SESSION_FILE, "utf-8").trim() !== onlyIfEquals) {
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
unlinkSync(SESSION_FILE);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
461
|
// src/formatter.ts
|
|
448
462
|
var URL_STALE_WARNING = "\u26A0 URL/title may be stale: the live page probe failed, values above are cached. Verify with a screenshot before concluding a navigation failed.";
|
|
449
463
|
function formatPageState(state) {
|
|
@@ -789,10 +803,21 @@ var WAIT_DEFAULT_TIMEOUT_S = 90;
|
|
|
789
803
|
var WAIT_MAX_TIMEOUT_S = 570;
|
|
790
804
|
var EXIT_TASK_STOPPED = 2;
|
|
791
805
|
var EXIT_STILL_RUNNING = 3;
|
|
792
|
-
|
|
806
|
+
var EXIT_WAITING_HUMAN = 4;
|
|
807
|
+
function waitingHumanLine(task) {
|
|
808
|
+
const w = task.waiting_human;
|
|
809
|
+
if (!w) return null;
|
|
810
|
+
return `WAITING_HUMAN kind=${w.kind} since=${w.since} message=${JSON.stringify(w.message)}`;
|
|
811
|
+
}
|
|
812
|
+
function waitingHumanMessage(task) {
|
|
813
|
+
return `${waitingHumanLine(task)}
|
|
814
|
+
The script is paused waiting for the store owner to act in their browser. Tell the owner now, then call as a separate command: browser-cli script-task ${task.id} --wait --through-human`;
|
|
815
|
+
}
|
|
816
|
+
async function waitForScriptTask(taskId, timeoutMs, first, throughHuman = false) {
|
|
793
817
|
const deadline = Date.now() + timeoutMs;
|
|
794
818
|
let task = first ?? await api.getScriptTask(taskId);
|
|
795
819
|
while (task.status === "running") {
|
|
820
|
+
if (task.waiting_human && !throughHuman) return { task, timedOut: false, waitingHuman: true };
|
|
796
821
|
const left = deadline - Date.now();
|
|
797
822
|
if (left <= 0) return { task, timedOut: true };
|
|
798
823
|
await sleep(Math.min(POLL_INTERVAL_MS, left));
|
|
@@ -811,7 +836,9 @@ function stillRunningMessage(task, waitedS, nowMs = Date.now()) {
|
|
|
811
836
|
const started = task.created_at ? Date.parse(task.created_at) : Number.NaN;
|
|
812
837
|
const elapsed = Number.isFinite(started) ? `running for ${Math.max(0, Math.round((nowMs - started) / 1e3))}s` : `waited ${waitedS}s`;
|
|
813
838
|
const step = task.total_steps ? `, step ${task.current_step}/${task.total_steps}` : "";
|
|
814
|
-
|
|
839
|
+
const waiting = waitingHumanLine(task);
|
|
840
|
+
return `Still running (${elapsed}${step}). Call again as a separate command: browser-cli script-task ${task.id} --wait` + (waiting ? ` --through-human
|
|
841
|
+
${waiting}` : "");
|
|
815
842
|
}
|
|
816
843
|
var REQUIRED_ARG_FLAG = {
|
|
817
844
|
ziniao_profile_id: "--ziniao-profile <id>",
|
|
@@ -974,6 +1001,8 @@ function formatScriptTask(task) {
|
|
|
974
1001
|
}
|
|
975
1002
|
lines.push(...haltOutputFields(task));
|
|
976
1003
|
} else {
|
|
1004
|
+
const waiting = waitingHumanLine(task);
|
|
1005
|
+
if (waiting) lines.push(waiting);
|
|
977
1006
|
lines.push(`Script status: ${task.status} (step ${task.current_step}/${task.total_steps})`);
|
|
978
1007
|
}
|
|
979
1008
|
return lines.join("\n");
|
|
@@ -1049,7 +1078,12 @@ function registerScriptCommands(program2) {
|
|
|
1049
1078
|
console.log(`Wait with: browser-cli script-task ${task.id} --wait`);
|
|
1050
1079
|
return;
|
|
1051
1080
|
}
|
|
1052
|
-
const { task: result, timedOut } = await waitForScriptTask(task.id, POLL_TIMEOUT_MS, task);
|
|
1081
|
+
const { task: result, timedOut, waitingHuman } = await waitForScriptTask(task.id, POLL_TIMEOUT_MS, task);
|
|
1082
|
+
if (waitingHuman) {
|
|
1083
|
+
console.log(waitingHumanMessage(result));
|
|
1084
|
+
process.exit(EXIT_WAITING_HUMAN);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1053
1087
|
if (timedOut) {
|
|
1054
1088
|
console.log(`Polling timed out. Task ${task.id} still running.`);
|
|
1055
1089
|
console.log(`Wait with: browser-cli script-task ${task.id} --wait`);
|
|
@@ -1084,18 +1118,25 @@ ${downloadHint(result)}`);
|
|
|
1084
1118
|
});
|
|
1085
1119
|
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(
|
|
1086
1120
|
"--wait",
|
|
1087
|
-
`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), 1 error`
|
|
1088
|
-
).option("--timeout <seconds>", `With --wait: max seconds to wait (default ${WAIT_DEFAULT_TIMEOUT_S}, max ${WAIT_MAX_TIMEOUT_S})`).action(async (taskId, opts) => {
|
|
1121
|
+
`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`
|
|
1122
|
+
).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) => {
|
|
1089
1123
|
try {
|
|
1090
1124
|
if (opts.json && opts.download !== void 0) {
|
|
1091
1125
|
throw new Error("--json \u4E0E --download \u53EA\u80FD\u7ED9\u4E00\u4E2A");
|
|
1092
1126
|
}
|
|
1093
1127
|
if (opts.timeout !== void 0 && !opts.wait) throw new Error("--timeout \u53EA\u80FD\u548C --wait \u4E00\u8D77\u7528");
|
|
1128
|
+
if (opts.throughHuman && !opts.wait) throw new Error("--through-human \u53EA\u80FD\u548C --wait \u4E00\u8D77\u7528");
|
|
1094
1129
|
const timeoutS = opts.timeout === void 0 ? WAIT_DEFAULT_TIMEOUT_S : parseWaitTimeout(opts.timeout);
|
|
1095
1130
|
let task;
|
|
1096
1131
|
if (opts.wait) {
|
|
1097
|
-
const waited = await waitForScriptTask(taskId, timeoutS * 1e3);
|
|
1132
|
+
const waited = await waitForScriptTask(taskId, timeoutS * 1e3, void 0, opts.throughHuman);
|
|
1098
1133
|
task = waited.task;
|
|
1134
|
+
if (waited.waitingHuman) {
|
|
1135
|
+
if (opts.json) console.log(JSON.stringify(task, null, 2));
|
|
1136
|
+
(opts.json ? console.error : console.log)(waitingHumanMessage(task));
|
|
1137
|
+
process.exit(EXIT_WAITING_HUMAN);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1099
1140
|
if (waited.timedOut) {
|
|
1100
1141
|
if (opts.json) console.log(JSON.stringify(task, null, 2));
|
|
1101
1142
|
(opts.json ? console.error : console.log)(stillRunningMessage(task, timeoutS));
|