@optima-chat/browser-cli 0.18.0 → 0.19.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 +112 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
|
+
// src/api.ts
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
|
|
7
10
|
// src/config.ts
|
|
8
11
|
import { readFileSync, existsSync } from "fs";
|
|
9
12
|
import { join } from "path";
|
|
@@ -77,26 +80,41 @@ function getCurrentEnv() {
|
|
|
77
80
|
}
|
|
78
81
|
return "development";
|
|
79
82
|
}
|
|
83
|
+
function resolveToken(required) {
|
|
84
|
+
for (const key of ["BROWSER_TOKEN", "OPTIMA_TOKEN"]) {
|
|
85
|
+
const value = process.env[key]?.trim();
|
|
86
|
+
if (value) return { token: value, source: key, required };
|
|
87
|
+
}
|
|
88
|
+
const noEnv = "\u672A\u8BBE\u7F6E BROWSER_TOKEN / OPTIMA_TOKEN";
|
|
89
|
+
const file = readOptimaToken();
|
|
90
|
+
if (!file?.access_token) {
|
|
91
|
+
return { token: null, source: null, required, missingReason: `${noEnv}\uFF0C~/.optima/token.json \u4E0D\u5B58\u5728\u6216\u6CA1\u6709 access_token` };
|
|
92
|
+
}
|
|
93
|
+
const expiresAt = typeof file.expires_at === "number" && Number.isFinite(file.expires_at) ? file.expires_at : void 0;
|
|
94
|
+
if (expiresAt !== void 0 && expiresAt <= Date.now()) {
|
|
95
|
+
return {
|
|
96
|
+
token: null,
|
|
97
|
+
source: null,
|
|
98
|
+
required,
|
|
99
|
+
missingReason: `${noEnv}\uFF0C~/.optima/token.json \u7684\u7968\u5DF2\u4E8E ${new Date(expiresAt).toISOString()} \u8FC7\u671F`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { token: file.access_token, source: "token.json", expiresAt, required };
|
|
103
|
+
}
|
|
80
104
|
function loadConfig() {
|
|
81
105
|
const apiUrlOverride = process.env.BROWSER_API_URL;
|
|
82
106
|
const env = apiUrlOverride ? void 0 : getCurrentEnv();
|
|
83
107
|
const apiUrl = apiUrlOverride ?? ENV_CONFIG[env].apiUrl;
|
|
84
|
-
|
|
85
|
-
if (!token) {
|
|
86
|
-
const optimaToken = readOptimaToken();
|
|
87
|
-
if (optimaToken?.access_token) {
|
|
88
|
-
token = optimaToken.access_token;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
108
|
+
const auth = resolveToken(env !== void 0 && env !== "development");
|
|
91
109
|
return {
|
|
92
110
|
apiUrl,
|
|
93
|
-
|
|
111
|
+
auth,
|
|
94
112
|
env
|
|
95
113
|
};
|
|
96
114
|
}
|
|
97
115
|
var config = loadConfig();
|
|
98
116
|
var BROWSER_API_URL = config.apiUrl;
|
|
99
|
-
var
|
|
117
|
+
var AUTH = config.auth;
|
|
100
118
|
|
|
101
119
|
// src/api.ts
|
|
102
120
|
var BrowserAPIError = class extends Error {
|
|
@@ -117,15 +135,34 @@ function toAPIError(status, statusText, body) {
|
|
|
117
135
|
}
|
|
118
136
|
return new BrowserAPIError(status, "unknown", `HTTP ${status}: ${statusText}`);
|
|
119
137
|
}
|
|
138
|
+
function authHeader() {
|
|
139
|
+
if (AUTH.token) return { Authorization: `Bearer ${AUTH.token}` };
|
|
140
|
+
if (AUTH.required) {
|
|
141
|
+
throw new Error(`\u65E0\u53EF\u7528\u51ED\u8BC1\uFF1A${AUTH.missingReason}\u3002\u8FD9\u662F Optima \u5E73\u53F0\u51ED\u8BC1\uFF0C\u4E0E\u5E97\u94FA/\u6D4F\u89C8\u5668\u7684\u767B\u5F55\u65E0\u5173\u3002`);
|
|
142
|
+
}
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
function withAuthContext(err) {
|
|
146
|
+
if (err.statusCode !== 401) return err;
|
|
147
|
+
if (!AUTH.token) {
|
|
148
|
+
err.message += `\uFF08\u672A\u5E26\u51ED\u8BC1\uFF1A${AUTH.missingReason}\uFF09`;
|
|
149
|
+
return err;
|
|
150
|
+
}
|
|
151
|
+
const fp = createHash("sha256").update(AUTH.token).digest("hex").slice(0, 12);
|
|
152
|
+
const exp = AUTH.expiresAt !== void 0 ? `\uFF0Cexpires_at=${new Date(AUTH.expiresAt).toISOString()}` : "";
|
|
153
|
+
err.message += `\uFF08\u51ED\u8BC1\u6765\u6E90 ${AUTH.source}${exp}\uFF0Cfp=${fp}\uFF09`;
|
|
154
|
+
return err;
|
|
155
|
+
}
|
|
120
156
|
async function request(path, options = {}) {
|
|
121
157
|
const url = `${BROWSER_API_URL}${path}`;
|
|
158
|
+
const auth = authHeader();
|
|
122
159
|
let response;
|
|
123
160
|
try {
|
|
124
161
|
response = await fetch(url, {
|
|
125
162
|
...options,
|
|
126
163
|
headers: {
|
|
127
164
|
"Content-Type": "application/json",
|
|
128
|
-
...
|
|
165
|
+
...auth,
|
|
129
166
|
...options.headers
|
|
130
167
|
}
|
|
131
168
|
});
|
|
@@ -139,13 +176,13 @@ async function request(path, options = {}) {
|
|
|
139
176
|
try {
|
|
140
177
|
body = await response.json();
|
|
141
178
|
} catch {
|
|
142
|
-
throw new BrowserAPIError(
|
|
179
|
+
throw withAuthContext(new BrowserAPIError(
|
|
143
180
|
response.status,
|
|
144
181
|
"unknown",
|
|
145
182
|
`HTTP ${response.status}: ${response.statusText}`
|
|
146
|
-
);
|
|
183
|
+
));
|
|
147
184
|
}
|
|
148
|
-
throw toAPIError(response.status, response.statusText, body);
|
|
185
|
+
throw withAuthContext(toAPIError(response.status, response.statusText, body));
|
|
149
186
|
}
|
|
150
187
|
if (response.status === 204) {
|
|
151
188
|
return void 0;
|
|
@@ -154,12 +191,11 @@ async function request(path, options = {}) {
|
|
|
154
191
|
}
|
|
155
192
|
async function requestBinary(path) {
|
|
156
193
|
const url = `${BROWSER_API_URL}${path}`;
|
|
194
|
+
const auth = authHeader();
|
|
157
195
|
let response;
|
|
158
196
|
try {
|
|
159
197
|
response = await fetch(url, {
|
|
160
|
-
headers:
|
|
161
|
-
...ACCESS_TOKEN ? { Authorization: `Bearer ${ACCESS_TOKEN}` } : {}
|
|
162
|
-
}
|
|
198
|
+
headers: auth
|
|
163
199
|
});
|
|
164
200
|
} catch {
|
|
165
201
|
throw new Error(
|
|
@@ -171,13 +207,13 @@ async function requestBinary(path) {
|
|
|
171
207
|
try {
|
|
172
208
|
body = await response.json();
|
|
173
209
|
} catch {
|
|
174
|
-
throw new BrowserAPIError(
|
|
210
|
+
throw withAuthContext(new BrowserAPIError(
|
|
175
211
|
response.status,
|
|
176
212
|
"unknown",
|
|
177
213
|
`HTTP ${response.status}: ${response.statusText}`
|
|
178
|
-
);
|
|
214
|
+
));
|
|
179
215
|
}
|
|
180
|
-
throw toAPIError(response.status, response.statusText, body);
|
|
216
|
+
throw withAuthContext(toAPIError(response.status, response.statusText, body));
|
|
181
217
|
}
|
|
182
218
|
const arrayBuffer = await response.arrayBuffer();
|
|
183
219
|
return Buffer.from(arrayBuffer);
|
|
@@ -749,6 +785,34 @@ import { tmpdir } from "os";
|
|
|
749
785
|
import { basename, dirname, isAbsolute, join as join3, relative, resolve } from "path";
|
|
750
786
|
var POLL_INTERVAL_MS = 2e3;
|
|
751
787
|
var POLL_TIMEOUT_MS = 18e4;
|
|
788
|
+
var WAIT_DEFAULT_TIMEOUT_S = 90;
|
|
789
|
+
var WAIT_MAX_TIMEOUT_S = 570;
|
|
790
|
+
var EXIT_TASK_STOPPED = 2;
|
|
791
|
+
var EXIT_STILL_RUNNING = 3;
|
|
792
|
+
async function waitForScriptTask(taskId, timeoutMs, first) {
|
|
793
|
+
const deadline = Date.now() + timeoutMs;
|
|
794
|
+
let task = first ?? await api.getScriptTask(taskId);
|
|
795
|
+
while (task.status === "running") {
|
|
796
|
+
const left = deadline - Date.now();
|
|
797
|
+
if (left <= 0) return { task, timedOut: true };
|
|
798
|
+
await sleep(Math.min(POLL_INTERVAL_MS, left));
|
|
799
|
+
task = await api.getScriptTask(taskId);
|
|
800
|
+
}
|
|
801
|
+
return { task, timedOut: false };
|
|
802
|
+
}
|
|
803
|
+
function parseWaitTimeout(raw) {
|
|
804
|
+
const n = Number(raw);
|
|
805
|
+
if (!/^\d+$/.test(raw.trim()) || !Number.isInteger(n) || n < 1 || n > WAIT_MAX_TIMEOUT_S) {
|
|
806
|
+
throw new Error(`--timeout \u987B\u4E3A 1\u2013${WAIT_MAX_TIMEOUT_S} \u7684\u6574\u6570\u79D2(bash \u786C\u4E0A\u9650 600s),\u6536\u5230 ${JSON.stringify(raw)}`);
|
|
807
|
+
}
|
|
808
|
+
return n;
|
|
809
|
+
}
|
|
810
|
+
function stillRunningMessage(task, waitedS, nowMs = Date.now()) {
|
|
811
|
+
const started = task.created_at ? Date.parse(task.created_at) : Number.NaN;
|
|
812
|
+
const elapsed = Number.isFinite(started) ? `running for ${Math.max(0, Math.round((nowMs - started) / 1e3))}s` : `waited ${waitedS}s`;
|
|
813
|
+
const step = task.total_steps ? `, step ${task.current_step}/${task.total_steps}` : "";
|
|
814
|
+
return `Still running (${elapsed}${step}). Call again as a separate command: browser-cli script-task ${task.id} --wait`;
|
|
815
|
+
}
|
|
752
816
|
var REQUIRED_ARG_FLAG = {
|
|
753
817
|
ziniao_profile_id: "--ziniao-profile <id>",
|
|
754
818
|
bitbrowser_profile_id: "--bitbrowser-profile <id>",
|
|
@@ -982,18 +1046,13 @@ function registerScriptCommands(program2) {
|
|
|
982
1046
|
const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile, opts.chromeProfile);
|
|
983
1047
|
console.log(`Script started (task: ${task.id})`);
|
|
984
1048
|
if (!opts.poll) {
|
|
985
|
-
console.log(`
|
|
1049
|
+
console.log(`Wait with: browser-cli script-task ${task.id} --wait`);
|
|
986
1050
|
return;
|
|
987
1051
|
}
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
while (result.status === "running" && Date.now() < deadline) {
|
|
991
|
-
await sleep(POLL_INTERVAL_MS);
|
|
992
|
-
result = await api.getScriptTask(task.id);
|
|
993
|
-
}
|
|
994
|
-
if (result.status === "running") {
|
|
1052
|
+
const { task: result, timedOut } = await waitForScriptTask(task.id, POLL_TIMEOUT_MS, task);
|
|
1053
|
+
if (timedOut) {
|
|
995
1054
|
console.log(`Polling timed out. Task ${task.id} still running.`);
|
|
996
|
-
console.log(`
|
|
1055
|
+
console.log(`Wait with: browser-cli script-task ${task.id} --wait`);
|
|
997
1056
|
return;
|
|
998
1057
|
}
|
|
999
1058
|
if (result.status === "needs_fallback" && result.session_id) {
|
|
@@ -1023,20 +1082,42 @@ ${downloadHint(result)}`);
|
|
|
1023
1082
|
process.exit(1);
|
|
1024
1083
|
}
|
|
1025
1084
|
});
|
|
1026
|
-
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").
|
|
1085
|
+
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
|
+
"--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) => {
|
|
1027
1089
|
try {
|
|
1028
1090
|
if (opts.json && opts.download !== void 0) {
|
|
1029
1091
|
throw new Error("--json \u4E0E --download \u53EA\u80FD\u7ED9\u4E00\u4E2A");
|
|
1030
1092
|
}
|
|
1031
|
-
|
|
1093
|
+
if (opts.timeout !== void 0 && !opts.wait) throw new Error("--timeout \u53EA\u80FD\u548C --wait \u4E00\u8D77\u7528");
|
|
1094
|
+
const timeoutS = opts.timeout === void 0 ? WAIT_DEFAULT_TIMEOUT_S : parseWaitTimeout(opts.timeout);
|
|
1095
|
+
let task;
|
|
1096
|
+
if (opts.wait) {
|
|
1097
|
+
const waited = await waitForScriptTask(taskId, timeoutS * 1e3);
|
|
1098
|
+
task = waited.task;
|
|
1099
|
+
if (waited.timedOut) {
|
|
1100
|
+
if (opts.json) console.log(JSON.stringify(task, null, 2));
|
|
1101
|
+
(opts.json ? console.error : console.log)(stillRunningMessage(task, timeoutS));
|
|
1102
|
+
process.exit(EXIT_STILL_RUNNING);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
task = await api.getScriptTask(taskId);
|
|
1107
|
+
}
|
|
1108
|
+
const stoppedExit = () => {
|
|
1109
|
+
if (opts.wait && task.status !== "completed") process.exit(EXIT_TASK_STOPPED);
|
|
1110
|
+
};
|
|
1032
1111
|
if (task.status === "needs_fallback" && task.session_id) saveSessionId(task.session_id);
|
|
1033
1112
|
if (opts.json) {
|
|
1034
1113
|
console.log(JSON.stringify(task, null, 2));
|
|
1114
|
+
stoppedExit();
|
|
1035
1115
|
return;
|
|
1036
1116
|
}
|
|
1037
1117
|
if (opts.download === void 0) {
|
|
1038
1118
|
console.log(formatScriptTask(task));
|
|
1039
1119
|
if (hasStagedArtifact(task)) console.log(downloadHint(task));
|
|
1120
|
+
stoppedExit();
|
|
1040
1121
|
return;
|
|
1041
1122
|
}
|
|
1042
1123
|
if (task.status === "running") {
|
|
@@ -1045,6 +1126,7 @@ ${downloadHint(result)}`);
|
|
|
1045
1126
|
const saved = await deliverArtifact(task, typeof opts.download === "string" ? opts.download : void 0);
|
|
1046
1127
|
if (!saved) throw new Error(`Task ${taskId} produced no file`);
|
|
1047
1128
|
console.log(`Saved -> ${saved.path}${saved.size === void 0 ? "" : ` (${saved.size} bytes)`}`);
|
|
1129
|
+
stoppedExit();
|
|
1048
1130
|
} catch (err) {
|
|
1049
1131
|
const gone = err.errorCode === "artifact_gone";
|
|
1050
1132
|
console.error(
|