@optima-chat/browser-cli 0.17.0 → 0.18.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 +114 -24
- 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);
|
|
@@ -823,6 +859,34 @@ function formatScriptList(scripts) {
|
|
|
823
859
|
}
|
|
824
860
|
return lines.join("\n").trimEnd();
|
|
825
861
|
}
|
|
862
|
+
var HALT_RENDERED_ELSEWHERE = /* @__PURE__ */ new Set(["completed_steps", "remaining_steps", "error"]);
|
|
863
|
+
var ARTIFACT_KEYS = /* @__PURE__ */ new Set(["artifact_path", "filename", "size"]);
|
|
864
|
+
var FIELD_MAX_CHARS = 2e3;
|
|
865
|
+
function formatFieldValue(v, taskId) {
|
|
866
|
+
if (v === null || v === void 0 || v === "") return null;
|
|
867
|
+
const s = typeof v === "string" ? v : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v);
|
|
868
|
+
const cps = Array.from(s);
|
|
869
|
+
return cps.length > FIELD_MAX_CHARS ? `${cps.slice(0, FIELD_MAX_CHARS).join("")}\u2026 (truncated, ${cps.length} chars; full: browser-cli script-task ${taskId} --json)` : s;
|
|
870
|
+
}
|
|
871
|
+
function haltOutputFields(task) {
|
|
872
|
+
const output = task.output ?? {};
|
|
873
|
+
const staged = typeof output.artifact_path === "string" && output.artifact_path !== "";
|
|
874
|
+
const lines = [];
|
|
875
|
+
for (const [k, v] of Object.entries(output)) {
|
|
876
|
+
if (HALT_RENDERED_ELSEWHERE.has(k)) continue;
|
|
877
|
+
if (staged && ARTIFACT_KEYS.has(k)) continue;
|
|
878
|
+
if (k === "snapshot") {
|
|
879
|
+
if (v !== null && v !== void 0 && v !== "") {
|
|
880
|
+
const n = Array.from(typeof v === "string" ? v : JSON.stringify(v)).length;
|
|
881
|
+
lines.push(`snapshot: (omitted, ${n} chars; full: browser-cli script-task ${task.id} --json)`);
|
|
882
|
+
}
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const s = formatFieldValue(v, task.id);
|
|
886
|
+
if (s !== null) lines.push(`${k}: ${s}`);
|
|
887
|
+
}
|
|
888
|
+
return lines;
|
|
889
|
+
}
|
|
826
890
|
function formatScriptTask(task) {
|
|
827
891
|
const lines = [];
|
|
828
892
|
if (task.status === "completed") {
|
|
@@ -860,12 +924,27 @@ function formatScriptTask(task) {
|
|
|
860
924
|
}
|
|
861
925
|
lines.push("");
|
|
862
926
|
}
|
|
927
|
+
const fields = haltOutputFields(task);
|
|
928
|
+
if (fields.length > 0) {
|
|
929
|
+
lines.push(...fields);
|
|
930
|
+
lines.push("");
|
|
931
|
+
}
|
|
863
932
|
const sid = task.session_id;
|
|
864
933
|
lines.push(
|
|
865
934
|
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"
|
|
866
935
|
);
|
|
867
936
|
} else if (task.status === "failed") {
|
|
868
937
|
lines.push(`\u2717 Script failed: ${task.error ?? "unknown error"}`);
|
|
938
|
+
const output = task.output ?? {};
|
|
939
|
+
const completed = output.completed_steps ?? [];
|
|
940
|
+
const remaining = output.remaining_steps ?? [];
|
|
941
|
+
if (completed.length > 0) {
|
|
942
|
+
lines.push(`\u2713 Completed: ${completed.map((s) => s.id).join(", ")}`);
|
|
943
|
+
}
|
|
944
|
+
if (remaining.length > 0) {
|
|
945
|
+
lines.push(`\u2717 Stopped at step [${completed.length + 1}] ${remaining[0].id}`);
|
|
946
|
+
}
|
|
947
|
+
lines.push(...haltOutputFields(task));
|
|
869
948
|
} else {
|
|
870
949
|
lines.push(`Script status: ${task.status} (step ${task.current_step}/${task.total_steps})`);
|
|
871
950
|
}
|
|
@@ -980,10 +1059,17 @@ ${downloadHint(result)}`);
|
|
|
980
1059
|
process.exit(1);
|
|
981
1060
|
}
|
|
982
1061
|
});
|
|
983
|
-
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>)").action(async (taskId, opts) => {
|
|
1062
|
+
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").action(async (taskId, opts) => {
|
|
984
1063
|
try {
|
|
1064
|
+
if (opts.json && opts.download !== void 0) {
|
|
1065
|
+
throw new Error("--json \u4E0E --download \u53EA\u80FD\u7ED9\u4E00\u4E2A");
|
|
1066
|
+
}
|
|
985
1067
|
const task = await api.getScriptTask(taskId);
|
|
986
1068
|
if (task.status === "needs_fallback" && task.session_id) saveSessionId(task.session_id);
|
|
1069
|
+
if (opts.json) {
|
|
1070
|
+
console.log(JSON.stringify(task, null, 2));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
987
1073
|
if (opts.download === void 0) {
|
|
988
1074
|
console.log(formatScriptTask(task));
|
|
989
1075
|
if (hasStagedArtifact(task)) console.log(downloadHint(task));
|
|
@@ -1018,16 +1104,20 @@ var BOSS_GUIDANCE = [
|
|
|
1018
1104
|
" 3. \u4E3A\u8BE5\u8D26\u53F7\u6388\u6743\u9700\u8981\u64CD\u4F5C\u7684\u5E97\u94FA\u3002",
|
|
1019
1105
|
`\u53C2\u8003\u6587\u6863: ${ZINIAO_WEBDRIVER_DOC_URL}`
|
|
1020
1106
|
].join("\n");
|
|
1107
|
+
var SELF_EXPLANATORY = /正在运行.{0,4}但未开启\s*WebDriver\s*模式/;
|
|
1108
|
+
var NOT_READY_HINT = "\u7D2B\u9E1F\u6CA1\u80FD\u5728 15 \u79D2\u5185\u8FDB\u5165 WebDriver \u6A21\u5F0F\uFF08\u4E0D\u662F\u6743\u9650\u95EE\u9898\uFF09\u3002\u2460 \u5148\u786E\u8BA4\u7D2B\u9E1F\u5DF2**\u5B8C\u5168\u9000\u51FA**\uFF08\u542B\u6258\u76D8/\u540E\u53F0\u8FDB\u7A0B\uFF09\u2014\u2014 \u5B83\u4EE5\u666E\u901A\u6A21\u5F0F\u8DD1\u7740\u65F6\u6211\u4EEC\u8D77\u4E0D\u4E86 WebDriver \u6A21\u5F0F\uFF1B\u2461 \u7136\u540E\u8BA9\u6211\u8FD9\u8FB9\u518D\u62C9\u4E00\u6B21\uFF08Windows \u4E0A\u82E5\u5B89\u88C5\u8DEF\u5F84\u662F\u63A2\u6D4B\u6765\u7684\uFF0C\u5931\u8D25\u90A3\u6B21\u4F1A\u6E05\u6389\u5B83\u5E76\u91CD\u67E5\u6CE8\u518C\u8868\uFF0C\u7B2C\u4E8C\u6B21\u5E38\u5E38\u5C31\u6210\uFF09\uFF1B\u2462 \u8FD8\u4E0D\u884C\u5C31\u95EE\u5E97\u4E3B\uFF1A\u7D2B\u9E1F\u88C5\u5728\u54EA\u3001\u662F\u4E0D\u662F\u8FD8\u88C5\u7740\u3002";
|
|
1021
1109
|
function ziniaoErrorHint(message) {
|
|
1022
|
-
if (
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1110
|
+
if (SELF_EXPLANATORY.test(message)) return null;
|
|
1111
|
+
if (/Failed to start ZiNiao WebDriver/i.test(message)) return NOT_READY_HINT;
|
|
1025
1112
|
if (/错误次数太多/.test(message)) {
|
|
1026
1113
|
return "\u7D2B\u9E1F\u98CE\u63A7\u5DF2\u4E34\u65F6\u9501\u5B9A\uFF0C\u8BF7\u7B49\u5F85\u7EA6 10 \u5206\u949F\u540E\u91CD\u8BD5\uFF08\u7EE7\u7EED\u5931\u8D25\u4F1A\u5EF6\u957F\u9501\u5B9A\uFF09\u3002";
|
|
1027
1114
|
}
|
|
1028
1115
|
if (/用户名或密码错误|密码错误/.test(message)) {
|
|
1029
1116
|
return "\u8BF7\u6838\u5BF9\u51ED\u636E\uFF08company=\u4F01\u4E1A\u540D\u79F0\uFF0Cusername/password=\u6210\u5458\u8D26\u53F7\uFF0C\u63A8\u8350 robot \u8D26\u53F7\uFF09\uFF0C\u7136\u540E\u7528 browser-cli ziniao set \u91CD\u65B0\u914D\u7F6E\u3002";
|
|
1030
1117
|
}
|
|
1118
|
+
if (/开通|权限|permission|授权|未开启|未启用|not enabled/i.test(message)) {
|
|
1119
|
+
return `\u7591\u4F3C\u672A\u5F00\u901A Webdriver \u6743\u9650\u6216\u8D26\u53F7\u6743\u9650\u4E0D\u8DB3\u3002${BOSS_GUIDANCE}`;
|
|
1120
|
+
}
|
|
1031
1121
|
return null;
|
|
1032
1122
|
}
|
|
1033
1123
|
async function runZiNiaoDoctor(client) {
|
|
@@ -1265,7 +1355,7 @@ function registerFeikuaCommands(program2) {
|
|
|
1265
1355
|
return;
|
|
1266
1356
|
}
|
|
1267
1357
|
console.log(
|
|
1268
|
-
'No paired shop browsers.
|
|
1358
|
+
'No paired shop browsers. In the desktop app tray, click \u300C\u914D\u5BF9\u98DE\u8DE8\u6D4F\u89C8\u5668\u2026\u300D/ "Pair a Feikua Browser\u2026" (older desktop versions: \u300C\u914D\u5BF9\u65B0\u5E97\u94FA\u6D4F\u89C8\u5668\u2026\u300D/ "Pair a Shop Browser\u2026"), then follow the on-screen code.'
|
|
1269
1359
|
);
|
|
1270
1360
|
return;
|
|
1271
1361
|
}
|