@optima-chat/browser-cli 0.12.0 → 0.13.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 +72 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -219,8 +219,20 @@ var BrowserAPI = class {
219
219
  async getFeikuaProfiles() {
220
220
  return request("/api/user/feikua-profiles");
221
221
  }
222
+ /** true = 后端确实关了它;false = 它本来就没了(idle reaper 已经收走)。
223
+ *
224
+ * 「已经没了」不是错误。抛出去的话调用方连本地记录都不敢清,下一次 close 再撞
225
+ * 同一堵墙 —— 而 close 是 SKILL.md 写给 agent 的标准恢复动作,必须幂等(#225)。 */
222
226
  async closeSession(sessionId) {
223
- await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
227
+ try {
228
+ await request(`/api/sessions/${sessionId}`, { method: "DELETE" });
229
+ return true;
230
+ } catch (err) {
231
+ if (err instanceof BrowserAPIError && err.errorCode === "session_not_found") {
232
+ return false;
233
+ }
234
+ throw err;
235
+ }
224
236
  }
225
237
  async getStatus(sessionId) {
226
238
  return request(`/api/sessions/${sessionId}/status`);
@@ -404,15 +416,17 @@ function loadSessionId() {
404
416
  }
405
417
  if (!existsSync2(SESSION_FILE)) {
406
418
  throw new Error(
407
- "No active browser session. Run 'browser-cli launch' first."
419
+ "No active browser session. Run 'browser-cli launch' first. (If the backend says you already have one, close it by id: browser-cli --session <id> close)"
408
420
  );
409
421
  }
410
422
  return readFileSync2(SESSION_FILE, "utf-8").trim();
411
423
  }
412
- function clearSessionId() {
413
- if (existsSync2(SESSION_FILE)) {
414
- unlinkSync(SESSION_FILE);
424
+ function clearSessionId(onlyIfEquals) {
425
+ if (!existsSync2(SESSION_FILE)) return;
426
+ if (onlyIfEquals !== void 0 && readFileSync2(SESSION_FILE, "utf-8").trim() !== onlyIfEquals) {
427
+ return;
415
428
  }
429
+ unlinkSync(SESSION_FILE);
416
430
  }
417
431
 
418
432
  // src/formatter.ts
@@ -435,6 +449,9 @@ function formatLaunch(state) {
435
449
  function formatClose(sessionId) {
436
450
  return `\u2713 Browser closed (session: ${sessionId})`;
437
451
  }
452
+ function formatAlreadyClosed(sessionId) {
453
+ return `\u2713 Session ${sessionId} was already closed (local record cleared)`;
454
+ }
438
455
  function formatStatus(status) {
439
456
  const lines = [];
440
457
  lines.push(`Session: ${status.session_id}`);
@@ -507,9 +524,9 @@ function registerLifecycleCommands(program2) {
507
524
  program2.command("close").description("Close the active browser session").action(async () => {
508
525
  try {
509
526
  const sessionId = loadSessionId();
510
- await api.closeSession(sessionId);
511
- clearSessionId();
512
- console.log(formatClose(sessionId));
527
+ const closed = await api.closeSession(sessionId);
528
+ clearSessionId(sessionId);
529
+ console.log(closed ? formatClose(sessionId) : formatAlreadyClosed(sessionId));
513
530
  } catch (err) {
514
531
  console.error(formatError(err));
515
532
  process.exit(1);
@@ -870,7 +887,6 @@ function registerScriptCommands(program2) {
870
887
  }
871
888
  }
872
889
  if (opts.out) params.out_path = resolve(opts.out);
873
- clearSessionId();
874
890
  const task = await api.runScript(name, params, opts.connector, opts.ziniaoProfile, opts.bitbrowserProfile, opts.feikuaProfile);
875
891
  console.log(`Script started (task: ${task.id})`);
876
892
  if (!opts.poll) {
@@ -1100,6 +1116,7 @@ function registerZiNiaoCommands(program2) {
1100
1116
  import { writeFileSync as writeFileSync4 } from "fs";
1101
1117
  import { resolve as resolve2 } from "path";
1102
1118
  var PUBLISH_TIMEOUT_MS = 18e5;
1119
+ var AD_REPORT_TIMEOUT_MS = 132e4;
1103
1120
  var PUBLISH_TIMEOUT_HINT = "\u4EFB\u52A1\u4ECD\u5728\u540E\u7AEF\u8FD0\u884C,\u89C6\u9891\u53EF\u80FD\u5DF2\u7ECF\u53D1\u51FA\u53BB\u4E86\u3002\u53D1\u5E03\u72B6\u6001\u672A\u77E5 \u2014\u2014 \u8BF7\u5230\u5356\u5BB6\u540E\u53F0\u786E\u8BA4,\u4E0D\u8981\u91CD\u8DD1\u672C\u547D\u4EE4(\u4F1A\u53D1\u51FA\u91CD\u590D\u89C6\u9891)\u3002";
1104
1121
  async function runExport(script, params, out, timeoutMs) {
1105
1122
  const outPath = resolve2(out);
@@ -1115,22 +1132,58 @@ async function runExport(script, params, out, timeoutMs) {
1115
1132
  console.log(`Saved ${result.filename} -> ${outPath} (${result.size} bytes)`);
1116
1133
  }
1117
1134
  function registerRecipeCommands(program2) {
1118
- 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) => {
1135
+ program2.command("export-ad-report").description("Export TikTok Shop ad data (per-creative by default) 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("--days <n>", "Date range: 7 / 30 / 90 / 180 / 365 (default: 7)").option(
1136
+ "--scope <scope>",
1137
+ "creative = per-creative rows (default) | daily = account-level daily totals"
1138
+ ).option("--out <path>", "Output CSV path", "./tt_ad_report.csv").action(
1139
+ async (opts) => {
1140
+ try {
1141
+ const params = { ziniao_profile_id: opts.ziniaoProfile };
1142
+ if (opts.reportUrl) params.report_url = opts.reportUrl;
1143
+ if (opts.days) params.days = opts.days;
1144
+ if (opts.scope) params.scope = opts.scope;
1145
+ await runExport("tiktok_video_ad_export", params, opts.out, AD_REPORT_TIMEOUT_MS);
1146
+ } catch (err) {
1147
+ console.error(formatError(err));
1148
+ process.exit(1);
1149
+ }
1150
+ }
1151
+ );
1152
+ 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) => {
1119
1153
  try {
1120
1154
  const params = { ziniao_profile_id: opts.ziniaoProfile };
1121
1155
  if (opts.reportUrl) params.report_url = opts.reportUrl;
1122
- await runExport("tiktok_video_ad_export", params, opts.out, 36e4);
1156
+ if (opts.days) params.days = opts.days;
1157
+ await runExport("tiktok_order_export", params, opts.out, 54e4);
1123
1158
  } catch (err) {
1124
1159
  console.error(formatError(err));
1125
1160
  process.exit(1);
1126
1161
  }
1127
1162
  });
1128
- 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) => {
1163
+ program2.command("publish-video-global").description("Publish one shoppable video to a TikTok Shop CROSS-BORDER store (one region per run)").requiredOption("--ziniao-profile <id>", "ZiNiao browser profile ID").requiredOption("--video <name>", "Video file name (from list-videos)").requiredOption("--account <username>", "Publishing account username; it also picks the region (one region per run)").requiredOption("--product-query <keyword>", "Product search keyword or product ID, narrow enough to hit exactly one product (ask the user)").requiredOption(
1164
+ "--product-name <text>",
1165
+ "Display name on the video: max 30 chars, letters/digits/spaces only (ask the user)"
1166
+ ).requiredOption("--caption <text>", "Video caption").option("--dry-run", "Fill everything, run the pre-publish checks, then STOP before clicking publish (nothing is published; the browser window stays open for you to inspect)").option("--hashtags <text>", "Comma-separated hashtags, bare words (the page adds the # itself)").option("--publish-url <url>", "Video publish page URL (include the right ?shop_region=)").action(async (opts) => {
1129
1167
  try {
1130
- const params = { ziniao_profile_id: opts.ziniaoProfile };
1131
- if (opts.reportUrl) params.report_url = opts.reportUrl;
1132
- if (opts.days) params.days = opts.days;
1133
- await runExport("tiktok_order_export", params, opts.out, 54e4);
1168
+ const params = {
1169
+ ziniao_profile_id: opts.ziniaoProfile,
1170
+ video: opts.video,
1171
+ account: opts.account,
1172
+ product_query: opts.productQuery,
1173
+ product_name: opts.productName,
1174
+ caption: opts.caption
1175
+ };
1176
+ if (opts.dryRun) params.dry_run = "1";
1177
+ if (opts.hashtags) params.hashtags = opts.hashtags;
1178
+ if (opts.publishUrl) params.publish_url = opts.publishUrl;
1179
+ const result = await api.runRecipeScript(
1180
+ "tiktok_video_publish_global",
1181
+ params,
1182
+ 2e3,
1183
+ PUBLISH_TIMEOUT_MS,
1184
+ PUBLISH_TIMEOUT_HINT
1185
+ );
1186
+ console.log(result.detail);
1134
1187
  } catch (err) {
1135
1188
  console.error(formatError(err));
1136
1189
  process.exit(1);
@@ -1139,7 +1192,7 @@ function registerRecipeCommands(program2) {
1139
1192
  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(
1140
1193
  "--product-name <text>",
1141
1194
  "Display name on the video: max 30 chars, letters/digits/spaces only (ask the user)"
1142
- ).requiredOption("--caption <text>", "Video caption").option("--hashtags <text>", "Comma-separated hashtags").option("--publish-url <url>", "Video publish page URL").action(async (opts) => {
1195
+ ).requiredOption("--caption <text>", "Video caption").option("--account <username>", "Publishing account username (default: the shop's official account; pass a marketing sub-account to post as it)").option("--dry-run", "Fill everything, run the pre-publish checks, then STOP before clicking publish (nothing is published; the browser window stays open for you to inspect)").option("--hashtags <text>", "Comma-separated hashtags, bare words (the page adds the # itself)").option("--publish-url <url>", "Video publish page URL").action(async (opts) => {
1143
1196
  try {
1144
1197
  const params = {
1145
1198
  ziniao_profile_id: opts.ziniaoProfile,
@@ -1148,6 +1201,8 @@ function registerRecipeCommands(program2) {
1148
1201
  product_name: opts.productName,
1149
1202
  caption: opts.caption
1150
1203
  };
1204
+ if (opts.account) params.account = opts.account;
1205
+ if (opts.dryRun) params.dry_run = "1";
1151
1206
  if (opts.hashtags) params.hashtags = opts.hashtags;
1152
1207
  if (opts.publishUrl) params.publish_url = opts.publishUrl;
1153
1208
  const result = await api.runRecipeScript(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/browser-cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {