@optima-chat/browser-cli 0.6.1 → 0.7.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.
Files changed (2) hide show
  1. package/dist/index.js +198 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -98,6 +98,16 @@ var BrowserAPIError = class extends Error {
98
98
  this.name = "BrowserAPIError";
99
99
  }
100
100
  };
101
+ function toAPIError(status, statusText, body) {
102
+ const detail = body.detail ?? body;
103
+ if (typeof detail === "string") {
104
+ return new BrowserAPIError(status, "http_error", `HTTP ${status}: ${detail}`);
105
+ }
106
+ if (detail?.message) {
107
+ return new BrowserAPIError(status, detail.error ?? "unknown", detail.message);
108
+ }
109
+ return new BrowserAPIError(status, "unknown", `HTTP ${status}: ${statusText}`);
110
+ }
101
111
  async function request(path, options = {}) {
102
112
  const url = `${BROWSER_API_URL}${path}`;
103
113
  let response;
@@ -126,8 +136,7 @@ async function request(path, options = {}) {
126
136
  `HTTP ${response.status}: ${response.statusText}`
127
137
  );
128
138
  }
129
- const detail = body.detail || body;
130
- throw new BrowserAPIError(response.status, detail.error, detail.message);
139
+ throw toAPIError(response.status, response.statusText, body);
131
140
  }
132
141
  if (response.status === 204) {
133
142
  return void 0;
@@ -159,20 +168,20 @@ async function requestBinary(path) {
159
168
  `HTTP ${response.status}: ${response.statusText}`
160
169
  );
161
170
  }
162
- const detail = body.detail || body;
163
- throw new BrowserAPIError(response.status, detail.error, detail.message);
171
+ throw toAPIError(response.status, response.statusText, body);
164
172
  }
165
173
  const arrayBuffer = await response.arrayBuffer();
166
174
  return Buffer.from(arrayBuffer);
167
175
  }
168
176
  var BrowserAPI = class {
169
- async createSession(url, profileId, force, connector, ziniaoProfileId) {
177
+ async createSession(url, profileId, force, connector, ziniaoProfileId, bitbrowserProfileId) {
170
178
  const body = {};
171
179
  if (url) body.url = url;
172
180
  if (profileId) body.profile_id = profileId;
173
181
  if (force) body.force = true;
174
182
  if (connector) body.connector = connector;
175
183
  if (ziniaoProfileId) body.ziniao_profile_id = ziniaoProfileId;
184
+ if (bitbrowserProfileId) body.bitbrowser_profile_id = bitbrowserProfileId;
176
185
  return request("/api/sessions", {
177
186
  method: "POST",
178
187
  body: JSON.stringify(body)
@@ -201,6 +210,9 @@ var BrowserAPI = class {
201
210
  async getZiNiaoProfiles() {
202
211
  return request("/api/user/ziniao-profiles");
203
212
  }
213
+ async getBitBrowserProfiles() {
214
+ return request("/api/user/bitbrowser-profiles");
215
+ }
204
216
  async closeSession(sessionId) {
205
217
  await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
206
218
  }
@@ -246,6 +258,22 @@ var BrowserAPI = class {
246
258
  async getScreenshot(sessionId) {
247
259
  return requestBinary(`/api/sessions/${sessionId}/screenshot`);
248
260
  }
261
+ async runRecipeScript(name, params, pollMs = 2e3) {
262
+ const started = await request(`/api/scripts/${name}/run`, {
263
+ method: "POST",
264
+ body: JSON.stringify({ params })
265
+ });
266
+ for (; ; ) {
267
+ const task = await request(
268
+ `/api/script-tasks/${started.id}`
269
+ );
270
+ if (task.status === "completed") return task.output;
271
+ if (task.status === "failed" || task.status === "needs_fallback") {
272
+ throw new Error(task.error || `recipe ${task.status}`);
273
+ }
274
+ await new Promise((r) => setTimeout(r, pollMs));
275
+ }
276
+ }
249
277
  async search(sessionId, pattern) {
250
278
  return request(`/api/sessions/${sessionId}/search`, {
251
279
  method: "POST",
@@ -300,9 +328,11 @@ var BrowserAPI = class {
300
328
  async deleteScript(name) {
301
329
  await request(`/api/scripts/${encodeURIComponent(name)}`, { method: "DELETE" });
302
330
  }
303
- async runScript(scriptName, params = {}, profileId) {
331
+ async runScript(scriptName, params = {}, profileId, connector, bitbrowserProfileId) {
304
332
  const payload = { params };
305
333
  if (profileId) payload.profile_id = profileId;
334
+ if (connector) payload.connector = connector;
335
+ if (bitbrowserProfileId) payload.bitbrowser_profile_id = bitbrowserProfileId;
306
336
  return request(`/api/scripts/${encodeURIComponent(scriptName)}/run`, {
307
337
  method: "POST",
308
338
  body: JSON.stringify(payload)
@@ -404,6 +434,9 @@ function formatFind(result) {
404
434
  }
405
435
  return lines.join("\n");
406
436
  }
437
+ function formatDesktopNotConnected(result) {
438
+ return result.message ?? "\u684C\u9762\u5E94\u7528\u672A\u8FDE\u63A5\uFF0C\u8BF7\u5148\u5B89\u88C5\u5E76\u542F\u52A8\u684C\u9762\u5E94\u7528\u540E\u91CD\u8BD5\u3002";
439
+ }
407
440
  function formatError(err) {
408
441
  if (err instanceof Error) {
409
442
  return `\u2717 Error: ${err.message}`;
@@ -430,9 +463,9 @@ function formatProfileDelete(profileId) {
430
463
 
431
464
  // src/commands/lifecycle.ts
432
465
  function registerLifecycleCommands(program2) {
433
- program2.command("launch").description("Launch a browser session").option("--url <url>", "Navigate to URL after launch").option("--profile <id>", "Use a named browser profile").option("--force", "Close existing session before launching").option("--connector <type>", "Browser connector: chrome or ziniao").option("--ziniao-profile <id>", "ZiNiao browser profile ID").action(async (opts) => {
466
+ program2.command("launch").description("Launch a browser session").option("--url <url>", "Navigate to URL after launch").option("--profile <id>", "Use a named browser profile").option("--force", "Close existing session before launching").option("--connector <type>", "Browser connector: chrome, ziniao, or bitbrowser").option("--ziniao-profile <id>", "ZiNiao browser profile ID").option("--bitbrowser-profile <id>", "BitBrowser profile ID").action(async (opts) => {
434
467
  try {
435
- const state = await api.createSession(opts.url, opts.profile, opts.force, opts.connector, opts.ziniaoProfile);
468
+ const state = await api.createSession(opts.url, opts.profile, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile);
436
469
  saveSessionId(state.session_id);
437
470
  console.log(formatLaunch(state));
438
471
  } catch (err) {
@@ -748,7 +781,7 @@ function registerScriptCommands(program2) {
748
781
  process.exit(1);
749
782
  }
750
783
  });
751
- program2.command("runscript").argument("<name>", "Script name").description("Run a script (polls until complete)").option("--param <params...>", "Parameters as key=value").option("--profile <id>", "Use a named browser profile").option("--no-poll", "Return immediately without polling").action(async (name, opts) => {
784
+ program2.command("runscript").argument("<name>", "Script name").description("Run a script (polls until complete)").option("--param <params...>", "Parameters as key=value").option("--profile <id>", "Use a named browser profile").option("--connector <type>", "Browser connector: chrome, ziniao, or bitbrowser").option("--bitbrowser-profile <id>", "BitBrowser profile ID (with --connector bitbrowser)").option("--no-poll", "Return immediately without polling").action(async (name, opts) => {
752
785
  try {
753
786
  const params = {};
754
787
  if (opts.param) {
@@ -758,7 +791,7 @@ function registerScriptCommands(program2) {
758
791
  }
759
792
  }
760
793
  clearSessionId();
761
- const task = await api.runScript(name, params, opts.profile);
794
+ const task = await api.runScript(name, params, opts.profile, opts.connector, opts.bitbrowserProfile);
762
795
  console.log(`Script started (task: ${task.id})`);
763
796
  if (!opts.poll) {
764
797
  console.log(`Poll with: browser-cli script-task ${task.id}`);
@@ -820,6 +853,105 @@ function registerProfileCommands(program2) {
820
853
  });
821
854
  }
822
855
 
856
+ // src/ziniao-doctor.ts
857
+ var ZINIAO_WEBDRIVER_DOC_URL = "https://open.ziniao.com/docSupport?docId=99#%E7%AC%AC%E4%B8%80%E6%AD%A5%EF%BC%9A%E7%99%BB%E5%BD%95%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A7%E5%88%B6%E5%8F%B00";
858
+ var BOSS_GUIDANCE = [
859
+ "\u9700\u8981\u7D2B\u9E1F BOSS \u8D26\u53F7\u5B8C\u6210\u5F00\u901A\uFF08\u4E00\u6B21\u6027\u914D\u7F6E\uFF09\uFF1A",
860
+ " 1. \u767B\u5F55\u7D2B\u9E1F\u5F00\u653E\u5E73\u53F0\u63A7\u5236\u53F0\uFF0C\u5F00\u901A Webdriver \u6743\u9650\uFF1B",
861
+ " 2. \u521B\u5EFA\u4E13\u7528\u81EA\u52A8\u5316\u6210\u5458\u8D26\u53F7\uFF08robot\uFF09\uFF0C\u89D2\u8272\u9700\u542B\u300C\u67E5\u770B\u8D26\u53F7\u300D\u6743\u9650\uFF1B",
862
+ " 3. \u4E3A\u8BE5\u8D26\u53F7\u6388\u6743\u9700\u8981\u64CD\u4F5C\u7684\u5E97\u94FA\u3002",
863
+ `\u53C2\u8003\u6587\u6863: ${ZINIAO_WEBDRIVER_DOC_URL}`
864
+ ].join("\n");
865
+ function ziniaoErrorHint(message) {
866
+ if (/webdriver|开通|权限|permission/i.test(message)) {
867
+ return `\u7591\u4F3C\u672A\u5F00\u901A Webdriver \u6743\u9650\u6216\u8D26\u53F7\u6743\u9650\u4E0D\u8DB3\u3002${BOSS_GUIDANCE}`;
868
+ }
869
+ if (/错误次数太多/.test(message)) {
870
+ 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";
871
+ }
872
+ if (/用户名或密码错误|密码错误/.test(message)) {
873
+ 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";
874
+ }
875
+ return null;
876
+ }
877
+ async function runZiNiaoDoctor(client) {
878
+ const checks = [];
879
+ const done = () => ({ checks, ok: checks.every((c) => c.ok === true) });
880
+ let cfg;
881
+ try {
882
+ cfg = await client.getZiNiaoConfig();
883
+ } catch (err) {
884
+ checks.push({
885
+ name: "\u540E\u7AEF\u8FDE\u63A5",
886
+ ok: false,
887
+ detail: err.message,
888
+ hint: "\u786E\u8BA4 browser-backend \u6B63\u5728\u8FD0\u884C\uFF0C\u4E14 BROWSER_API_URL \u6307\u5411\u6B63\u786E\u5730\u5740\u3002"
889
+ });
890
+ return done();
891
+ }
892
+ checks.push({ name: "\u540E\u7AEF\u8FDE\u63A5", ok: true });
893
+ if (!cfg.configured) {
894
+ checks.push({
895
+ name: "\u7D2B\u9E1F\u51ED\u636E",
896
+ ok: false,
897
+ hint: "browser-cli ziniao set --company <\u4F01\u4E1A\u540D\u79F0> --username <\u6210\u5458\u8D26\u53F7> --password <\u5BC6\u7801>\uFF08\u63A8\u8350\u4F7F\u7528\u4E13\u7528 robot \u6210\u5458\u8D26\u53F7\uFF09"
898
+ });
899
+ return done();
900
+ }
901
+ checks.push({ name: "\u7D2B\u9E1F\u51ED\u636E", ok: true, detail: `${cfg.company} / ${cfg.username}` });
902
+ try {
903
+ const r = await client.getZiNiaoProfiles();
904
+ if (!r.connected) {
905
+ checks.push({
906
+ name: "\u684C\u9762\u5E94\u7528",
907
+ ok: false,
908
+ detail: r.message,
909
+ hint: `\u8BF7\u542F\u52A8 Optima \u684C\u9762\u5E94\u7528\u5E76\u5B8C\u6210\u8FDE\u63A5\u3002${r.download_url ? `\u4E0B\u8F7D: ${r.download_url}` : ""}`.trim()
910
+ });
911
+ return done();
912
+ }
913
+ checks.push({ name: "\u684C\u9762\u5E94\u7528", ok: true });
914
+ if (!r.ziniao_available) {
915
+ checks.push({
916
+ name: "\u7D2B\u9E1F\u5BA2\u6237\u7AEF",
917
+ ok: false,
918
+ hint: "\u684C\u9762\u7AEF\u672A\u68C0\u6D4B\u5230\u53EF\u7528\u7684\u7D2B\u9E1F\uFF1A\u786E\u8BA4\u672C\u673A\u5DF2\u5B89\u88C5\u7D2B\u9E1F\u5BA2\u6237\u7AEF\uFF0C\u4E14\u51ED\u636E\u5DF2\u540C\u6B65\u5230\u684C\u9762\uFF08\u91CD\u542F\u684C\u9762\u5E94\u7528\u53EF\u89E6\u53D1\u540C\u6B65\uFF09\u3002"
919
+ });
920
+ return done();
921
+ }
922
+ checks.push({ name: "\u7D2B\u9E1F\u5BA2\u6237\u7AEF", ok: true });
923
+ if (r.profiles.length === 0) {
924
+ checks.push({
925
+ name: "\u5E97\u94FA\u5217\u8868",
926
+ ok: "warn",
927
+ detail: "\u767B\u5F55\u6210\u529F\uFF0C\u4F46\u5E97\u94FA\u5217\u8868\u4E3A\u7A7A",
928
+ hint: `robot \u8D26\u53F7\u7684\u89D2\u8272\u53EF\u80FD\u7F3A\u300C\u67E5\u770B\u8D26\u53F7\u300D\u6743\u9650\uFF0C\u6216\u5C1A\u672A\u6388\u6743\u4EFB\u4F55\u5E97\u94FA\u3002${BOSS_GUIDANCE}`
929
+ });
930
+ return done();
931
+ }
932
+ checks.push({ name: "\u5E97\u94FA\u5217\u8868", ok: true, detail: `${r.profiles.length} \u4E2A\u5E97\u94FA\u53EF\u7528` });
933
+ } catch (err) {
934
+ checks.push({
935
+ name: "\u7D2B\u9E1F Webdriver \u901A\u9053",
936
+ ok: false,
937
+ detail: err.message,
938
+ hint: ziniaoErrorHint(err.message ?? "") ?? void 0
939
+ });
940
+ }
941
+ return done();
942
+ }
943
+ function formatDoctorReport(checks) {
944
+ const lines = [];
945
+ for (const c of checks) {
946
+ const mark = c.ok === true ? "\u2713" : c.ok === "warn" ? "\u26A0" : "\u2717";
947
+ lines.push(`${mark} ${c.name}${c.detail ? `: ${c.detail}` : ""}`);
948
+ if (c.hint) {
949
+ lines.push(...c.hint.split("\n").map((l) => ` ${l}`));
950
+ }
951
+ }
952
+ return lines.join("\n");
953
+ }
954
+
823
955
  // src/commands/ziniao.ts
824
956
  function registerZiNiaoCommands(program2) {
825
957
  const ziniao = program2.command("ziniao").description("Manage ZiNiao browser credentials");
@@ -851,15 +983,17 @@ function registerZiNiaoCommands(program2) {
851
983
  try {
852
984
  const result = await api.getZiNiaoProfiles();
853
985
  if (!result.connected) {
854
- console.log("Desktop app not connected. Start Optima Browser Operator first.");
986
+ console.log(formatDesktopNotConnected(result));
855
987
  process.exit(1);
856
988
  }
857
989
  if (!result.ziniao_available) {
858
990
  console.log("ZiNiao not available on Desktop app. Start ZiNiao WebDriver mode first.");
991
+ console.log("Run: browser-cli ziniao doctor \uFF08\u9010\u9879\u6392\u67E5\u7D2B\u9E1F\u901A\u9053\uFF09");
859
992
  process.exit(1);
860
993
  }
861
994
  if (result.profiles.length === 0) {
862
995
  console.log("No ZiNiao profiles found.");
996
+ console.log("Run: browser-cli ziniao doctor \uFF08\u9010\u9879\u6392\u67E5\u7D2B\u9E1F\u901A\u9053\uFF0C\u5E38\u89C1\u539F\u56E0\uFF1Arobot \u8D26\u53F7\u7F3A\u300C\u67E5\u770B\u8D26\u53F7\u300D\u6743\u9650\u6216\u672A\u6388\u6743\u5E97\u94FA\uFF09");
863
997
  return;
864
998
  }
865
999
  console.log(`ZiNiao profiles (${result.profiles.length}):
@@ -870,9 +1004,16 @@ function registerZiNiaoCommands(program2) {
870
1004
  console.log("\nUse: browser-cli launch --connector ziniao --ziniao-profile <id> --url <url>");
871
1005
  } catch (err) {
872
1006
  console.error(formatError(err));
1007
+ const hint = ziniaoErrorHint(err?.message ?? "");
1008
+ if (hint) console.error(hint);
873
1009
  process.exit(1);
874
1010
  }
875
1011
  });
1012
+ ziniao.command("doctor").description("Diagnose the ZiNiao Webdriver chain (credentials / desktop / permission / stores)").action(async () => {
1013
+ const { checks, ok } = await runZiNiaoDoctor(api);
1014
+ console.log(formatDoctorReport(checks));
1015
+ if (!ok) process.exit(1);
1016
+ });
876
1017
  ziniao.command("clear").description("Remove ZiNiao credentials").action(async () => {
877
1018
  try {
878
1019
  await api.deleteZiNiaoConfig();
@@ -884,6 +1025,50 @@ function registerZiNiaoCommands(program2) {
884
1025
  });
885
1026
  }
886
1027
 
1028
+ // src/commands/recipes.ts
1029
+ import { writeFileSync as writeFileSync3 } from "fs";
1030
+ function registerRecipeCommands(program2) {
1031
+ program2.command("export-ad-report").description("Export TikTok Shop last-7-days video ad data into the session workspace").requiredOption("--ziniao-profile <id>", "ZiNiao browser profile ID").option("--out <path>", "Output CSV path", "./tt_video_ad_7d.csv").action(async (opts) => {
1032
+ try {
1033
+ const out = await api.runRecipeScript("tiktok_video_ad_export", {
1034
+ ziniao_profile_id: opts.ziniaoProfile
1035
+ });
1036
+ writeFileSync3(opts.out, Buffer.from(out.content_b64, "base64"));
1037
+ console.log(`Saved ${out.filename} -> ${opts.out} (${out.size} bytes)`);
1038
+ } catch (err) {
1039
+ console.error(formatError(err));
1040
+ process.exit(1);
1041
+ }
1042
+ });
1043
+ }
1044
+
1045
+ // src/commands/bitbrowser.ts
1046
+ function registerBitBrowserCommands(program2) {
1047
+ const bitbrowser = program2.command("bitbrowser").description("Manage BitBrowser (\u6BD4\u7279\u6D4F\u89C8\u5668) anti-detect browser");
1048
+ bitbrowser.command("profiles").description("List BitBrowser profiles (requires Desktop app online + BitBrowser running)").action(async () => {
1049
+ try {
1050
+ const result = await api.getBitBrowserProfiles();
1051
+ if (!result.connected) {
1052
+ console.log(formatDesktopNotConnected(result));
1053
+ process.exit(1);
1054
+ }
1055
+ if (result.profiles.length === 0) {
1056
+ console.log("No BitBrowser profiles found. Is BitBrowser running with its local API enabled?");
1057
+ return;
1058
+ }
1059
+ console.log(`BitBrowser profiles (${result.profiles.length}):
1060
+ `);
1061
+ for (const p of result.profiles) {
1062
+ console.log(` ${p.id} ${p.name}`);
1063
+ }
1064
+ console.log("\nUse: browser-cli launch --connector bitbrowser --bitbrowser-profile <id> --url <url>");
1065
+ } catch (err) {
1066
+ console.error(formatError(err));
1067
+ process.exit(1);
1068
+ }
1069
+ });
1070
+ }
1071
+
887
1072
  // src/index.ts
888
1073
  var require2 = createRequire(import.meta.url);
889
1074
  var { version } = require2("../package.json");
@@ -903,4 +1088,6 @@ registerTabCommands(program);
903
1088
  registerScriptCommands(program);
904
1089
  registerProfileCommands(program);
905
1090
  registerZiNiaoCommands(program);
1091
+ registerRecipeCommands(program);
1092
+ registerBitBrowserCommands(program);
906
1093
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/browser-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {