@optima-chat/browser-cli 0.14.0 → 0.15.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.
Files changed (2) hide show
  1. package/dist/index.js +123 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -183,11 +183,12 @@ async function requestBinary(path) {
183
183
  return Buffer.from(arrayBuffer);
184
184
  }
185
185
  var BrowserAPI = class {
186
- async createSession(url, force, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId) {
186
+ async createSession(url, force, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId, chromeProfile) {
187
187
  const body = {};
188
188
  if (url) body.url = url;
189
189
  if (force) body.force = true;
190
190
  if (connector) body.connector = connector;
191
+ if (chromeProfile) body.chrome_profile = chromeProfile;
191
192
  if (ziniaoProfileId) body.ziniao_profile_id = ziniaoProfileId;
192
193
  if (bitbrowserProfileId) body.bitbrowser_profile_id = bitbrowserProfileId;
193
194
  if (feikuaProfileId) body.feikua_profile_id = feikuaProfileId;
@@ -211,6 +212,10 @@ var BrowserAPI = class {
211
212
  async getZiNiaoProfiles() {
212
213
  return request("/api/user/ziniao-profiles");
213
214
  }
215
+ // logged_in 缺席 = 没探(未开 check、无 homeUrl 或探测失败),下游按未知处理,别当 false。
216
+ async getChromeProfiles(check) {
217
+ return request(`/api/user/chrome-profiles${check ? "?check=true" : ""}`);
218
+ }
214
219
  async getBitBrowserProfiles() {
215
220
  return request("/api/user/bitbrowser-profiles");
216
221
  }
@@ -370,9 +375,10 @@ var BrowserAPI = class {
370
375
  async deleteScript(name) {
371
376
  await request(`/api/scripts/${encodeURIComponent(name)}`, { method: "DELETE" });
372
377
  }
373
- async runScript(scriptName, params = {}, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId) {
378
+ async runScript(scriptName, params = {}, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId, chromeProfile) {
374
379
  const payload = { params };
375
380
  if (connector) payload.connector = connector;
381
+ if (chromeProfile) payload.chrome_profile = chromeProfile;
376
382
  if (ziniaoProfileId) payload.ziniao_profile_id = ziniaoProfileId;
377
383
  if (bitbrowserProfileId) payload.bitbrowser_profile_id = bitbrowserProfileId;
378
384
  if (feikuaProfileId) payload.feikua_profile_id = feikuaProfileId;
@@ -430,9 +436,13 @@ function clearSessionId(onlyIfEquals) {
430
436
  }
431
437
 
432
438
  // src/formatter.ts
439
+ 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.";
433
440
  function formatPageState(state) {
434
441
  const lines = [];
435
442
  lines.push(`Page: ${state.title} (${state.url})`);
443
+ if (state.url_stale) {
444
+ lines.push(URL_STALE_WARNING);
445
+ }
436
446
  lines.push("");
437
447
  lines.push("DOM:");
438
448
  lines.push(state.dom);
@@ -472,6 +482,9 @@ function formatTabs(tabList) {
472
482
  for (const tab of tabList.tabs) {
473
483
  lines.push(` [${tab.tab_id}] ${tab.title} (${tab.url})`);
474
484
  }
485
+ if (tabList.url_stale) {
486
+ lines.push(URL_STALE_WARNING);
487
+ }
475
488
  return lines.join("\n");
476
489
  }
477
490
  function formatSearch(result) {
@@ -515,9 +528,9 @@ function formatTabsInline(tabs) {
515
528
 
516
529
  // src/commands/lifecycle.ts
517
530
  function registerLifecycleCommands(program2) {
518
- 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, bitbrowser, or feikua").option("--ziniao-profile <id>", "ZiNiao browser profile ID").option("--bitbrowser-profile <id>", "BitBrowser profile ID").option("--feikua-profile <id>", "Feikua (\u98DE\u8DE8) shop browser instance ID").action(async (opts) => {
531
+ 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, bitbrowser, or feikua").option("--ziniao-profile <id>", "ZiNiao browser profile ID").option("--bitbrowser-profile <id>", "BitBrowser profile ID").option("--feikua-profile <id>", "Feikua (\u98DE\u8DE8) shop browser instance ID").option("--chrome-profile <name>", "Chrome named profile (per-store login state; default: shared profile)").action(async (opts) => {
519
532
  try {
520
- const state = await api.createSession(opts.url, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile);
533
+ const state = await api.createSession(opts.url, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile, opts.chromeProfile);
521
534
  saveSessionId(state.session_id);
522
535
  console.log(formatLaunch(state));
523
536
  } catch (err) {
@@ -758,8 +771,9 @@ function registerTabCommands(program2) {
758
771
  }
759
772
 
760
773
  // src/commands/script.ts
761
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
762
- import { basename, dirname, resolve } from "path";
774
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
775
+ import { tmpdir } from "os";
776
+ import { basename, dirname, join as join3, resolve } from "path";
763
777
  var POLL_INTERVAL_MS = 2e3;
764
778
  var POLL_TIMEOUT_MS = 18e4;
765
779
  var REQUIRED_ARG_FLAG = {
@@ -772,16 +786,50 @@ function formatRequiredArgs(s) {
772
786
  const flags = s.required_args.map((a) => REQUIRED_ARG_FLAG[a] ?? `--param ${a}=<value>`).join(" ");
773
787
  return ` requires: ${flags}`;
774
788
  }
789
+ var ArtifactLostError = class extends Error {
790
+ };
791
+ var errMsg = (err) => err instanceof Error ? err.message : String(err);
792
+ function resolveArtifactTarget(out, name) {
793
+ if (!out) return resolve(`./${name}`);
794
+ const wantsDir = /[\\/]$/.test(out) || existsSync3(out) && statSync(out).isDirectory();
795
+ return wantsDir ? resolve(out, name) : resolve(out);
796
+ }
775
797
  async function deliverArtifact(task, out) {
776
798
  const output = task.output;
777
799
  if (!output) return null;
778
- if (output.path) return output.path;
800
+ if (output.path) return { path: output.path, size: output.size };
779
801
  if (!output.artifact_path) return null;
780
- const target = resolve(out ?? `./${basename(output.filename ?? "artifact")}`);
781
- const content = await api.fetchArtifact(output.artifact_path);
802
+ const name = basename(output.filename ?? "") || "artifact";
803
+ const target = resolveArtifactTarget(out, name);
782
804
  mkdirSync2(dirname(target), { recursive: true });
783
- writeFileSync3(target, content);
784
- return target;
805
+ const content = await api.fetchArtifact(output.artifact_path);
806
+ try {
807
+ writeFileSync3(target, content);
808
+ } catch (err) {
809
+ const fallback = join3(tmpdir(), name);
810
+ try {
811
+ writeFileSync3(fallback, content);
812
+ } catch (err2) {
813
+ throw new ArtifactLostError(
814
+ `Could not write ${target} (${errMsg(err)}) nor ${fallback} (${errMsg(err2)}); the artifact was already fetched and is no longer on the server`
815
+ );
816
+ }
817
+ console.error(`Could not write ${target} (${errMsg(err)}); saved to ${fallback} instead`);
818
+ return { path: fallback, size: content.length };
819
+ }
820
+ return { path: target, size: content.length };
821
+ }
822
+ function artifactUrl(artifactPath) {
823
+ return `${BROWSER_API_URL}${artifactPath}`;
824
+ }
825
+ function hasStagedArtifact(task) {
826
+ const output = task.output;
827
+ return task.status === "completed" && !!output?.artifact_path;
828
+ }
829
+ function downloadHint(task) {
830
+ const filename = task.output?.filename;
831
+ const dest = filename ? ` ./${basename(filename)}` : "";
832
+ return `Download with: browser-cli script-task ${task.id} --download${dest}`;
785
833
  }
786
834
  function formatScriptList(scripts) {
787
835
  if (scripts.length === 0) return "No scripts registered.";
@@ -804,7 +852,7 @@ function formatScriptTask(task) {
804
852
  lines.push("\u2713 Script completed");
805
853
  if (task.output) {
806
854
  for (const [k, v] of Object.entries(task.output)) {
807
- lines.push(`${k}: ${v}`);
855
+ lines.push(k === "artifact_path" ? `${k}: ${artifactUrl(String(v))}` : `${k}: ${v}`);
808
856
  }
809
857
  }
810
858
  } else if (task.status === "needs_fallback") {
@@ -881,7 +929,7 @@ function registerScriptCommands(program2) {
881
929
  process.exit(1);
882
930
  }
883
931
  });
884
- 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, bitbrowser, or feikua").option("--ziniao-profile <id>", "ZiNiao browser profile ID (with --connector ziniao)").option("--bitbrowser-profile <id>", "BitBrowser profile ID (with --connector bitbrowser)").option("--feikua-profile <id>", "Feikua (\u98DE\u8DE8) shop browser instance ID (with --connector feikua)").option("--out <path>", "Where to write the script's exported file (default: ./<filename>)").option("--no-poll", "Return immediately without polling").action(async (name, opts) => {
932
+ 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, bitbrowser, or feikua").option("--ziniao-profile <id>", "ZiNiao browser profile ID (with --connector ziniao)").option("--bitbrowser-profile <id>", "BitBrowser profile ID (with --connector bitbrowser)").option("--feikua-profile <id>", "Feikua (\u98DE\u8DE8) shop browser instance ID (with --connector feikua)").option("--chrome-profile <name>", "Chrome named profile (per-store login state, with --connector chrome)").option("--out <path>", "Where to write the script's exported file (default: ./<filename>)").option("--no-poll", "Return immediately without polling").action(async (name, opts) => {
885
933
  try {
886
934
  const params = {};
887
935
  if (opts.param) {
@@ -891,7 +939,7 @@ function registerScriptCommands(program2) {
891
939
  }
892
940
  }
893
941
  if (opts.out) params.out_path = resolve(opts.out);
894
- const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile);
942
+ const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile, opts.chromeProfile);
895
943
  console.log(`Script started (task: ${task.id})`);
896
944
  if (!opts.poll) {
897
945
  console.log(`Poll with: browser-cli script-task ${task.id}`);
@@ -915,12 +963,15 @@ function registerScriptCommands(program2) {
915
963
  if (result.status === "completed") {
916
964
  try {
917
965
  const saved = await deliverArtifact(result, opts.out);
918
- if (saved) console.log(`Saved -> ${saved}`);
966
+ if (saved) console.log(`Saved -> ${saved.path}`);
919
967
  } catch (err) {
920
968
  const staged = result.output?.artifact_path;
921
969
  console.error(`${formatError(err)}
922
970
  Script completed; only saving the file failed.`);
923
- if (staged) console.error(`The artifact is still at ${staged}.`);
971
+ if (staged && !(err instanceof ArtifactLostError)) {
972
+ console.error(`The artifact is still at ${artifactUrl(staged)}.
973
+ ${downloadHint(result)}`);
974
+ }
924
975
  process.exit(1);
925
976
  }
926
977
  }
@@ -929,12 +980,26 @@ Script completed; only saving the file failed.`);
929
980
  process.exit(1);
930
981
  }
931
982
  });
932
- program2.command("script-task").argument("<task-id>", "Task ID").description("Check script task status").action(async (taskId) => {
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) => {
933
984
  try {
934
985
  const task = await api.getScriptTask(taskId);
935
- console.log(formatScriptTask(task));
986
+ if (opts.download === void 0) {
987
+ console.log(formatScriptTask(task));
988
+ if (hasStagedArtifact(task)) console.log(downloadHint(task));
989
+ return;
990
+ }
991
+ if (task.status !== "completed") {
992
+ throw new Error(`Task ${taskId} is ${task.status} \u2014 nothing to download yet`);
993
+ }
994
+ const saved = await deliverArtifact(task, typeof opts.download === "string" ? opts.download : void 0);
995
+ if (!saved) throw new Error(`Task ${taskId} produced no file`);
996
+ console.log(`Saved -> ${saved.path}${saved.size === void 0 ? "" : ` (${saved.size} bytes)`}`);
936
997
  } catch (err) {
937
- console.error(formatError(err));
998
+ const gone = err.errorCode === "artifact_gone";
999
+ console.error(
1000
+ gone ? `${formatError(err)}
1001
+ Each artifact can be fetched once; if runscript printed "Saved ->", the file is already local.` : formatError(err)
1002
+ );
938
1003
  process.exit(1);
939
1004
  }
940
1005
  });
@@ -1116,6 +1181,44 @@ function registerZiNiaoCommands(program2) {
1116
1181
  });
1117
1182
  }
1118
1183
 
1184
+ // src/commands/chrome.ts
1185
+ function registerChromeCommands(program2) {
1186
+ const chrome = program2.command("chrome").description("Manage Chrome named profiles (per-store login state)");
1187
+ chrome.command("profiles").description("List Chrome named profiles (requires Desktop app online)").option("--check", "Probe login state for profiles with a homeUrl (opens a headed Chrome per profile, ~10s each)").action(async (opts) => {
1188
+ try {
1189
+ const result = await api.getChromeProfiles(opts.check);
1190
+ if (!result.connected) {
1191
+ console.log(formatDesktopNotConnected(result));
1192
+ process.exit(1);
1193
+ }
1194
+ console.log(`Chrome profiles (${result.profiles.length}):
1195
+ `);
1196
+ for (const p of result.profiles) {
1197
+ const label = p.name === "default" ? "\u5171\u4EAB\u9ED8\u8BA4" : p.label ?? "-";
1198
+ const login = p.logged_in === void 0 ? "-" : p.logged_in ? "\u2705" : "\u274C \u9700\u4EBA\u5DE5\u91CD\u767B";
1199
+ const home = p.homeUrl ? ` ${p.homeUrl}` : "";
1200
+ console.log(` ${p.name.padEnd(12)} ${label.padEnd(18)} running: ${p.running ? "yes" : "no"} logged_in: ${login}${home}`);
1201
+ }
1202
+ console.log("\nUse: browser-cli launch --connector chrome --chrome-profile <name> --url <url>");
1203
+ if (!opts.check) console.log("Add --check to probe login state (profiles with a homeUrl only).");
1204
+ } catch (err) {
1205
+ console.error(formatError(err));
1206
+ process.exit(1);
1207
+ }
1208
+ });
1209
+ chrome.command("open").description("Open a named profile's headed Chrome for manual login (the official login entry)").requiredOption("--profile <name>", "Chrome named profile to open (created on first use)").option("--url <url>", "Page to open, e.g. the seller center login page").action(async (opts) => {
1210
+ try {
1211
+ const state = await api.createSession(opts.url, false, "chrome", void 0, void 0, void 0, opts.profile);
1212
+ saveSessionId(state.session_id);
1213
+ console.log(`\u2713 Opened Chrome profile "${opts.profile}"${opts.url ? ` at ${opts.url}` : ""} (session: ${state.session_id})`);
1214
+ console.log("\u5728\u5F39\u51FA\u7684 Chrome \u7A97\u53E3\u91CC\u5B8C\u6210\u767B\u5F55;\u767B\u5F55\u6001\u4F1A\u7559\u5728\u8BE5 profile,\u4E4B\u540E\u5E26 --chrome-profile \u7684\u8C03\u7528\u76F4\u63A5\u590D\u7528\u3002");
1215
+ } catch (err) {
1216
+ console.error(formatError(err));
1217
+ process.exit(1);
1218
+ }
1219
+ });
1220
+ }
1221
+
1119
1222
  // src/commands/recipes.ts
1120
1223
  import { writeFileSync as writeFileSync4 } from "fs";
1121
1224
  import { resolve as resolve2 } from "path";
@@ -1339,6 +1442,7 @@ registerInfoCommands(program);
1339
1442
  registerTabCommands(program);
1340
1443
  registerScriptCommands(program);
1341
1444
  registerZiNiaoCommands(program);
1445
+ registerChromeCommands(program);
1342
1446
  registerRecipeCommands(program);
1343
1447
  registerBitBrowserCommands(program);
1344
1448
  registerFeikuaCommands(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/browser-cli",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {