@optima-chat/browser-cli 0.10.0 → 0.10.2

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 +121 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22,24 +22,33 @@ var ENV_CONFIG = {
22
22
  // alicloud/stacks/cn-prod-ingress-sae/main.tf 的 yzsgo_sub=browser-api)。
23
23
  "cn-prod": {
24
24
  apiUrl: "https://browser-api.yzsgo.com"
25
+ },
26
+ // 阿里云 cn-stage 预发(独立于 AWS stage 的 .optima.onl;权威映射见 optima-terraform
27
+ // alicloud/stacks/cn-stage-ingress-sae/main.tf:browser-backend → sub=browser-api,
28
+ // 通配证书 *.stage.optima.chat 覆盖)。
29
+ // 漏这一档的后果:cn-stage 容器 OPTIMA_ENV=cn-stage → agent-runtime 写
30
+ // token.json.env="cn-stage" → 下面 fail-loud 抛 Unknown environment,而那句 throw
31
+ // 在模块加载期就执行,browser-cli 在任何子命令跑起来之前直接死掉。
32
+ // optima-scout 撞过同一个 bug(optima-gateway#1384 P0-2)。
33
+ "cn-stage": {
34
+ apiUrl: "https://browser-api.stage.optima.chat"
25
35
  }
26
36
  };
37
+ function isBrowserEnv(value) {
38
+ return Object.prototype.hasOwnProperty.call(ENV_CONFIG, value);
39
+ }
40
+ var TOKEN_ENV_MAP = {
41
+ prod: "production",
42
+ production: "production",
43
+ stage: "stage",
44
+ ci: "development",
45
+ development: "development",
46
+ "cn-prod": "cn-prod",
47
+ "cn-stage": "cn-stage"
48
+ };
27
49
  var OPTIMA_TOKEN_FILE = join(homedir(), ".optima", "token.json");
28
50
  function tokenEnvToBrowserEnv(tokenEnv) {
29
- switch (tokenEnv) {
30
- case "prod":
31
- case "production":
32
- return "production";
33
- case "stage":
34
- return "stage";
35
- case "ci":
36
- case "development":
37
- return "development";
38
- case "cn-prod":
39
- return "cn-prod";
40
- default:
41
- return null;
42
- }
51
+ return Object.prototype.hasOwnProperty.call(TOKEN_ENV_MAP, tokenEnv) ? TOKEN_ENV_MAP[tokenEnv] : null;
43
52
  }
44
53
  function readOptimaToken() {
45
54
  if (!existsSync(OPTIMA_TOKEN_FILE)) return null;
@@ -51,11 +60,11 @@ function readOptimaToken() {
51
60
  }
52
61
  function getCurrentEnv() {
53
62
  const browserEnv = process.env.BROWSER_ENV;
54
- if (browserEnv && ["production", "stage", "development", "cn-prod"].includes(browserEnv)) {
63
+ if (browserEnv && isBrowserEnv(browserEnv)) {
55
64
  return browserEnv;
56
65
  }
57
66
  const optimaEnv = process.env.OPTIMA_ENV;
58
- if (optimaEnv && ["production", "stage", "development", "cn-prod"].includes(optimaEnv)) {
67
+ if (optimaEnv && isBrowserEnv(optimaEnv)) {
59
68
  return optimaEnv;
60
69
  }
61
70
  const optimaToken = readOptimaToken();
@@ -63,7 +72,7 @@ function getCurrentEnv() {
63
72
  const mapped = tokenEnvToBrowserEnv(optimaToken.env);
64
73
  if (mapped) return mapped;
65
74
  throw new Error(
66
- `Unknown environment "${optimaToken.env}" in ~/.optima/token.json. Known environments: prod, stage, ci, cn-prod. Upgrade browser-cli to a version that knows this environment, or set BROWSER_API_URL to override the endpoint explicitly.`
75
+ `Unknown environment "${optimaToken.env}" in ~/.optima/token.json. Known environments: ${Object.keys(TOKEN_ENV_MAP).join(", ")}. Upgrade browser-cli to a version that knows this environment, or set BROWSER_API_URL to override the endpoint explicitly.`
67
76
  );
68
77
  }
69
78
  return "development";
@@ -174,13 +183,14 @@ async function requestBinary(path) {
174
183
  return Buffer.from(arrayBuffer);
175
184
  }
176
185
  var BrowserAPI = class {
177
- async createSession(url, force, connector, ziniaoProfileId, bitbrowserProfileId) {
186
+ async createSession(url, force, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId) {
178
187
  const body = {};
179
188
  if (url) body.url = url;
180
189
  if (force) body.force = true;
181
190
  if (connector) body.connector = connector;
182
191
  if (ziniaoProfileId) body.ziniao_profile_id = ziniaoProfileId;
183
192
  if (bitbrowserProfileId) body.bitbrowser_profile_id = bitbrowserProfileId;
193
+ if (feikuaProfileId) body.feikua_profile_id = feikuaProfileId;
184
194
  return request("/api/sessions", {
185
195
  method: "POST",
186
196
  body: JSON.stringify(body)
@@ -204,6 +214,11 @@ var BrowserAPI = class {
204
214
  async getBitBrowserProfiles() {
205
215
  return request("/api/user/bitbrowser-profiles");
206
216
  }
217
+ // paired_count = 配对过的店铺浏览器总数(含没在运行的)。老桌面端不返回它,
218
+ // 所以是可选的 —— 缺失时调用方只能退回「从未配对」的指引。
219
+ async getFeikuaProfiles() {
220
+ return request("/api/user/feikua-profiles");
221
+ }
207
222
  async closeSession(sessionId) {
208
223
  await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
209
224
  }
@@ -234,8 +249,18 @@ var BrowserAPI = class {
234
249
  async input(sessionId, index, text, clear = true) {
235
250
  return this.actThenState(sessionId, "input", { index, text, clear });
236
251
  }
237
- async scroll(sessionId, down, pages = 1) {
238
- return this.actThenState(sessionId, "scroll", { down, pages });
252
+ async scroll(sessionId, down, pages = 1, index) {
253
+ return this.actThenState(sessionId, "scroll", {
254
+ down,
255
+ pages,
256
+ ...index === void 0 ? {} : { index }
257
+ });
258
+ }
259
+ async clickTo(sessionId, selector, native = false) {
260
+ return this.actThenState(sessionId, "click_to", { selector, ...native ? { native } : {} });
261
+ }
262
+ async scrollTo(sessionId, selector) {
263
+ return this.actThenState(sessionId, "scroll_to", { selector });
239
264
  }
240
265
  async keys(sessionId, keys) {
241
266
  return this.actThenState(sessionId, "keys", { keys });
@@ -333,11 +358,12 @@ var BrowserAPI = class {
333
358
  async deleteScript(name) {
334
359
  await request(`/api/scripts/${encodeURIComponent(name)}`, { method: "DELETE" });
335
360
  }
336
- async runScript(scriptName, params = {}, connector, ziniaoProfileId, bitbrowserProfileId) {
361
+ async runScript(scriptName, params = {}, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId) {
337
362
  const payload = { params };
338
363
  if (connector) payload.connector = connector;
339
364
  if (ziniaoProfileId) payload.ziniao_profile_id = ziniaoProfileId;
340
365
  if (bitbrowserProfileId) payload.bitbrowser_profile_id = bitbrowserProfileId;
366
+ if (feikuaProfileId) payload.feikua_profile_id = feikuaProfileId;
341
367
  return request(`/api/scripts/${encodeURIComponent(scriptName)}/run`, {
342
368
  method: "POST",
343
369
  body: JSON.stringify(payload)
@@ -441,7 +467,12 @@ function formatFind(result) {
441
467
  const lines = [`Found ${result.total} element(s):`, ""];
442
468
  for (const match of result.matches) {
443
469
  const attrs = Object.entries(match.attributes).map(([k, v]) => `${k}="${v}"`).join(" ");
444
- lines.push(` [${match.index}] <${match.tag} ${attrs}>`);
470
+ const state = Object.entries(match.state ?? {}).filter(([, v]) => v).map(([k]) => k).join(" ");
471
+ const head = ` [${match.index}] <${match.tag} ${attrs}>`;
472
+ lines.push(state ? `${head} [${state}]` : head);
473
+ if (match.text) {
474
+ lines.push(` ${match.text}`);
475
+ }
445
476
  }
446
477
  return lines.join("\n");
447
478
  }
@@ -460,9 +491,9 @@ function formatTabsInline(tabs) {
460
491
 
461
492
  // src/commands/lifecycle.ts
462
493
  function registerLifecycleCommands(program2) {
463
- 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) => {
494
+ 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) => {
464
495
  try {
465
- const state = await api.createSession(opts.url, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile);
496
+ const state = await api.createSession(opts.url, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile);
466
497
  saveSessionId(state.session_id);
467
498
  console.log(formatLaunch(state));
468
499
  } catch (err) {
@@ -554,12 +585,36 @@ function registerInteractionCommands(program2) {
554
585
  process.exit(1);
555
586
  }
556
587
  });
557
- program2.command("scroll").description("Scroll the page up or down").argument("<direction>", "Scroll direction: up or down").option("--pages <n>", "Number of pages to scroll", "1").action(async (direction, opts) => {
588
+ program2.command("scroll").description("Scroll the page up or down").argument("<direction>", "Scroll direction: up or down").option("--pages <n>", "Number of pages to scroll", "1").option(
589
+ "--index <n>",
590
+ "Scroll inside this element instead of the window (for pages with inner scroll containers)"
591
+ ).action(async (direction, opts) => {
558
592
  try {
559
593
  const sessionId = loadSessionId();
560
594
  const down = direction.toLowerCase() === "down";
561
595
  const pages = parseInt(opts.pages, 10);
562
- const state = await api.scroll(sessionId, down, pages);
596
+ const index = opts.index === void 0 ? void 0 : parseInt(opts.index, 10);
597
+ const state = await api.scroll(sessionId, down, pages, index);
598
+ console.log(formatPageState(state));
599
+ } catch (err) {
600
+ console.error(formatError(err));
601
+ process.exit(1);
602
+ }
603
+ });
604
+ program2.command("click-to").description("Click the element matching a CSS selector (for elements with no DOM index)").argument("<selector>", "CSS selector of the element to click").option("--native", "Dispatch real mouse events (for controls that ignore synthetic clicks)").action(async (selector, opts) => {
605
+ try {
606
+ const sessionId = loadSessionId();
607
+ const state = await api.clickTo(sessionId, selector, opts.native === true);
608
+ console.log(formatPageState(state));
609
+ } catch (err) {
610
+ console.error(formatError(err));
611
+ process.exit(1);
612
+ }
613
+ });
614
+ program2.command("scroll-to").description("Scroll the element matching a CSS selector into view").argument("<selector>", "CSS selector of the element to scroll into view").action(async (selector) => {
615
+ try {
616
+ const sessionId = loadSessionId();
617
+ const state = await api.scrollTo(sessionId, selector);
563
618
  console.log(formatPageState(state));
564
619
  } catch (err) {
565
620
  console.error(formatError(err));
@@ -685,7 +740,8 @@ var POLL_INTERVAL_MS = 2e3;
685
740
  var POLL_TIMEOUT_MS = 18e4;
686
741
  var REQUIRED_ARG_FLAG = {
687
742
  ziniao_profile_id: "--ziniao-profile <id>",
688
- bitbrowser_profile_id: "--bitbrowser-profile <id>"
743
+ bitbrowser_profile_id: "--bitbrowser-profile <id>",
744
+ feikua_profile_id: "--feikua-profile <id>"
689
745
  };
690
746
  function formatRequiredArgs(s) {
691
747
  if (!s.required_args?.length) return null;
@@ -801,7 +857,7 @@ function registerScriptCommands(program2) {
801
857
  process.exit(1);
802
858
  }
803
859
  });
804
- 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("--out <path>", "Where to write the script's exported file (default: ./<filename>)").option("--no-poll", "Return immediately without polling").action(async (name, opts) => {
860
+ 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) => {
805
861
  try {
806
862
  const params = {};
807
863
  if (opts.param) {
@@ -812,7 +868,7 @@ function registerScriptCommands(program2) {
812
868
  }
813
869
  if (opts.out) params.out_path = resolve(opts.out);
814
870
  clearSessionId();
815
- const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile);
871
+ const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile);
816
872
  console.log(`Script started (task: ${task.id})`);
817
873
  if (!opts.poll) {
818
874
  console.log(`Poll with: browser-cli script-task ${task.id}`);
@@ -1104,6 +1160,41 @@ function registerBitBrowserCommands(program2) {
1104
1160
  });
1105
1161
  }
1106
1162
 
1163
+ // src/commands/feikua.ts
1164
+ function registerFeikuaCommands(program2) {
1165
+ const feikua = program2.command("feikua").description("Manage Feikua (\u98DE\u8DE8) anti-detect browser");
1166
+ feikua.command("profiles").description("List paired Feikua shop browsers (requires Desktop app online + extension paired)").action(async () => {
1167
+ try {
1168
+ const result = await api.getFeikuaProfiles();
1169
+ if (!result.connected) {
1170
+ console.log(formatDesktopNotConnected(result));
1171
+ process.exit(1);
1172
+ }
1173
+ if (result.profiles.length === 0) {
1174
+ if (result.paired_count) {
1175
+ console.log(
1176
+ `${result.paired_count} paired shop browser(s), but none are running. Open the Feikua shop browser you paired, then retry.`
1177
+ );
1178
+ return;
1179
+ }
1180
+ console.log(
1181
+ 'No paired shop browsers. Open the desktop app tray and choose "Pair a Shop Browser", then follow the on-screen code.'
1182
+ );
1183
+ return;
1184
+ }
1185
+ console.log(`Feikua shop browsers (${result.profiles.length}):
1186
+ `);
1187
+ for (const p of result.profiles) {
1188
+ console.log(` ${p.id} ${p.name}`);
1189
+ }
1190
+ console.log("\nUse: browser-cli launch --connector feikua --feikua-profile <id> --url <url>");
1191
+ } catch (err) {
1192
+ console.error(formatError(err));
1193
+ process.exit(1);
1194
+ }
1195
+ });
1196
+ }
1197
+
1107
1198
  // src/commands/files.ts
1108
1199
  function registerFileCommands(program2) {
1109
1200
  program2.command("send-file").description("Send a file from your workspace to your local download folder").requiredOption("--src <path>", "Source path in your workspace (e.g. ~/output/report.pdf)").requiredOption("--dest <path>", "Destination path relative to the download folder").action(async (opts) => {
@@ -1137,5 +1228,6 @@ registerScriptCommands(program);
1137
1228
  registerZiNiaoCommands(program);
1138
1229
  registerRecipeCommands(program);
1139
1230
  registerBitBrowserCommands(program);
1231
+ registerFeikuaCommands(program);
1140
1232
  registerFileCommands(program);
1141
1233
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/browser-cli",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {