siluzan-tso-cli 1.1.37-beta.3 → 1.1.37-beta.4

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/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.37-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.37-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -110511,6 +110511,7 @@ function validateCampaignExtensionsForBatchJob(campaign, errors, warnings, lengt
110511
110511
 
110512
110512
  // src/commands/ad/keyword.ts
110513
110513
  init_auth();
110514
+ import { readFileSync as readFileSync4 } from "fs";
110514
110515
  init_cli_json_snapshot();
110515
110516
  init_strip_legacy_google_fields();
110516
110517
  init_cli_table();
@@ -110539,6 +110540,68 @@ function formatKeywordTextForMatchType(rawCoreOrDisplay, matchType) {
110539
110540
  }
110540
110541
  }
110541
110542
  }
110543
+ function parseKeywordsCsv(raw) {
110544
+ const out = [];
110545
+ let cur = "";
110546
+ let inDouble = false;
110547
+ let inBracket = false;
110548
+ for (let i = 0; i < raw.length; i++) {
110549
+ const ch = raw[i];
110550
+ if (ch === '"' && !inBracket) {
110551
+ inDouble = !inDouble;
110552
+ cur += ch;
110553
+ continue;
110554
+ }
110555
+ if (ch === "[" && !inDouble && !inBracket) {
110556
+ inBracket = true;
110557
+ cur += ch;
110558
+ continue;
110559
+ }
110560
+ if (ch === "]" && inBracket) {
110561
+ inBracket = false;
110562
+ cur += ch;
110563
+ continue;
110564
+ }
110565
+ if (ch === "," && !inDouble && !inBracket) {
110566
+ const t = cur.trim();
110567
+ if (t) out.push(t);
110568
+ cur = "";
110569
+ continue;
110570
+ }
110571
+ cur += ch;
110572
+ }
110573
+ const last = cur.trim();
110574
+ if (last) out.push(last);
110575
+ return out;
110576
+ }
110577
+ function loadKeywordsFromFile(filePath) {
110578
+ let raw;
110579
+ try {
110580
+ raw = JSON.parse(readFileSync4(filePath, "utf8"));
110581
+ } catch (err) {
110582
+ throw new Error(
110583
+ `\u8BFB\u53D6\u5173\u952E\u8BCD\u6587\u4EF6\u5931\u8D25\uFF08${filePath}\uFF09\uFF1A${err instanceof Error ? err.message : String(err)}`
110584
+ );
110585
+ }
110586
+ if (!Array.isArray(raw) || raw.length === 0) {
110587
+ throw new Error(`\u5173\u952E\u8BCD\u6587\u4EF6\u987B\u4E3A\u975E\u7A7A JSON \u5B57\u7B26\u4E32\u6570\u7EC4\uFF1A${filePath}`);
110588
+ }
110589
+ const list = raw.map((x) => typeof x === "string" ? x.trim() : "").filter((s) => s.length > 0);
110590
+ if (list.length === 0) {
110591
+ throw new Error(`\u5173\u952E\u8BCD\u6587\u4EF6\u65E0\u6709\u6548\u5B57\u7B26\u4E32\uFF1A${filePath}`);
110592
+ }
110593
+ return list;
110594
+ }
110595
+ function resolveKeywordTexts(opts) {
110596
+ const fromCsv = opts.keywords?.length ? opts.keywords : [];
110597
+ const fromFile = opts.keywordsFile ? loadKeywordsFromFile(opts.keywordsFile) : [];
110598
+ const merged = [...fromCsv, ...fromFile];
110599
+ if (merged.length === 0) {
110600
+ throw new Error("\u8BF7\u4F20 --keywords \u548C/\u6216 --keywords-file\uFF08\u591A\u8BCD\u77ED\u8BED\u76F4\u63A5\u5199\u7A7A\u683C\u5373\u53EF\uFF0C\u5982 how to make\uFF09");
110601
+ }
110602
+ if (!opts.matchType) return merged;
110603
+ return merged.map((k) => formatKeywordTextForMatchType(k, opts.matchType));
110604
+ }
110542
110605
  function firstKeywordTextFromRecord(k) {
110543
110606
  const kt = k["keywordText"];
110544
110607
  if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
@@ -110661,8 +110724,21 @@ ${label}\uFF08\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672C
110661
110724
  async function runAdKeywordCreate(opts) {
110662
110725
  const config = loadConfig(opts.token);
110663
110726
  const googleApiUrl = requireGoogleApi(config);
110727
+ let keywords;
110728
+ try {
110729
+ keywords = resolveKeywordTexts({
110730
+ keywords: opts.keywords,
110731
+ keywordsFile: opts.keywordsFile,
110732
+ matchType: opts.matchType
110733
+ });
110734
+ } catch (err) {
110735
+ console.error(`
110736
+ \u274C ${err instanceof Error ? err.message : String(err)}
110737
+ `);
110738
+ process.exit(1);
110739
+ }
110664
110740
  const body = {
110665
- KeywordText: opts.keywords,
110741
+ KeywordText: keywords,
110666
110742
  FinalUrl: opts.finalUrl ?? "",
110667
110743
  AdGroup: opts.adgroupName,
110668
110744
  AdGroupId: opts.adgroupId,
@@ -110688,20 +110764,33 @@ async function runAdKeywordCreate(opts) {
110688
110764
  section: `ad-keyword-create-${opts.account}-${opts.adgroupId}`,
110689
110765
  commandLabel: "ad keyword-create",
110690
110766
  commandHint: `${opts.account}:${opts.adgroupId}`,
110691
- payload: { request: { adgroupId: opts.adgroupId, count: opts.keywords.length }, response },
110767
+ payload: { request: { adgroupId: opts.adgroupId, count: keywords.length }, response },
110692
110768
  idSuffix: `${opts.account}-${opts.adgroupId}`
110693
110769
  })) {
110694
110770
  return;
110695
110771
  }
110696
110772
  console.log(`
110697
- \u2705 \u5DF2\u6DFB\u52A0 ${opts.keywords.length} \u4E2A\u5173\u952E\u8BCD\u5230\u5E7F\u544A\u7EC4\u300C${opts.adgroupName}\u300D
110773
+ \u2705 \u5DF2\u6DFB\u52A0 ${keywords.length} \u4E2A\u5173\u952E\u8BCD\u5230\u5E7F\u544A\u7EC4\u300C${opts.adgroupName}\u300D
110698
110774
  `);
110699
110775
  }
110700
110776
  async function runAdNegativeKeywordCreate(opts) {
110701
110777
  const config = loadConfig(opts.token);
110702
110778
  const googleApiUrl = requireGoogleApi(config);
110779
+ let keywords;
110780
+ try {
110781
+ keywords = resolveKeywordTexts({
110782
+ keywords: opts.keywords,
110783
+ keywordsFile: opts.keywordsFile,
110784
+ matchType: opts.matchType
110785
+ });
110786
+ } catch (err) {
110787
+ console.error(`
110788
+ \u274C ${err instanceof Error ? err.message : String(err)}
110789
+ `);
110790
+ process.exit(1);
110791
+ }
110703
110792
  const body = {
110704
- KeywordText: opts.keywords,
110793
+ KeywordText: keywords,
110705
110794
  CampaignId: opts.campaignId,
110706
110795
  Campaign: opts.campaignName
110707
110796
  };
@@ -110720,7 +110809,7 @@ async function runAdNegativeKeywordCreate(opts) {
110720
110809
  process.exit(1);
110721
110810
  }
110722
110811
  console.log(`
110723
- \u2705 \u5DF2\u5728${level}\u5C42\u7EA7\u6DFB\u52A0 ${opts.keywords.length} \u4E2A\u5426\u5B9A\u5173\u952E\u8BCD
110812
+ \u2705 \u5DF2\u5728${level}\u5C42\u7EA7\u6DFB\u52A0 ${keywords.length} \u4E2A\u5426\u5B9A\u5173\u952E\u8BCD
110724
110813
  `);
110725
110814
  }
110726
110815
  async function runAdKeywordDelete(opts) {
@@ -111957,7 +112046,7 @@ function runCampaignCreateValidation(cfg) {
111957
112046
  }
111958
112047
 
111959
112048
  // src/commands/ad/campaign-load.ts
111960
- import { readFileSync as readFileSync4 } from "fs";
112049
+ import { readFileSync as readFileSync5 } from "fs";
111961
112050
  function stripMetaKeys(value) {
111962
112051
  if (Array.isArray(value)) {
111963
112052
  return value.map((item) => stripMetaKeys(item));
@@ -111988,7 +112077,7 @@ function loadCampaignCreateConfig(configFile) {
111988
112077
  function tryLoadCampaignCreateConfig(configFile) {
111989
112078
  let raw;
111990
112079
  try {
111991
- raw = JSON.parse(readFileSync4(configFile, "utf8"));
112080
+ raw = JSON.parse(readFileSync5(configFile, "utf8"));
111992
112081
  } catch {
111993
112082
  return null;
111994
112083
  }
@@ -113017,12 +113106,12 @@ async function runAdEdit(opts) {
113017
113106
 
113018
113107
  // src/commands/ad/extension.ts
113019
113108
  init_auth();
113020
- import { readFileSync as readFileSync7 } from "fs";
113109
+ import { readFileSync as readFileSync8 } from "fs";
113021
113110
  init_cli_json_snapshot();
113022
113111
  init_strip_legacy_google_fields();
113023
113112
 
113024
113113
  // src/commands/ad/extension-lead-form.ts
113025
- import { readFileSync as readFileSync5 } from "fs";
113114
+ import { readFileSync as readFileSync6 } from "fs";
113026
113115
  var VALID_LEAD_FORM_INPUT_TYPES = [
113027
113116
  "FULL_NAME",
113028
113117
  "FIRST_NAME",
@@ -113046,7 +113135,7 @@ var LEAD_FORM_CTA_DESCRIPTION_MAX_LEN = 30;
113046
113135
  function loadLeadFormExtensionConfig(configFile) {
113047
113136
  let raw;
113048
113137
  try {
113049
- raw = JSON.parse(readFileSync5(configFile, "utf8"));
113138
+ raw = JSON.parse(readFileSync6(configFile, "utf8"));
113050
113139
  } catch {
113051
113140
  console.error(`
113052
113141
  \u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
@@ -113158,7 +113247,7 @@ function buildLeadFormExtensionBody(cfg, extensionId) {
113158
113247
  }
113159
113248
 
113160
113249
  // src/commands/ad/extension-whatsapp.ts
113161
- import { readFileSync as readFileSync6 } from "fs";
113250
+ import { readFileSync as readFileSync7 } from "fs";
113162
113251
  var VALID_WHATSAPP_CTA_SELECTIONS = [
113163
113252
  "CONTACT_US",
113164
113253
  "LEARN_MORE",
@@ -113170,7 +113259,7 @@ var VALID_WHATSAPP_CTA_SELECTIONS = [
113170
113259
  function loadWhatsappExtensionConfig(configFile) {
113171
113260
  let raw;
113172
113261
  try {
113173
- raw = JSON.parse(readFileSync6(configFile, "utf8"));
113262
+ raw = JSON.parse(readFileSync7(configFile, "utf8"));
113174
113263
  } catch {
113175
113264
  console.error(`
113176
113265
  \u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${configFile}\uFF09
@@ -113632,7 +113721,7 @@ async function runAdExtensionUpdate(opts) {
113632
113721
  const googleApiUrl = requireGoogleApi(config);
113633
113722
  let raw;
113634
113723
  try {
113635
- raw = stripMetaKeys(JSON.parse(readFileSync7(opts.configFile, "utf8")));
113724
+ raw = stripMetaKeys(JSON.parse(readFileSync8(opts.configFile, "utf8")));
113636
113725
  } catch {
113637
113726
  console.error(`
113638
113727
  \u274C \u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6\u5931\u8D25\uFF08${opts.configFile}\uFF09
@@ -115826,7 +115915,7 @@ async function runPmaxImageValidation(configFile, cfg) {
115826
115915
  // src/commands/ad/pmax-image-resolve.ts
115827
115916
  init_auth();
115828
115917
  import { basename as basename4, dirname as dirname8, isAbsolute as isAbsolute3, resolve as resolve7 } from "path";
115829
- import { readFileSync as readFileSync8 } from "fs";
115918
+ import { readFileSync as readFileSync9 } from "fs";
115830
115919
 
115831
115920
  // src/commands/ad/pmax-urls.ts
115832
115921
  function pmaxChannelTypesUrl(googleApiUrl) {
@@ -115989,7 +116078,7 @@ async function uploadPmaxImageFileBase64(config, googleApiUrl, accountId, absIma
115989
116078
  async function uploadPmaxImageFile(config, googleApiUrl, accountId, absImagePath, name2, verbose) {
115990
116079
  const fileName = basename4(absImagePath);
115991
116080
  const assetName = (name2 ?? fileName).trim() || fileName;
115992
- const fileBuffer = readFileSync8(absImagePath);
116081
+ const fileBuffer = readFileSync9(absImagePath);
115993
116082
  if (fileBuffer.length <= MAX_BASE64_UPLOAD_BYTES) {
115994
116083
  return uploadPmaxImageFileBase64(
115995
116084
  config,
@@ -116075,7 +116164,7 @@ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "squa
116075
116164
  }
116076
116165
 
116077
116166
  // src/commands/ad/pmax-load.ts
116078
- import { readFileSync as readFileSync9 } from "fs";
116167
+ import { readFileSync as readFileSync10 } from "fs";
116079
116168
 
116080
116169
  // src/commands/ad/pmax-campaign-extensions.ts
116081
116170
  init_auth();
@@ -116705,7 +116794,7 @@ function detectDeprecatedPmaxVideoKeys(raw) {
116705
116794
  function tryLoadPmaxCreateConfig(configFile) {
116706
116795
  let raw;
116707
116796
  try {
116708
- raw = JSON.parse(readFileSync9(configFile, "utf8"));
116797
+ raw = JSON.parse(readFileSync10(configFile, "utf8"));
116709
116798
  } catch {
116710
116799
  return null;
116711
116800
  }
@@ -116713,7 +116802,7 @@ function tryLoadPmaxCreateConfig(configFile) {
116713
116802
  }
116714
116803
  function loadPmaxCreateConfigRaw(configFile) {
116715
116804
  try {
116716
- const raw = JSON.parse(readFileSync9(configFile, "utf8"));
116805
+ const raw = JSON.parse(readFileSync10(configFile, "utf8"));
116717
116806
  return stripMetaKeys(raw);
116718
116807
  } catch {
116719
116808
  return null;
@@ -116764,19 +116853,19 @@ function buildPmaxCreateUrl(googleApiUrl, accountId) {
116764
116853
  }
116765
116854
 
116766
116855
  // src/commands/ad/pmax-video-upload.ts
116767
- import { existsSync as existsSync3, readFileSync as readFileSync11 } from "fs";
116856
+ import { existsSync as existsSync3, readFileSync as readFileSync12 } from "fs";
116768
116857
  import { basename as basename5, dirname as dirname10, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
116769
116858
 
116770
116859
  // src/commands/ad/pmax-shared.ts
116771
116860
  init_auth();
116772
116861
  init_cli_json_snapshot();
116773
- import { readFileSync as readFileSync10 } from "fs";
116862
+ import { readFileSync as readFileSync11 } from "fs";
116774
116863
  import { dirname as dirname9, isAbsolute as isAbsolute4, resolve as resolve8 } from "path";
116775
116864
  var PMAX_MONEY_KEYS = /* @__PURE__ */ new Set(["budget", "targetCpa_BidingAmount"]);
116776
116865
  function loadPmaxJsonFile(configFile) {
116777
116866
  let raw;
116778
116867
  try {
116779
- raw = JSON.parse(readFileSync10(configFile, "utf8"));
116868
+ raw = JSON.parse(readFileSync11(configFile, "utf8"));
116780
116869
  } catch (e) {
116781
116870
  const msg = e instanceof Error ? e.message : String(e);
116782
116871
  console.error(`
@@ -116976,7 +117065,7 @@ function runPmaxVideoValidation(configFile, cfg) {
116976
117065
  return { errors, warnings };
116977
117066
  }
116978
117067
  try {
116979
- const buf = readFileSync11(abs);
117068
+ const buf = readFileSync12(abs);
116980
117069
  if (buf.length === 0) errors.push(`videoPath \u4E3A\u7A7A\u6587\u4EF6\uFF1A${abs}`);
116981
117070
  } catch (e) {
116982
117071
  const msg = e instanceof Error ? e.message : String(e);
@@ -117039,7 +117128,7 @@ async function uploadPmaxLocalVideo(opts) {
117039
117128
  const fileName = basename5(opts.absVideoPath);
117040
117129
  const title = (opts.title ?? fileName).trim() || fileName;
117041
117130
  const description = (opts.description ?? "Uploaded via siluzan-tso PMax video upload").trim() || "Uploaded via siluzan-tso";
117042
- const fileBuffer = readFileSync11(opts.absVideoPath);
117131
+ const fileBuffer = readFileSync12(opts.absVideoPath);
117043
117132
  const form = new FormData();
117044
117133
  form.append("media_account_id", opts.accountId.replace(/-/g, ""));
117045
117134
  form.append("title", title);
@@ -117097,7 +117186,7 @@ function validatePmaxVideoPathQuick(absPath) {
117097
117186
  return `\u89C6\u9891\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${absPath}`;
117098
117187
  }
117099
117188
  try {
117100
- if (readFileSync11(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
117189
+ if (readFileSync12(absPath).length === 0) return `\u89C6\u9891\u6587\u4EF6\u4E3A\u7A7A\uFF1A${absPath}`;
117101
117190
  } catch (e) {
117102
117191
  const msg = e instanceof Error ? e.message : String(e);
117103
117192
  return `\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u6587\u4EF6\uFF08${absPath}\uFF09\uFF1A${msg}`;
@@ -118500,7 +118589,7 @@ async function runAdCampaignValidate(opts) {
118500
118589
  }
118501
118590
 
118502
118591
  // src/commands/ad/pmax-image-convert.ts
118503
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync12, existsSync as existsSync4 } from "fs";
118592
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, readFileSync as readFileSync13, existsSync as existsSync4 } from "fs";
118504
118593
  import { resolve as resolve10, dirname as dirname11, basename as basename7, extname } from "path";
118505
118594
  var SPECS = [
118506
118595
  { kind: "marketing", width: 1200, height: 628, fit: "cover", suffix: "_marketing" },
@@ -118589,7 +118678,7 @@ async function runAdPmaxImageConvert(opts) {
118589
118678
  if (opts.updateConfig) {
118590
118679
  const configPath = resolve10(opts.updateConfig);
118591
118680
  try {
118592
- const raw = JSON.parse(readFileSync12(configPath, "utf8"));
118681
+ const raw = JSON.parse(readFileSync13(configPath, "utf8"));
118593
118682
  const imgPaths = raw["imagePaths"] ?? {};
118594
118683
  if (outputPaths.marketing) imgPaths["marketing"] = outputPaths.marketing;
118595
118684
  if (outputPaths.square) imgPaths["square"] = outputPaths.square;
@@ -118745,6 +118834,17 @@ async function runAdPmaxBrandGuidelinesEnable(opts) {
118745
118834
  }
118746
118835
 
118747
118836
  // src/commands/ad/_register.ts
118837
+ function parseOptionalMatchType(raw) {
118838
+ if (!raw?.trim()) return void 0;
118839
+ const t = raw.trim().toLowerCase();
118840
+ if (t === "broad") return "Broad";
118841
+ if (t === "phrase") return "Phrase";
118842
+ if (t === "exact") return "Exact";
118843
+ console.error(`
118844
+ \u274C --match-type \u65E0\u6548\uFF1A${raw}\uFF1B\u5408\u6CD5\u503C Broad | Phrase | Exact
118845
+ `);
118846
+ process.exit(1);
118847
+ }
118748
118848
  function register20(program2) {
118749
118849
  const adCmd = program2.command("ad").description(
118750
118850
  "Google \u5E7F\u544A\u7BA1\u7406\uFF08Google API \u5730\u5740\u4ECE TSO API \u81EA\u52A8\u63A8\u5BFC\uFF0C\u53EF\u901A\u8FC7 SILUZAN_GOOGLE_API \u73AF\u5883\u53D8\u91CF\u8986\u76D6\uFF09\n \u542B\uFF1A\u7CFB\u5217/\u7EC4/\u521B\u610F/\u5173\u952E\u8BCD\u7B49\u5E38\u89C4\u5E7F\u544A\u6295\u653E\u4E0E\u7BA1\u7406\u80FD\u529B"
@@ -119029,10 +119129,16 @@ function register20(program2) {
119029
119129
  });
119030
119130
  }
119031
119131
  );
119032
- adCmd.command("keyword-create").description("\u5411\u5E7F\u544A\u7EC4\u6DFB\u52A0\u641C\u7D22\u5173\u952E\u8BCD").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--adgroup-id <id>", "\u76EE\u6807\u5E7F\u544A\u7EC4 ID").requiredOption("--adgroup-name <name>", "\u76EE\u6807\u5E7F\u544A\u7EC4\u540D\u79F0").requiredOption("--campaign-id <id>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--campaign-name <name>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").requiredOption(
119132
+ adCmd.command("keyword-create").description("\u5411\u5E7F\u544A\u7EC4\u6DFB\u52A0\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u652F\u6301\u7A7A\u683C\u591A\u8BCD\uFF1B\u53EF\u7528 --keywords-file\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--adgroup-id <id>", "\u76EE\u6807\u5E7F\u544A\u7EC4 ID").requiredOption("--adgroup-name <name>", "\u76EE\u6807\u5E7F\u544A\u7EC4\u540D\u79F0").requiredOption("--campaign-id <id>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--campaign-name <name>", "\u6240\u5C5E\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").option(
119033
119133
  "--keywords <kw1,kw2,...>",
119034
- "\u5173\u952E\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694",
119035
- (v) => v.split(",").map((s) => s.trim())
119134
+ "\u5173\u952E\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u7A7A\u683C\u591A\u8BCD\u5408\u6CD5\uFF0C\u5982 how to make\uFF09\uFF1B\u53EF\u4E0E --keywords-file \u5E76\u7528",
119135
+ (v) => parseKeywordsCsv(v)
119136
+ ).option(
119137
+ "--keywords-file <path>",
119138
+ 'JSON \u5B57\u7B26\u4E32\u6570\u7EC4\u6587\u4EF6\uFF08\u63A8\u8350\u591A\u8BCD/\u5E26\u5F15\u53F7\u573A\u666F\uFF09\uFF0C\u5982 ["how to make","home cooking"]'
119139
+ ).option(
119140
+ "--match-type <Broad|Phrase|Exact>",
119141
+ '\u7EDF\u4E00\u5339\u914D\u7C7B\u578B\uFF08\u5199\u5165\u65F6\u7528\u6587\u6848 "\u2026" / [\u2026] \u5305\u88C5\uFF1B\u7701\u7565\u5219\u6309\u8BCD\u9762\u539F\u6837\u63D0\u4EA4\uFF09'
119036
119142
  ).option("--final-url <url>", "\u5173\u952E\u8BCD\u6700\u7EC8\u5230\u8FBE\u7F51\u5740\uFF08\u53EF\u9009\uFF09").option("-t, --token <token>", "Auth Token").option(
119037
119143
  "--json-out <path>",
119038
119144
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json",
@@ -119047,16 +119153,26 @@ function register20(program2) {
119047
119153
  campaignId: opts.campaignId,
119048
119154
  campaignName: opts.campaignName,
119049
119155
  keywords: opts.keywords,
119156
+ keywordsFile: opts.keywordsFile,
119157
+ matchType: parseOptionalMatchType(opts.matchType),
119050
119158
  finalUrl: opts.finalUrl,
119051
119159
  jsonOut: opts.jsonOut,
119052
119160
  verbose: opts.verbose
119053
119161
  });
119054
119162
  }
119055
119163
  );
119056
- adCmd.command("keyword-negative-create").description("\u6DFB\u52A0\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u5E7F\u544A\u7CFB\u5217\u5C42\u7EA7\uFF1B\u4F20 --adgroup-id \u5219\u4E3A\u5E7F\u544A\u7EC4\u5C42\u7EA7\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--campaign-id <id>", "\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--campaign-name <name>", "\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").requiredOption(
119164
+ adCmd.command("keyword-negative-create").description(
119165
+ "\u6DFB\u52A0\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u7CFB\u5217/\u7EC4\u7EA7\uFF1B\u652F\u6301\u7A7A\u683C\u591A\u8BCD\u77ED\u8BED\uFF0C\u5982 how to make\uFF1B\u53EF\u7528 --keywords-file\uFF09"
119166
+ ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--campaign-id <id>", "\u5E7F\u544A\u7CFB\u5217 ID").requiredOption("--campaign-name <name>", "\u5E7F\u544A\u7CFB\u5217\u540D\u79F0").option(
119057
119167
  "--keywords <kw1,kw2,...>",
119058
- "\u5426\u5B9A\u5173\u952E\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694",
119059
- (v) => v.split(",").map((s) => s.trim())
119168
+ "\u5426\u5B9A\u8BCD\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694\uFF08\u7A7A\u683C\u591A\u8BCD\u5408\u6CD5\uFF09\uFF1B\u53EF\u4E0E --keywords-file \u5E76\u7528",
119169
+ (v) => parseKeywordsCsv(v)
119170
+ ).option(
119171
+ "--keywords-file <path>",
119172
+ 'JSON \u5B57\u7B26\u4E32\u6570\u7EC4\u6587\u4EF6\uFF08\u63A8\u8350\u591A\u8BCD\u77ED\u8BED\uFF09\uFF0C\u5982 ["how to make","home cooking","second hand"]'
119173
+ ).option(
119174
+ "--match-type <Broad|Phrase|Exact>",
119175
+ "\u7EDF\u4E00\u5339\u914D\u7C7B\u578B\uFF08Broad \u65E0\u5305\u88C5 / Phrase \u52A0\u53CC\u5F15\u53F7 / Exact \u52A0\u65B9\u62EC\u53F7\uFF09"
119060
119176
  ).option("--adgroup-id <id>", "\u5E7F\u544A\u7EC4 ID\uFF08\u6307\u5B9A\u540E\u4E3A\u5E7F\u544A\u7EC4\u5C42\u7EA7\u5426\u5B9A\uFF09").option("--adgroup-name <name>", "\u5E7F\u544A\u7EC4\u540D\u79F0").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
119061
119177
  async (opts) => {
119062
119178
  await runAdNegativeKeywordCreate({
@@ -119067,6 +119183,8 @@ function register20(program2) {
119067
119183
  adgroupId: opts.adgroupId,
119068
119184
  adgroupName: opts.adgroupName,
119069
119185
  keywords: opts.keywords,
119186
+ keywordsFile: opts.keywordsFile,
119187
+ matchType: parseOptionalMatchType(opts.matchType),
119070
119188
  verbose: opts.verbose
119071
119189
  });
119072
119190
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.37-beta.3",
4
- "publishedAt": 1783922347102
3
+ "version": "1.1.37-beta.4",
4
+ "publishedAt": 1783934085806
5
5
  }
@@ -7,6 +7,7 @@
7
7
  "agentPitfalls": [
8
8
  "campaign-validate 推荐 --json-out ",
9
9
  "否词只写 campaign.NegativeKeywordsForBatchJob,禁止把 free/jobs/tutorial 等否词抄进 AdGroupsForBatchJob[].KeywordsForBatchJob",
10
+ "KeywordText 是 JSON 字符串数组,空格多词合法(如 how to make);禁止因多词省略否词或让用户去后台手补",
10
11
  "RSA 须同时写 DestinationUrl 与 Finalurl(后端必填 Finalurl;仅有 DestinationUrl 时 validate 可能仍通过)",
11
12
  "SITELINK 须写 AssetFieldType: SITELINK(与 typeV2 一致)",
12
13
  "提交前:ad campaign-validate → 用户确认 → ad campaign-create(勿对 validate 加 --json-out)"
@@ -65,7 +66,24 @@
65
66
  ],
66
67
 
67
68
  "NegativeKeywordsForBatchJob": [
68
- { "KeywordText": ["free", "cheap", "download"], "MatchTypeV2": "BROAD", "FinalURL": "" }
69
+ {
70
+ "KeywordText": [
71
+ "free",
72
+ "cheap",
73
+ "download",
74
+ "how to make",
75
+ "home cooking",
76
+ "second hand",
77
+ "near me"
78
+ ],
79
+ "MatchTypeV2": "BROAD",
80
+ "FinalURL": ""
81
+ },
82
+ {
83
+ "KeywordText": ["\"vegan recipe\"", "\"stock price\""],
84
+ "MatchTypeV2": "PHRASE",
85
+ "FinalURL": ""
86
+ }
69
87
  ],
70
88
 
71
89
  "ExtensionsForBatchJob": [
@@ -31,6 +31,8 @@ JSON 模板:同目录 [`campaign-create-template.json`](campaign-create-templa
31
31
  **禁止**:把方案 Markdown「否定词表」里的 `free` / `jobs` / `tutorial` 等抄进 `KeywordsForBatchJob`——那会**主动购买**这些流量。
32
32
  **正确**:否词只写 `NegativeKeywordsForBatchJob`;`ad campaign-validate` 会对误填给出 **warnings**(常见否词词根 / 与否词块重复)。
33
33
 
34
+ **多词短语(与 `keyword-negative-create` 同类误判)**:`KeywordText` 是 **JSON 字符串数组**,**空格多词完全合法**(如 `"how to make"`、`"home cooking"`)。PHRASE 写 `"\"vegan recipe\""` 或裸词 + `MatchTypeV2: "PHRASE"`(validate 会补引号)。**禁止**因「多词」就省略否词或让用户去 Google Ads 后台手补——那是 Agent 误判,不是 `campaign-create` 限制。
35
+
34
36
  投放方案 Markdown 中 §3.3 关键词矩阵 → `KeywordsForBatchJob`;§3.3 系列级否定词 / §3.4 账户级否定词 → `NegativeKeywordsForBatchJob`(账户级列表仍须在 Google 后台单独应用)。
35
37
 
36
38
  ### CLI 参数(勿混用)
@@ -184,7 +186,7 @@ siluzan-tso ad batch diff --batch-id <taskId> --config-file ./campaign.json --js
184
186
  | 字段 | 类型 | 说明 |
185
187
  | ----------------------------- | -------- | --------------------------------------------------------------------------------- |
186
188
  | `AdGroupsForBatchJob` | object[] | **至少 1 组**;见下 |
187
- | `NegativeKeywordsForBatchJob` | object[] | 系列级否词;元素:`{ KeywordText: string[], MatchTypeV2: "BROAD", FinalURL: "" }` |
189
+ | `NegativeKeywordsForBatchJob` | object[] | 系列级否词;元素:`{ KeywordText: string[], MatchTypeV2: "BROAD"\|"PHRASE"\|"EXACT", FinalURL: "" }`;**KeywordText 可含空格多词**(见模板示例) |
188
190
  | `ExtensionsForBatchJob` | object[] | 附加信息;`Properties` 须 **string→string**(勿用数组值)。SITELINK 见下表 |
189
191
 
190
192
  #### SITELINK(`ExtensionsForBatchJob[i]`,`typeV2` / `AssetFieldType` = `SITELINK`)
@@ -27,7 +27,8 @@
27
27
  2. **结论先行**:第一句给出可操作的判断(正常 / 有问题 / 原因是什么),不要让用户从表格里自己猜。
28
28
  3. **数据支撑**:引用**当次 CLI/脚本**的真实数字(消耗、CPA、余额、状态等);禁止编造、禁止示例 ID。
29
29
  4. **行动建议**:1~3 条可执行项,点名对象(系列名、账户、关键词)并引用数据。
30
- 5. **下一步**:一句预测或邀请(「要我帮您调整预算吗?」「还想看关键词明细吗?」)。
30
+ 5. **下一步**:一句预测或邀请(「要我帮您调整预算吗?」「还想看关键词明细吗?」)。
31
+ **注意**:客户端会另行展示可点击的 suggestion chips(完整用户话术)。本文件与各任务 reference **只要求回复里出现一句邀请**,**不要**在 skill 各任务文档里维护「可点 chips 全文清单」——那份清单由产品侧 chips 规则维护,skill 更新不应与之逐条对齐。
31
32
 
32
33
  长任务(预计 >30s):中间用**进度式**短句(「正在拉取系列数据…」「正在检查余额…」),不要长时间沉默后突然扔一大段表。
33
34
 
@@ -59,7 +60,8 @@
59
60
  | 帮我开个户 / 建广告 | W2 / W3 | 「没问题,我先告诉您需要准备哪些资料。」 |
60
61
  | 网站行不行 | P8 | 「我来给这个网站做个体检,看是否符合投放要求。」 |
61
62
 
62
- 完整话术矩阵见产品文档第十章;Skill 内记住**结构**即可,勿背诵全文。
63
+ 完整话术矩阵见产品文档第十章;Skill 内记住**结构**即可,勿背诵全文。
64
+ 可点击的「下一步」完整句子由客户端 suggestion chips 提供,**禁止**在本 skill 的 playbooks/workflows 里复制维护 chips 文案表(避免 skill 更新后与产品侧脱节)。
63
65
 
64
66
  ---
65
67
 
@@ -106,7 +108,8 @@
106
108
  | ---- | ---- |
107
109
  | `agent-conventions.md` | 怎么拉数、怎么读 outline、怎么自检、硬规范 |
108
110
  | `intent-routing.md` | 报告/分析消歧、选哪个 P/W |
109
- | `playbooks.md` / `workflows.md` | 步骤清单 |
110
- | **本文件** | **对用户怎么说** |
111
+ | `playbooks.md` / `workflows.md` | 步骤清单(执行),不维护 chips 全文 |
112
+ | **本文件** | **对用户怎么说**(结构 + 一句下一步邀请) |
113
+ | 客户端 suggestion chips | **可点的下一步完整话术**(产品侧维护,与本 skill 解耦) |
111
114
 
112
115
  数值纪律不变:所有金额、状态、ID 仍只来自当次 CLI/`--json-out` 脚本;本文件只约束**表述方式**,不放宽数据真实性要求。
@@ -268,26 +268,38 @@ siluzan-tso ad keyword-create \
268
268
  -a <accountId> \
269
269
  --adgroup-id <adGroupId> --adgroup-name <adGroupName> \
270
270
  --campaign-id <campaignId> --campaign-name <campaignName> \
271
- --keywords "词1,词2,词3" [--final-url <url>] \
271
+ --keywords "词1,词2,多词短语" [--final-url <url>] \
272
+ [--match-type Broad|Phrase|Exact] [--keywords-file ./kws.json] \
272
273
  [--json-out <path>]
273
274
  ```
274
275
 
275
- `--json-out` 落盘 `{ request: { adgroupId, count }, response: ... }`,批量脚本可直接据此核对成功量。
276
+ `--keywords` **支持空格多词**(逗号才是分隔符)。`--json-out` 落盘 `{ request: { adgroupId, count }, response: ... }`,批量脚本可直接据此核对成功量。
276
277
 
277
278
  ### 否定关键词
278
279
 
280
+ **支持空格多词短语**(如 `how to make`、`home cooking`)。匹配类型与 Web 一致:裸词=广泛,`"短语"`=词组,`[词]`=完全;也可用 `--match-type`。多词/带引号时推荐 `--keywords-file`,避免 shell 转义。
281
+
279
282
  ```bash
280
283
  # 添加(默认系列层级;传 --adgroup-id 则为组层级)
284
+ # 多词短语直接写空格,逗号分隔多条——CLI 支持,无需改后台手工补
281
285
  siluzan-tso ad keyword-negative-create \
282
286
  -a <accountId> \
283
287
  --campaign-id <campaignId> --campaign-name <campaignName> \
284
- --keywords "词1,词2" \
285
- [--adgroup-id <id> --adgroup-name <name>]
288
+ --keywords "how to make,home cooking,second hand,near me"
289
+
290
+ # 统一词组匹配 + JSON 文件(推荐)
291
+ # negatives.json → ["how to make","home cooking","vegan recipe"]
292
+ siluzan-tso ad keyword-negative-create \
293
+ -a <accountId> \
294
+ --campaign-id <campaignId> --campaign-name <campaignName> \
295
+ --keywords-file ./negatives.json --match-type Phrase
286
296
 
287
297
  # 删除(先用 ad keywords --negative --json-out ./snap 获取 id)
288
298
  siluzan-tso ad keyword-negative-delete -a <accountId> --id <negativeKeywordId>
289
299
  ```
290
300
 
301
+ > **禁止**因「多词」就让用户去 Google Ads 后台手工补否词——那是 Agent 误判;用上面命令即可。
302
+
291
303
  ---
292
304
 
293
305
  ## ad batch — 异步批量创建记录
@@ -405,6 +417,8 @@ BatchJob 中 `ExtensionsForBatchJob`(尤其 **Sitelink**)失败率较高,
405
417
 
406
418
  **关键词匹配:** 写在 `KeywordsForBatchJob` 块内;同一块同匹配类型,`MatchTypeV2` 与 `KeywordText` 词面格式必须对齐(`PHRASE` 用 `"词"`、`EXACT` 用 `[词]`、`BROAD` 直写)。
407
419
 
420
+ **多词短语:** `KeywordText` / 否词 `NegativeKeywordsForBatchJob[].KeywordText` 是 **JSON 字符串数组**,**空格多词合法**(如 `"how to make"`)。与 `keyword-negative-create` 不同,这里**没有**逗号 CLI 解析问题——**禁止**因「多词」省略否词或让用户去后台手补。
421
+
408
422
  广告组/关键词/创意的分步创建仍用 `adgroup-create`、`keyword-create`、`ad-create`。
409
423
 
410
424
  ---
@@ -409,8 +409,8 @@ Broad Match + 受众信号 = 伪装成广泛定向的精准投放。
409
409
  | ------------ | ------------------------------------------------------------ | ----------------------------------------------------------------------- |
410
410
  | 关键词调研 | `keyword -k "词" [--url]` | 结果按意图分层后再添加 |
411
411
  | 添加关键词 | `ad keyword-create --keywords "词1,词2"` | 格式:`词→BROAD`、`"词"→PHRASE`、`[词]→EXACT` |
412
- | 添加否定词 | `ad keyword-negative-create` | 每周从 `ad search-terms` 挖掘 |
413
- | 创建广告系列 | `ad campaign-create --config-file`(先 `campaign-validate`) | 按本文第三章架构设计系列结构;匹配块见 `google-ads-keyword-taxonomy.md` |
412
+ | 添加否定词 | `ad keyword-negative-create` | **支持空格多词**(`how to make`);多词用 `--keywords` 或 `--keywords-file`,**禁止**因多词改让用户后台手补 |
413
+ | 创建广告系列 | `ad campaign-create --config-file`(先 `campaign-validate`) | `KeywordText` 为 JSON 数组,**空格多词合法**;否词写 `NegativeKeywordsForBatchJob`,勿因多词省略 |
414
414
  | 搜索词分析 | `ad search-terms` | 每周导出,按第八章流程处理 |
415
415
  | 竞品监控 | `ad auction-insights`(如有) | 按第九章分析竞争态势 |
416
416
  | 出价调整 | 广告系列级 Smart Bidding 设置 | 新账户用 Manual/ECPC → 有数据后切 tCPA/tROAS |
@@ -9,7 +9,7 @@ $ErrorActionPreference = 'Stop'
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  $PKG_NAME = 'siluzan-tso-cli'
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- $PKG_VERSION = '1.1.37-beta.3'
12
+ $PKG_VERSION = '1.1.37-beta.4'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
15
  $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  readonly PKG_NAME="siluzan-tso-cli"
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- readonly PKG_VERSION="1.1.37-beta.3"
12
+ readonly PKG_VERSION="1.1.37-beta.4"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.37-beta.3",
3
+ "version": "1.1.37-beta.4",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",