@optima-chat/browser-cli 0.6.1 → 0.8.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 +227 -15
  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,19 @@ 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, force, connector, ziniaoProfileId, bitbrowserProfileId) {
170
178
  const body = {};
171
179
  if (url) body.url = url;
172
- if (profileId) body.profile_id = profileId;
173
180
  if (force) body.force = true;
174
181
  if (connector) body.connector = connector;
175
182
  if (ziniaoProfileId) body.ziniao_profile_id = ziniaoProfileId;
183
+ if (bitbrowserProfileId) body.bitbrowser_profile_id = bitbrowserProfileId;
176
184
  return request("/api/sessions", {
177
185
  method: "POST",
178
186
  body: JSON.stringify(body)
@@ -201,6 +209,9 @@ var BrowserAPI = class {
201
209
  async getZiNiaoProfiles() {
202
210
  return request("/api/user/ziniao-profiles");
203
211
  }
212
+ async getBitBrowserProfiles() {
213
+ return request("/api/user/bitbrowser-profiles");
214
+ }
204
215
  async closeSession(sessionId) {
205
216
  await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
206
217
  }
@@ -246,6 +257,29 @@ var BrowserAPI = class {
246
257
  async getScreenshot(sessionId) {
247
258
  return requestBinary(`/api/sessions/${sessionId}/screenshot`);
248
259
  }
260
+ async runRecipeScript(name, params, pollMs = 2e3, timeoutMs = 36e4) {
261
+ const started = await request(`/api/scripts/${name}/run`, {
262
+ method: "POST",
263
+ body: JSON.stringify({ params })
264
+ });
265
+ const deadline = Date.now() + timeoutMs;
266
+ for (; ; ) {
267
+ const task = await request(`/api/script-tasks/${started.id}`);
268
+ if (task.status === "completed") return task.output;
269
+ if (task.status === "failed" || task.status === "needs_fallback") {
270
+ throw new Error(task.error || task.output?.error || `recipe ${task.status}`);
271
+ }
272
+ if (Date.now() >= deadline) {
273
+ throw new Error(
274
+ `recipe ${name} did not finish within ${Math.round(timeoutMs / 1e3)}s (task ${started.id})`
275
+ );
276
+ }
277
+ await new Promise((r) => setTimeout(r, pollMs));
278
+ }
279
+ }
280
+ async fetchArtifact(artifactPath) {
281
+ return requestBinary(artifactPath);
282
+ }
249
283
  async search(sessionId, pattern) {
250
284
  return request(`/api/sessions/${sessionId}/search`, {
251
285
  method: "POST",
@@ -300,9 +334,11 @@ var BrowserAPI = class {
300
334
  async deleteScript(name) {
301
335
  await request(`/api/scripts/${encodeURIComponent(name)}`, { method: "DELETE" });
302
336
  }
303
- async runScript(scriptName, params = {}, profileId) {
337
+ async runScript(scriptName, params = {}, connector, ziniaoProfileId, bitbrowserProfileId) {
304
338
  const payload = { params };
305
- if (profileId) payload.profile_id = profileId;
339
+ if (connector) payload.connector = connector;
340
+ if (ziniaoProfileId) payload.ziniao_profile_id = ziniaoProfileId;
341
+ if (bitbrowserProfileId) payload.bitbrowser_profile_id = bitbrowserProfileId;
306
342
  return request(`/api/scripts/${encodeURIComponent(scriptName)}/run`, {
307
343
  method: "POST",
308
344
  body: JSON.stringify(payload)
@@ -404,6 +440,9 @@ function formatFind(result) {
404
440
  }
405
441
  return lines.join("\n");
406
442
  }
443
+ function formatDesktopNotConnected(result) {
444
+ 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";
445
+ }
407
446
  function formatError(err) {
408
447
  if (err instanceof Error) {
409
448
  return `\u2717 Error: ${err.message}`;
@@ -430,9 +469,9 @@ function formatProfileDelete(profileId) {
430
469
 
431
470
  // src/commands/lifecycle.ts
432
471
  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) => {
472
+ program2.command("launch").description("Launch a browser session").option("--url <url>", "Navigate to URL after launch").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
473
  try {
435
- const state = await api.createSession(opts.url, opts.profile, opts.force, opts.connector, opts.ziniaoProfile);
474
+ const state = await api.createSession(opts.url, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile);
436
475
  saveSessionId(state.session_id);
437
476
  console.log(formatLaunch(state));
438
477
  } catch (err) {
@@ -748,7 +787,7 @@ function registerScriptCommands(program2) {
748
787
  process.exit(1);
749
788
  }
750
789
  });
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) => {
790
+ program2.command("runscript").argument("<name>", "Script name").description("Run a script (polls until complete)").option("--param <params...>", "Parameters as key=value").option("--connector <type>", "Browser connector: chrome, ziniao, or bitbrowser").option("--ziniao-profile <id>", "ZiNiao browser profile ID (with --connector ziniao)").option("--bitbrowser-profile <id>", "BitBrowser profile ID (with --connector bitbrowser)").option("--no-poll", "Return immediately without polling").action(async (name, opts) => {
752
791
  try {
753
792
  const params = {};
754
793
  if (opts.param) {
@@ -758,7 +797,7 @@ function registerScriptCommands(program2) {
758
797
  }
759
798
  }
760
799
  clearSessionId();
761
- const task = await api.runScript(name, params, opts.profile);
800
+ const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile);
762
801
  console.log(`Script started (task: ${task.id})`);
763
802
  if (!opts.poll) {
764
803
  console.log(`Poll with: browser-cli script-task ${task.id}`);
@@ -795,7 +834,7 @@ function registerScriptCommands(program2) {
795
834
  });
796
835
  }
797
836
  function sleep(ms) {
798
- return new Promise((resolve) => setTimeout(resolve, ms));
837
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
799
838
  }
800
839
 
801
840
  // src/commands/profile.ts
@@ -820,6 +859,105 @@ function registerProfileCommands(program2) {
820
859
  });
821
860
  }
822
861
 
862
+ // src/ziniao-doctor.ts
863
+ 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";
864
+ var BOSS_GUIDANCE = [
865
+ "\u9700\u8981\u7D2B\u9E1F BOSS \u8D26\u53F7\u5B8C\u6210\u5F00\u901A\uFF08\u4E00\u6B21\u6027\u914D\u7F6E\uFF09\uFF1A",
866
+ " 1. \u767B\u5F55\u7D2B\u9E1F\u5F00\u653E\u5E73\u53F0\u63A7\u5236\u53F0\uFF0C\u5F00\u901A Webdriver \u6743\u9650\uFF1B",
867
+ " 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",
868
+ " 3. \u4E3A\u8BE5\u8D26\u53F7\u6388\u6743\u9700\u8981\u64CD\u4F5C\u7684\u5E97\u94FA\u3002",
869
+ `\u53C2\u8003\u6587\u6863: ${ZINIAO_WEBDRIVER_DOC_URL}`
870
+ ].join("\n");
871
+ function ziniaoErrorHint(message) {
872
+ if (/webdriver|开通|权限|permission/i.test(message)) {
873
+ return `\u7591\u4F3C\u672A\u5F00\u901A Webdriver \u6743\u9650\u6216\u8D26\u53F7\u6743\u9650\u4E0D\u8DB3\u3002${BOSS_GUIDANCE}`;
874
+ }
875
+ if (/错误次数太多/.test(message)) {
876
+ 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";
877
+ }
878
+ if (/用户名或密码错误|密码错误/.test(message)) {
879
+ 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";
880
+ }
881
+ return null;
882
+ }
883
+ async function runZiNiaoDoctor(client) {
884
+ const checks = [];
885
+ const done = () => ({ checks, ok: checks.every((c) => c.ok === true) });
886
+ let cfg;
887
+ try {
888
+ cfg = await client.getZiNiaoConfig();
889
+ } catch (err) {
890
+ checks.push({
891
+ name: "\u540E\u7AEF\u8FDE\u63A5",
892
+ ok: false,
893
+ detail: err.message,
894
+ hint: "\u786E\u8BA4 browser-backend \u6B63\u5728\u8FD0\u884C\uFF0C\u4E14 BROWSER_API_URL \u6307\u5411\u6B63\u786E\u5730\u5740\u3002"
895
+ });
896
+ return done();
897
+ }
898
+ checks.push({ name: "\u540E\u7AEF\u8FDE\u63A5", ok: true });
899
+ if (!cfg.configured) {
900
+ checks.push({
901
+ name: "\u7D2B\u9E1F\u51ED\u636E",
902
+ ok: false,
903
+ 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"
904
+ });
905
+ return done();
906
+ }
907
+ checks.push({ name: "\u7D2B\u9E1F\u51ED\u636E", ok: true, detail: `${cfg.company} / ${cfg.username}` });
908
+ try {
909
+ const r = await client.getZiNiaoProfiles();
910
+ if (!r.connected) {
911
+ checks.push({
912
+ name: "\u684C\u9762\u5E94\u7528",
913
+ ok: false,
914
+ detail: r.message,
915
+ hint: `\u8BF7\u542F\u52A8 Optima \u684C\u9762\u5E94\u7528\u5E76\u5B8C\u6210\u8FDE\u63A5\u3002${r.download_url ? `\u4E0B\u8F7D: ${r.download_url}` : ""}`.trim()
916
+ });
917
+ return done();
918
+ }
919
+ checks.push({ name: "\u684C\u9762\u5E94\u7528", ok: true });
920
+ if (!r.ziniao_available) {
921
+ checks.push({
922
+ name: "\u7D2B\u9E1F\u5BA2\u6237\u7AEF",
923
+ ok: false,
924
+ 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"
925
+ });
926
+ return done();
927
+ }
928
+ checks.push({ name: "\u7D2B\u9E1F\u5BA2\u6237\u7AEF", ok: true });
929
+ if (r.profiles.length === 0) {
930
+ checks.push({
931
+ name: "\u5E97\u94FA\u5217\u8868",
932
+ ok: "warn",
933
+ detail: "\u767B\u5F55\u6210\u529F\uFF0C\u4F46\u5E97\u94FA\u5217\u8868\u4E3A\u7A7A",
934
+ 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}`
935
+ });
936
+ return done();
937
+ }
938
+ checks.push({ name: "\u5E97\u94FA\u5217\u8868", ok: true, detail: `${r.profiles.length} \u4E2A\u5E97\u94FA\u53EF\u7528` });
939
+ } catch (err) {
940
+ checks.push({
941
+ name: "\u7D2B\u9E1F Webdriver \u901A\u9053",
942
+ ok: false,
943
+ detail: err.message,
944
+ hint: ziniaoErrorHint(err.message ?? "") ?? void 0
945
+ });
946
+ }
947
+ return done();
948
+ }
949
+ function formatDoctorReport(checks) {
950
+ const lines = [];
951
+ for (const c of checks) {
952
+ const mark = c.ok === true ? "\u2713" : c.ok === "warn" ? "\u26A0" : "\u2717";
953
+ lines.push(`${mark} ${c.name}${c.detail ? `: ${c.detail}` : ""}`);
954
+ if (c.hint) {
955
+ lines.push(...c.hint.split("\n").map((l) => ` ${l}`));
956
+ }
957
+ }
958
+ return lines.join("\n");
959
+ }
960
+
823
961
  // src/commands/ziniao.ts
824
962
  function registerZiNiaoCommands(program2) {
825
963
  const ziniao = program2.command("ziniao").description("Manage ZiNiao browser credentials");
@@ -851,28 +989,38 @@ function registerZiNiaoCommands(program2) {
851
989
  try {
852
990
  const result = await api.getZiNiaoProfiles();
853
991
  if (!result.connected) {
854
- console.log("Desktop app not connected. Start Optima Browser Operator first.");
992
+ console.log(formatDesktopNotConnected(result));
855
993
  process.exit(1);
856
994
  }
857
995
  if (!result.ziniao_available) {
858
996
  console.log("ZiNiao not available on Desktop app. Start ZiNiao WebDriver mode first.");
997
+ console.log("Run: browser-cli ziniao doctor \uFF08\u9010\u9879\u6392\u67E5\u7D2B\u9E1F\u901A\u9053\uFF09");
859
998
  process.exit(1);
860
999
  }
861
1000
  if (result.profiles.length === 0) {
862
1001
  console.log("No ZiNiao profiles found.");
1002
+ 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
1003
  return;
864
1004
  }
865
1005
  console.log(`ZiNiao profiles (${result.profiles.length}):
866
1006
  `);
867
1007
  for (const p of result.profiles) {
868
- console.log(` ${p.id} ${p.name}`);
1008
+ const site = p.platform_name ? ` [${p.platform_name}]` : "";
1009
+ console.log(` ${p.id} ${p.name}${site}`);
869
1010
  }
870
1011
  console.log("\nUse: browser-cli launch --connector ziniao --ziniao-profile <id> --url <url>");
871
1012
  } catch (err) {
872
1013
  console.error(formatError(err));
1014
+ const hint = ziniaoErrorHint(err?.message ?? "");
1015
+ if (hint) console.error(hint);
873
1016
  process.exit(1);
874
1017
  }
875
1018
  });
1019
+ ziniao.command("doctor").description("Diagnose the ZiNiao Webdriver chain (credentials / desktop / permission / stores)").action(async () => {
1020
+ const { checks, ok } = await runZiNiaoDoctor(api);
1021
+ console.log(formatDoctorReport(checks));
1022
+ if (!ok) process.exit(1);
1023
+ });
876
1024
  ziniao.command("clear").description("Remove ZiNiao credentials").action(async () => {
877
1025
  try {
878
1026
  await api.deleteZiNiaoConfig();
@@ -884,6 +1032,68 @@ function registerZiNiaoCommands(program2) {
884
1032
  });
885
1033
  }
886
1034
 
1035
+ // src/commands/recipes.ts
1036
+ import { writeFileSync as writeFileSync3 } from "fs";
1037
+ import { resolve } from "path";
1038
+ async function runExport(script, params, out, timeoutMs) {
1039
+ const outPath = resolve(out);
1040
+ const result = await api.runRecipeScript(script, { ...params, out_path: outPath }, 2e3, timeoutMs);
1041
+ if (!result.path) {
1042
+ writeFileSync3(outPath, await api.fetchArtifact(result.artifact_path));
1043
+ }
1044
+ console.log(`Saved ${result.filename} -> ${outPath} (${result.size} bytes)`);
1045
+ }
1046
+ function registerRecipeCommands(program2) {
1047
+ 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("--report-url <url>", "Ad dashboard URL (default: TikTok Shop MY local site)").option("--out <path>", "Output CSV path", "./tt_video_ad_7d.csv").action(async (opts) => {
1048
+ try {
1049
+ const params = { ziniao_profile_id: opts.ziniaoProfile };
1050
+ if (opts.reportUrl) params.report_url = opts.reportUrl;
1051
+ await runExport("tiktok_video_ad_export", params, opts.out, 36e4);
1052
+ } catch (err) {
1053
+ console.error(formatError(err));
1054
+ process.exit(1);
1055
+ }
1056
+ });
1057
+ program2.command("export-order-report").description("Export TikTok Shop order data into the session workspace").requiredOption("--ziniao-profile <id>", "ZiNiao browser profile ID").option("--report-url <url>", "Order list URL (default: TikTok Shop MY local site)").option("--days <n>", "Export the last N days (default: 7, max 365)").option("--out <path>", "Output CSV path", "./tt_orders.csv").action(async (opts) => {
1058
+ try {
1059
+ const params = { ziniao_profile_id: opts.ziniaoProfile };
1060
+ if (opts.reportUrl) params.report_url = opts.reportUrl;
1061
+ if (opts.days) params.days = opts.days;
1062
+ await runExport("tiktok_order_export", params, opts.out, 54e4);
1063
+ } catch (err) {
1064
+ console.error(formatError(err));
1065
+ process.exit(1);
1066
+ }
1067
+ });
1068
+ }
1069
+
1070
+ // src/commands/bitbrowser.ts
1071
+ function registerBitBrowserCommands(program2) {
1072
+ const bitbrowser = program2.command("bitbrowser").description("Manage BitBrowser (\u6BD4\u7279\u6D4F\u89C8\u5668) anti-detect browser");
1073
+ bitbrowser.command("profiles").description("List BitBrowser profiles (requires Desktop app online + BitBrowser running)").action(async () => {
1074
+ try {
1075
+ const result = await api.getBitBrowserProfiles();
1076
+ if (!result.connected) {
1077
+ console.log(formatDesktopNotConnected(result));
1078
+ process.exit(1);
1079
+ }
1080
+ if (result.profiles.length === 0) {
1081
+ console.log("No BitBrowser profiles found. Is BitBrowser running with its local API enabled?");
1082
+ return;
1083
+ }
1084
+ console.log(`BitBrowser profiles (${result.profiles.length}):
1085
+ `);
1086
+ for (const p of result.profiles) {
1087
+ console.log(` ${p.id} ${p.name}`);
1088
+ }
1089
+ console.log("\nUse: browser-cli launch --connector bitbrowser --bitbrowser-profile <id> --url <url>");
1090
+ } catch (err) {
1091
+ console.error(formatError(err));
1092
+ process.exit(1);
1093
+ }
1094
+ });
1095
+ }
1096
+
887
1097
  // src/index.ts
888
1098
  var require2 = createRequire(import.meta.url);
889
1099
  var { version } = require2("../package.json");
@@ -903,4 +1113,6 @@ registerTabCommands(program);
903
1113
  registerScriptCommands(program);
904
1114
  registerProfileCommands(program);
905
1115
  registerZiNiaoCommands(program);
1116
+ registerRecipeCommands(program);
1117
+ registerBitBrowserCommands(program);
906
1118
  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.8.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {