@optima-chat/browser-cli 0.10.1 → 0.11.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 +147 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -183,13 +183,14 @@ 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) {
186
+ async createSession(url, force, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId) {
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
191
  if (ziniaoProfileId) body.ziniao_profile_id = ziniaoProfileId;
192
192
  if (bitbrowserProfileId) body.bitbrowser_profile_id = bitbrowserProfileId;
193
+ if (feikuaProfileId) body.feikua_profile_id = feikuaProfileId;
193
194
  return request("/api/sessions", {
194
195
  method: "POST",
195
196
  body: JSON.stringify(body)
@@ -213,6 +214,11 @@ var BrowserAPI = class {
213
214
  async getBitBrowserProfiles() {
214
215
  return request("/api/user/bitbrowser-profiles");
215
216
  }
217
+ // paired_count = 配对过的店铺浏览器总数(含没在运行的)。老桌面端不返回它,
218
+ // 所以是可选的 —— 缺失时调用方只能退回「从未配对」的指引。
219
+ async getFeikuaProfiles() {
220
+ return request("/api/user/feikua-profiles");
221
+ }
216
222
  async closeSession(sessionId) {
217
223
  await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
218
224
  }
@@ -243,8 +249,18 @@ var BrowserAPI = class {
243
249
  async input(sessionId, index, text, clear = true) {
244
250
  return this.actThenState(sessionId, "input", { index, text, clear });
245
251
  }
246
- async scroll(sessionId, down, pages = 1) {
247
- 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 });
248
264
  }
249
265
  async keys(sessionId, keys) {
250
266
  return this.actThenState(sessionId, "keys", { keys });
@@ -342,11 +358,12 @@ var BrowserAPI = class {
342
358
  async deleteScript(name) {
343
359
  await request(`/api/scripts/${encodeURIComponent(name)}`, { method: "DELETE" });
344
360
  }
345
- async runScript(scriptName, params = {}, connector, ziniaoProfileId, bitbrowserProfileId) {
361
+ async runScript(scriptName, params = {}, connector, ziniaoProfileId, bitbrowserProfileId, feikuaProfileId) {
346
362
  const payload = { params };
347
363
  if (connector) payload.connector = connector;
348
364
  if (ziniaoProfileId) payload.ziniao_profile_id = ziniaoProfileId;
349
365
  if (bitbrowserProfileId) payload.bitbrowser_profile_id = bitbrowserProfileId;
366
+ if (feikuaProfileId) payload.feikua_profile_id = feikuaProfileId;
350
367
  return request(`/api/scripts/${encodeURIComponent(scriptName)}/run`, {
351
368
  method: "POST",
352
369
  body: JSON.stringify(payload)
@@ -361,6 +378,9 @@ var BrowserAPI = class {
361
378
  body: JSON.stringify({ src, dest })
362
379
  });
363
380
  }
381
+ async listVideos() {
382
+ return request("/api/files/videos");
383
+ }
364
384
  };
365
385
  var api = new BrowserAPI();
366
386
 
@@ -450,7 +470,12 @@ function formatFind(result) {
450
470
  const lines = [`Found ${result.total} element(s):`, ""];
451
471
  for (const match of result.matches) {
452
472
  const attrs = Object.entries(match.attributes).map(([k, v]) => `${k}="${v}"`).join(" ");
453
- lines.push(` [${match.index}] <${match.tag} ${attrs}>`);
473
+ const state = Object.entries(match.state ?? {}).filter(([, v]) => v).map(([k]) => k).join(" ");
474
+ const head = ` [${match.index}] <${match.tag} ${attrs}>`;
475
+ lines.push(state ? `${head} [${state}]` : head);
476
+ if (match.text) {
477
+ lines.push(` ${match.text}`);
478
+ }
454
479
  }
455
480
  return lines.join("\n");
456
481
  }
@@ -469,9 +494,9 @@ function formatTabsInline(tabs) {
469
494
 
470
495
  // src/commands/lifecycle.ts
471
496
  function registerLifecycleCommands(program2) {
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) => {
497
+ 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) => {
473
498
  try {
474
- const state = await api.createSession(opts.url, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile);
499
+ const state = await api.createSession(opts.url, opts.force, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile);
475
500
  saveSessionId(state.session_id);
476
501
  console.log(formatLaunch(state));
477
502
  } catch (err) {
@@ -563,12 +588,36 @@ function registerInteractionCommands(program2) {
563
588
  process.exit(1);
564
589
  }
565
590
  });
566
- 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) => {
591
+ 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(
592
+ "--index <n>",
593
+ "Scroll inside this element instead of the window (for pages with inner scroll containers)"
594
+ ).action(async (direction, opts) => {
567
595
  try {
568
596
  const sessionId = loadSessionId();
569
597
  const down = direction.toLowerCase() === "down";
570
598
  const pages = parseInt(opts.pages, 10);
571
- const state = await api.scroll(sessionId, down, pages);
599
+ const index = opts.index === void 0 ? void 0 : parseInt(opts.index, 10);
600
+ const state = await api.scroll(sessionId, down, pages, index);
601
+ console.log(formatPageState(state));
602
+ } catch (err) {
603
+ console.error(formatError(err));
604
+ process.exit(1);
605
+ }
606
+ });
607
+ 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) => {
608
+ try {
609
+ const sessionId = loadSessionId();
610
+ const state = await api.clickTo(sessionId, selector, opts.native === true);
611
+ console.log(formatPageState(state));
612
+ } catch (err) {
613
+ console.error(formatError(err));
614
+ process.exit(1);
615
+ }
616
+ });
617
+ 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) => {
618
+ try {
619
+ const sessionId = loadSessionId();
620
+ const state = await api.scrollTo(sessionId, selector);
572
621
  console.log(formatPageState(state));
573
622
  } catch (err) {
574
623
  console.error(formatError(err));
@@ -694,7 +743,8 @@ var POLL_INTERVAL_MS = 2e3;
694
743
  var POLL_TIMEOUT_MS = 18e4;
695
744
  var REQUIRED_ARG_FLAG = {
696
745
  ziniao_profile_id: "--ziniao-profile <id>",
697
- bitbrowser_profile_id: "--bitbrowser-profile <id>"
746
+ bitbrowser_profile_id: "--bitbrowser-profile <id>",
747
+ feikua_profile_id: "--feikua-profile <id>"
698
748
  };
699
749
  function formatRequiredArgs(s) {
700
750
  if (!s.required_args?.length) return null;
@@ -810,7 +860,7 @@ function registerScriptCommands(program2) {
810
860
  process.exit(1);
811
861
  }
812
862
  });
813
- 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) => {
863
+ 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) => {
814
864
  try {
815
865
  const params = {};
816
866
  if (opts.param) {
@@ -821,7 +871,7 @@ function registerScriptCommands(program2) {
821
871
  }
822
872
  if (opts.out) params.out_path = resolve(opts.out);
823
873
  clearSessionId();
824
- const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile);
874
+ const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile);
825
875
  console.log(`Script started (task: ${task.id})`);
826
876
  if (!opts.poll) {
827
877
  console.log(`Poll with: browser-cli script-task ${task.id}`);
@@ -1049,6 +1099,7 @@ function registerZiNiaoCommands(program2) {
1049
1099
  // src/commands/recipes.ts
1050
1100
  import { writeFileSync as writeFileSync4 } from "fs";
1051
1101
  import { resolve as resolve2 } from "path";
1102
+ var PUBLISH_TIMEOUT_MS = 144e4;
1052
1103
  async function runExport(script, params, out, timeoutMs) {
1053
1104
  const outPath = resolve2(out);
1054
1105
  const result = await api.runRecipeScript(script, { ...params, out_path: outPath }, 2e3, timeoutMs);
@@ -1084,6 +1135,32 @@ function registerRecipeCommands(program2) {
1084
1135
  process.exit(1);
1085
1136
  }
1086
1137
  });
1138
+ program2.command("publish-video").description("Publish one shoppable video to TikTok Shop").requiredOption("--ziniao-profile <id>", "ZiNiao browser profile ID").requiredOption("--video <name>", "Video file name (from list-videos)").requiredOption("--product-query <keyword>", "Product search keyword, narrow enough to hit exactly one product (ask the user)").requiredOption(
1139
+ "--product-name <text>",
1140
+ "Display name on the video: max 30 chars, letters/digits/spaces only (ask the user)"
1141
+ ).requiredOption("--caption <text>", "Video caption").option("--hashtags <text>", "Comma-separated hashtags").option("--publish-url <url>", "Video publish page URL").action(async (opts) => {
1142
+ try {
1143
+ const params = {
1144
+ ziniao_profile_id: opts.ziniaoProfile,
1145
+ video: opts.video,
1146
+ product_query: opts.productQuery,
1147
+ product_name: opts.productName,
1148
+ caption: opts.caption
1149
+ };
1150
+ if (opts.hashtags) params.hashtags = opts.hashtags;
1151
+ if (opts.publishUrl) params.publish_url = opts.publishUrl;
1152
+ const result = await api.runRecipeScript(
1153
+ "tiktok_video_publish",
1154
+ params,
1155
+ 2e3,
1156
+ PUBLISH_TIMEOUT_MS
1157
+ );
1158
+ console.log(result.detail);
1159
+ } catch (err) {
1160
+ console.error(formatError(err));
1161
+ process.exit(1);
1162
+ }
1163
+ });
1087
1164
  }
1088
1165
 
1089
1166
  // src/commands/bitbrowser.ts
@@ -1113,7 +1190,55 @@ function registerBitBrowserCommands(program2) {
1113
1190
  });
1114
1191
  }
1115
1192
 
1193
+ // src/commands/feikua.ts
1194
+ function registerFeikuaCommands(program2) {
1195
+ const feikua = program2.command("feikua").description("Manage Feikua (\u98DE\u8DE8) anti-detect browser");
1196
+ feikua.command("profiles").description("List paired Feikua shop browsers (requires Desktop app online + extension paired)").action(async () => {
1197
+ try {
1198
+ const result = await api.getFeikuaProfiles();
1199
+ if (!result.connected) {
1200
+ console.log(formatDesktopNotConnected(result));
1201
+ process.exit(1);
1202
+ }
1203
+ if (result.profiles.length === 0) {
1204
+ if (result.paired_count) {
1205
+ console.log(
1206
+ `${result.paired_count} paired shop browser(s), but none are running. Open the Feikua shop browser you paired, then retry.`
1207
+ );
1208
+ return;
1209
+ }
1210
+ console.log(
1211
+ 'No paired shop browsers. Open the desktop app tray and choose "Pair a Shop Browser", then follow the on-screen code.'
1212
+ );
1213
+ return;
1214
+ }
1215
+ console.log(`Feikua shop browsers (${result.profiles.length}):
1216
+ `);
1217
+ for (const p of result.profiles) {
1218
+ console.log(` ${p.id} ${p.name}`);
1219
+ }
1220
+ console.log("\nUse: browser-cli launch --connector feikua --feikua-profile <id> --url <url>");
1221
+ } catch (err) {
1222
+ console.error(formatError(err));
1223
+ process.exit(1);
1224
+ }
1225
+ });
1226
+ }
1227
+
1116
1228
  // src/commands/files.ts
1229
+ function formatVideoList(videos) {
1230
+ if (videos.length === 0) {
1231
+ return `No videos found. Put your videos in:
1232
+ macOS/Linux: ~/Downloads/OptimaConnect/videos
1233
+ Windows: C:\\Users\\<\u7528\u6237\u540D>\\Downloads\\OptimaConnect\\videos
1234
+ Then try again.`;
1235
+ }
1236
+ return videos.map((v) => {
1237
+ const mb = (v.size / 1024 / 1024).toFixed(1);
1238
+ const time = v.mtime.slice(0, 19).replace("T", " ") + " UTC";
1239
+ return `${v.name} \u2014 ${mb} MB, ${time}`;
1240
+ }).join("\n");
1241
+ }
1117
1242
  function registerFileCommands(program2) {
1118
1243
  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) => {
1119
1244
  try {
@@ -1124,6 +1249,15 @@ function registerFileCommands(program2) {
1124
1249
  process.exit(1);
1125
1250
  }
1126
1251
  });
1252
+ program2.command("list-videos").description("List videos in your local video folder (requires Desktop app online)").action(async () => {
1253
+ try {
1254
+ const { videos } = await api.listVideos();
1255
+ console.log(formatVideoList(videos));
1256
+ } catch (err) {
1257
+ console.error(formatError(err));
1258
+ process.exit(1);
1259
+ }
1260
+ });
1127
1261
  }
1128
1262
 
1129
1263
  // src/index.ts
@@ -1146,5 +1280,6 @@ registerScriptCommands(program);
1146
1280
  registerZiNiaoCommands(program);
1147
1281
  registerRecipeCommands(program);
1148
1282
  registerBitBrowserCommands(program);
1283
+ registerFeikuaCommands(program);
1149
1284
  registerFileCommands(program);
1150
1285
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/browser-cli",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {