@optima-chat/browser-cli 0.16.0 → 0.18.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.
- package/dist/index.js +60 -142
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -291,33 +291,6 @@ var BrowserAPI = class {
|
|
|
291
291
|
async getScreenshot(sessionId) {
|
|
292
292
|
return requestBinary(`/api/sessions/${sessionId}/screenshot`);
|
|
293
293
|
}
|
|
294
|
-
async runRecipeScript(name, params, pollMs = 2e3, timeoutMs = 36e4, timeoutHint) {
|
|
295
|
-
const started = await request(`/api/scripts/${name}/run`, {
|
|
296
|
-
method: "POST",
|
|
297
|
-
body: JSON.stringify({ params })
|
|
298
|
-
});
|
|
299
|
-
const deadline = Date.now() + timeoutMs;
|
|
300
|
-
for (; ; ) {
|
|
301
|
-
const task = await request(`/api/script-tasks/${started.id}`);
|
|
302
|
-
if (task.status === "completed") {
|
|
303
|
-
if (!task.output) {
|
|
304
|
-
throw new Error(
|
|
305
|
-
`recipe ${name} reported completed but returned no output (task ${started.id})`
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
return task.output;
|
|
309
|
-
}
|
|
310
|
-
if (task.status === "failed" || task.status === "needs_fallback") {
|
|
311
|
-
throw new Error(task.error || task.output?.error || `recipe ${task.status}`);
|
|
312
|
-
}
|
|
313
|
-
if (Date.now() >= deadline) {
|
|
314
|
-
throw new Error(
|
|
315
|
-
`recipe ${name} did not finish within ${Math.round(timeoutMs / 1e3)}s (task ${started.id})` + (timeoutHint ? ` \u2014\u2014 ${timeoutHint}` : "")
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
294
|
async fetchArtifact(artifactPath) {
|
|
322
295
|
return requestBinary(artifactPath);
|
|
323
296
|
}
|
|
@@ -850,6 +823,34 @@ function formatScriptList(scripts) {
|
|
|
850
823
|
}
|
|
851
824
|
return lines.join("\n").trimEnd();
|
|
852
825
|
}
|
|
826
|
+
var HALT_RENDERED_ELSEWHERE = /* @__PURE__ */ new Set(["completed_steps", "remaining_steps", "error"]);
|
|
827
|
+
var ARTIFACT_KEYS = /* @__PURE__ */ new Set(["artifact_path", "filename", "size"]);
|
|
828
|
+
var FIELD_MAX_CHARS = 2e3;
|
|
829
|
+
function formatFieldValue(v, taskId) {
|
|
830
|
+
if (v === null || v === void 0 || v === "") return null;
|
|
831
|
+
const s = typeof v === "string" ? v : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v);
|
|
832
|
+
const cps = Array.from(s);
|
|
833
|
+
return cps.length > FIELD_MAX_CHARS ? `${cps.slice(0, FIELD_MAX_CHARS).join("")}\u2026 (truncated, ${cps.length} chars; full: browser-cli script-task ${taskId} --json)` : s;
|
|
834
|
+
}
|
|
835
|
+
function haltOutputFields(task) {
|
|
836
|
+
const output = task.output ?? {};
|
|
837
|
+
const staged = typeof output.artifact_path === "string" && output.artifact_path !== "";
|
|
838
|
+
const lines = [];
|
|
839
|
+
for (const [k, v] of Object.entries(output)) {
|
|
840
|
+
if (HALT_RENDERED_ELSEWHERE.has(k)) continue;
|
|
841
|
+
if (staged && ARTIFACT_KEYS.has(k)) continue;
|
|
842
|
+
if (k === "snapshot") {
|
|
843
|
+
if (v !== null && v !== void 0 && v !== "") {
|
|
844
|
+
const n = Array.from(typeof v === "string" ? v : JSON.stringify(v)).length;
|
|
845
|
+
lines.push(`snapshot: (omitted, ${n} chars; full: browser-cli script-task ${task.id} --json)`);
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const s = formatFieldValue(v, task.id);
|
|
850
|
+
if (s !== null) lines.push(`${k}: ${s}`);
|
|
851
|
+
}
|
|
852
|
+
return lines;
|
|
853
|
+
}
|
|
853
854
|
function formatScriptTask(task) {
|
|
854
855
|
const lines = [];
|
|
855
856
|
if (task.status === "completed") {
|
|
@@ -887,12 +888,27 @@ function formatScriptTask(task) {
|
|
|
887
888
|
}
|
|
888
889
|
lines.push("");
|
|
889
890
|
}
|
|
891
|
+
const fields = haltOutputFields(task);
|
|
892
|
+
if (fields.length > 0) {
|
|
893
|
+
lines.push(...fields);
|
|
894
|
+
lines.push("");
|
|
895
|
+
}
|
|
890
896
|
const sid = task.session_id;
|
|
891
897
|
lines.push(
|
|
892
898
|
sid ? `Session ${sid} may still be open \u2014 continue with browser-cli --session ${sid} dom/click/input, or free it with browser-cli --session ${sid} close` : "Session id not reported \u2014 if one remains, close it by id: browser-cli --session <id> close"
|
|
893
899
|
);
|
|
894
900
|
} else if (task.status === "failed") {
|
|
895
901
|
lines.push(`\u2717 Script failed: ${task.error ?? "unknown error"}`);
|
|
902
|
+
const output = task.output ?? {};
|
|
903
|
+
const completed = output.completed_steps ?? [];
|
|
904
|
+
const remaining = output.remaining_steps ?? [];
|
|
905
|
+
if (completed.length > 0) {
|
|
906
|
+
lines.push(`\u2713 Completed: ${completed.map((s) => s.id).join(", ")}`);
|
|
907
|
+
}
|
|
908
|
+
if (remaining.length > 0) {
|
|
909
|
+
lines.push(`\u2717 Stopped at step [${completed.length + 1}] ${remaining[0].id}`);
|
|
910
|
+
}
|
|
911
|
+
lines.push(...haltOutputFields(task));
|
|
896
912
|
} else {
|
|
897
913
|
lines.push(`Script status: ${task.status} (step ${task.current_step}/${task.total_steps})`);
|
|
898
914
|
}
|
|
@@ -1007,10 +1023,17 @@ ${downloadHint(result)}`);
|
|
|
1007
1023
|
process.exit(1);
|
|
1008
1024
|
}
|
|
1009
1025
|
});
|
|
1010
|
-
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) => {
|
|
1026
|
+
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>)").option("--json", "Print the raw task (status / error / full output) as JSON").action(async (taskId, opts) => {
|
|
1011
1027
|
try {
|
|
1028
|
+
if (opts.json && opts.download !== void 0) {
|
|
1029
|
+
throw new Error("--json \u4E0E --download \u53EA\u80FD\u7ED9\u4E00\u4E2A");
|
|
1030
|
+
}
|
|
1012
1031
|
const task = await api.getScriptTask(taskId);
|
|
1013
1032
|
if (task.status === "needs_fallback" && task.session_id) saveSessionId(task.session_id);
|
|
1033
|
+
if (opts.json) {
|
|
1034
|
+
console.log(JSON.stringify(task, null, 2));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1014
1037
|
if (opts.download === void 0) {
|
|
1015
1038
|
console.log(formatScriptTask(task));
|
|
1016
1039
|
if (hasStagedArtifact(task)) console.log(downloadHint(task));
|
|
@@ -1033,7 +1056,7 @@ Each artifact can be fetched once; if runscript printed "Saved ->", the file is
|
|
|
1033
1056
|
});
|
|
1034
1057
|
}
|
|
1035
1058
|
function sleep(ms) {
|
|
1036
|
-
return new Promise((
|
|
1059
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1037
1060
|
}
|
|
1038
1061
|
|
|
1039
1062
|
// src/ziniao-doctor.ts
|
|
@@ -1045,16 +1068,20 @@ var BOSS_GUIDANCE = [
|
|
|
1045
1068
|
" 3. \u4E3A\u8BE5\u8D26\u53F7\u6388\u6743\u9700\u8981\u64CD\u4F5C\u7684\u5E97\u94FA\u3002",
|
|
1046
1069
|
`\u53C2\u8003\u6587\u6863: ${ZINIAO_WEBDRIVER_DOC_URL}`
|
|
1047
1070
|
].join("\n");
|
|
1071
|
+
var SELF_EXPLANATORY = /正在运行.{0,4}但未开启\s*WebDriver\s*模式/;
|
|
1072
|
+
var NOT_READY_HINT = "\u7D2B\u9E1F\u6CA1\u80FD\u5728 15 \u79D2\u5185\u8FDB\u5165 WebDriver \u6A21\u5F0F\uFF08\u4E0D\u662F\u6743\u9650\u95EE\u9898\uFF09\u3002\u2460 \u5148\u786E\u8BA4\u7D2B\u9E1F\u5DF2**\u5B8C\u5168\u9000\u51FA**\uFF08\u542B\u6258\u76D8/\u540E\u53F0\u8FDB\u7A0B\uFF09\u2014\u2014 \u5B83\u4EE5\u666E\u901A\u6A21\u5F0F\u8DD1\u7740\u65F6\u6211\u4EEC\u8D77\u4E0D\u4E86 WebDriver \u6A21\u5F0F\uFF1B\u2461 \u7136\u540E\u8BA9\u6211\u8FD9\u8FB9\u518D\u62C9\u4E00\u6B21\uFF08Windows \u4E0A\u82E5\u5B89\u88C5\u8DEF\u5F84\u662F\u63A2\u6D4B\u6765\u7684\uFF0C\u5931\u8D25\u90A3\u6B21\u4F1A\u6E05\u6389\u5B83\u5E76\u91CD\u67E5\u6CE8\u518C\u8868\uFF0C\u7B2C\u4E8C\u6B21\u5E38\u5E38\u5C31\u6210\uFF09\uFF1B\u2462 \u8FD8\u4E0D\u884C\u5C31\u95EE\u5E97\u4E3B\uFF1A\u7D2B\u9E1F\u88C5\u5728\u54EA\u3001\u662F\u4E0D\u662F\u8FD8\u88C5\u7740\u3002";
|
|
1048
1073
|
function ziniaoErrorHint(message) {
|
|
1049
|
-
if (
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1074
|
+
if (SELF_EXPLANATORY.test(message)) return null;
|
|
1075
|
+
if (/Failed to start ZiNiao WebDriver/i.test(message)) return NOT_READY_HINT;
|
|
1052
1076
|
if (/错误次数太多/.test(message)) {
|
|
1053
1077
|
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";
|
|
1054
1078
|
}
|
|
1055
1079
|
if (/用户名或密码错误|密码错误/.test(message)) {
|
|
1056
1080
|
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";
|
|
1057
1081
|
}
|
|
1082
|
+
if (/开通|权限|permission|授权|未开启|未启用|not enabled/i.test(message)) {
|
|
1083
|
+
return `\u7591\u4F3C\u672A\u5F00\u901A Webdriver \u6743\u9650\u6216\u8D26\u53F7\u6743\u9650\u4E0D\u8DB3\u3002${BOSS_GUIDANCE}`;
|
|
1084
|
+
}
|
|
1058
1085
|
return null;
|
|
1059
1086
|
}
|
|
1060
1087
|
async function runZiNiaoDoctor(client) {
|
|
@@ -1247,114 +1274,6 @@ function registerChromeCommands(program2) {
|
|
|
1247
1274
|
});
|
|
1248
1275
|
}
|
|
1249
1276
|
|
|
1250
|
-
// src/commands/recipes.ts
|
|
1251
|
-
import { writeFileSync as writeFileSync4 } from "fs";
|
|
1252
|
-
import { resolve as resolve2 } from "path";
|
|
1253
|
-
var PUBLISH_TIMEOUT_MS = 18e5;
|
|
1254
|
-
var AD_REPORT_TIMEOUT_MS = 132e4;
|
|
1255
|
-
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";
|
|
1256
|
-
async function runExport(script, params, out, timeoutMs) {
|
|
1257
|
-
const outPath = resolve2(out);
|
|
1258
|
-
const result = await api.runRecipeScript(script, { ...params, out_path: outPath }, 2e3, timeoutMs);
|
|
1259
|
-
if (!result.path) {
|
|
1260
|
-
if (!result.artifact_path) {
|
|
1261
|
-
throw new Error(
|
|
1262
|
-
`Script ${script} finished but delivered no file \u2014 no workspace path and no staged artifact. Nothing was written to ${outPath}.`
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
writeFileSync4(outPath, await api.fetchArtifact(result.artifact_path));
|
|
1266
|
-
}
|
|
1267
|
-
console.log(`Saved ${result.filename} -> ${outPath} (${result.size} bytes)`);
|
|
1268
|
-
}
|
|
1269
|
-
function registerRecipeCommands(program2) {
|
|
1270
|
-
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(
|
|
1271
|
-
"--scope <scope>",
|
|
1272
|
-
"creative = per-creative rows (default) | daily = account-level daily totals"
|
|
1273
|
-
).option("--out <path>", "Output CSV path", "./tt_ad_report.csv").action(
|
|
1274
|
-
async (opts) => {
|
|
1275
|
-
try {
|
|
1276
|
-
const params = { ziniao_profile_id: opts.ziniaoProfile };
|
|
1277
|
-
if (opts.reportUrl) params.report_url = opts.reportUrl;
|
|
1278
|
-
if (opts.days) params.days = opts.days;
|
|
1279
|
-
if (opts.scope) params.scope = opts.scope;
|
|
1280
|
-
await runExport("tiktok_video_ad_export", params, opts.out, AD_REPORT_TIMEOUT_MS);
|
|
1281
|
-
} catch (err) {
|
|
1282
|
-
console.error(formatError(err));
|
|
1283
|
-
process.exit(1);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
);
|
|
1287
|
-
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) => {
|
|
1288
|
-
try {
|
|
1289
|
-
const params = { ziniao_profile_id: opts.ziniaoProfile };
|
|
1290
|
-
if (opts.reportUrl) params.report_url = opts.reportUrl;
|
|
1291
|
-
if (opts.days) params.days = opts.days;
|
|
1292
|
-
await runExport("tiktok_order_export", params, opts.out, 54e4);
|
|
1293
|
-
} catch (err) {
|
|
1294
|
-
console.error(formatError(err));
|
|
1295
|
-
process.exit(1);
|
|
1296
|
-
}
|
|
1297
|
-
});
|
|
1298
|
-
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(
|
|
1299
|
-
"--product-name <text>",
|
|
1300
|
-
"Display name on the video: max 30 chars, letters/digits/spaces only (ask the user)"
|
|
1301
|
-
).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) => {
|
|
1302
|
-
try {
|
|
1303
|
-
const params = {
|
|
1304
|
-
ziniao_profile_id: opts.ziniaoProfile,
|
|
1305
|
-
video: opts.video,
|
|
1306
|
-
account: opts.account,
|
|
1307
|
-
product_query: opts.productQuery,
|
|
1308
|
-
product_name: opts.productName,
|
|
1309
|
-
caption: opts.caption
|
|
1310
|
-
};
|
|
1311
|
-
if (opts.dryRun) params.dry_run = "1";
|
|
1312
|
-
if (opts.hashtags) params.hashtags = opts.hashtags;
|
|
1313
|
-
if (opts.publishUrl) params.publish_url = opts.publishUrl;
|
|
1314
|
-
const result = await api.runRecipeScript(
|
|
1315
|
-
"tiktok_video_publish_global",
|
|
1316
|
-
params,
|
|
1317
|
-
2e3,
|
|
1318
|
-
PUBLISH_TIMEOUT_MS,
|
|
1319
|
-
PUBLISH_TIMEOUT_HINT
|
|
1320
|
-
);
|
|
1321
|
-
console.log(result.detail);
|
|
1322
|
-
} catch (err) {
|
|
1323
|
-
console.error(formatError(err));
|
|
1324
|
-
process.exit(1);
|
|
1325
|
-
}
|
|
1326
|
-
});
|
|
1327
|
-
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(
|
|
1328
|
-
"--product-name <text>",
|
|
1329
|
-
"Display name on the video: max 30 chars, letters/digits/spaces only (ask the user)"
|
|
1330
|
-
).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) => {
|
|
1331
|
-
try {
|
|
1332
|
-
const params = {
|
|
1333
|
-
ziniao_profile_id: opts.ziniaoProfile,
|
|
1334
|
-
video: opts.video,
|
|
1335
|
-
product_query: opts.productQuery,
|
|
1336
|
-
product_name: opts.productName,
|
|
1337
|
-
caption: opts.caption
|
|
1338
|
-
};
|
|
1339
|
-
if (opts.account) params.account = opts.account;
|
|
1340
|
-
if (opts.dryRun) params.dry_run = "1";
|
|
1341
|
-
if (opts.hashtags) params.hashtags = opts.hashtags;
|
|
1342
|
-
if (opts.publishUrl) params.publish_url = opts.publishUrl;
|
|
1343
|
-
const result = await api.runRecipeScript(
|
|
1344
|
-
"tiktok_video_publish",
|
|
1345
|
-
params,
|
|
1346
|
-
2e3,
|
|
1347
|
-
PUBLISH_TIMEOUT_MS,
|
|
1348
|
-
PUBLISH_TIMEOUT_HINT
|
|
1349
|
-
);
|
|
1350
|
-
console.log(result.detail);
|
|
1351
|
-
} catch (err) {
|
|
1352
|
-
console.error(formatError(err));
|
|
1353
|
-
process.exit(1);
|
|
1354
|
-
}
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
1277
|
// src/commands/bitbrowser.ts
|
|
1359
1278
|
function registerBitBrowserCommands(program2) {
|
|
1360
1279
|
const bitbrowser = program2.command("bitbrowser").description("Manage BitBrowser (\u6BD4\u7279\u6D4F\u89C8\u5668) anti-detect browser");
|
|
@@ -1400,7 +1319,7 @@ function registerFeikuaCommands(program2) {
|
|
|
1400
1319
|
return;
|
|
1401
1320
|
}
|
|
1402
1321
|
console.log(
|
|
1403
|
-
'No paired shop browsers.
|
|
1322
|
+
'No paired shop browsers. In the desktop app tray, click \u300C\u914D\u5BF9\u98DE\u8DE8\u6D4F\u89C8\u5668\u2026\u300D/ "Pair a Feikua Browser\u2026" (older desktop versions: \u300C\u914D\u5BF9\u65B0\u5E97\u94FA\u6D4F\u89C8\u5668\u2026\u300D/ "Pair a Shop Browser\u2026"), then follow the on-screen code.'
|
|
1404
1323
|
);
|
|
1405
1324
|
return;
|
|
1406
1325
|
}
|
|
@@ -1471,7 +1390,6 @@ registerTabCommands(program);
|
|
|
1471
1390
|
registerScriptCommands(program);
|
|
1472
1391
|
registerZiNiaoCommands(program);
|
|
1473
1392
|
registerChromeCommands(program);
|
|
1474
|
-
registerRecipeCommands(program);
|
|
1475
1393
|
registerBitBrowserCommands(program);
|
|
1476
1394
|
registerFeikuaCommands(program);
|
|
1477
1395
|
registerFileCommands(program);
|