siluzan-tso-cli 1.1.64-beta.1 → 1.1.64-beta.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.
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.64-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.64-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -5560,6 +5560,17 @@ function resolveAdAccountId(account, label2 = "-a, --account") {
5560
5560
  function withResolvedAdAccount(opts) {
5561
5561
  return { ...opts, account: resolveAdAccountId(opts.account) };
5562
5562
  }
5563
+ function negativeKeywordSlimListUrl(googleApiUrl, accountId, query) {
5564
+ const params = new URLSearchParams();
5565
+ if (query?.appliedLevel) params.set("appliedlevel", query.appliedLevel);
5566
+ if (query?.readDeleted) params.set("readDeleted", "true");
5567
+ if (query?.excludePaused) params.set("excludePaused", "true");
5568
+ const text3 = query?.keywordText?.trim();
5569
+ if (text3) params.set("keywordtext", text3);
5570
+ if (query?.matchType) params.set("matchtype", query.matchType);
5571
+ const qs = params.toString();
5572
+ return `${googleApiUrl}/negativekeywordmanagement/v2/list/${accountId}/slim${qs ? `?${qs}` : ""}`;
5573
+ }
5563
5574
  async function findItemInList(listUrl, config, id, verbose) {
5564
5575
  const res = await apiFetch2(listUrl, config, {}, verbose);
5565
5576
  const item = (res.data ?? []).find((x) => x["id"] === id);
@@ -6398,6 +6409,20 @@ function coerceToRowArray(value) {
6398
6409
  }
6399
6410
  return [];
6400
6411
  }
6412
+ function dedupeRowsById(items3) {
6413
+ const seen = /* @__PURE__ */ new Set();
6414
+ const out = [];
6415
+ for (const item of items3) {
6416
+ const rec = asRecord(item);
6417
+ const id = rec?.id == null ? "" : String(rec.id);
6418
+ if (id) {
6419
+ if (seen.has(id)) continue;
6420
+ seen.add(id);
6421
+ }
6422
+ out.push(item);
6423
+ }
6424
+ return out;
6425
+ }
6401
6426
  function asRecord(value) {
6402
6427
  if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
6403
6428
  return value;
@@ -6432,7 +6457,7 @@ function wrapGoogleAnalysisSnapshotPayload(section, payload) {
6432
6457
  const bag = asRecord(payload);
6433
6458
  if (bag) {
6434
6459
  const r = readGatewayList(bag);
6435
- items3 = r.items;
6460
+ items3 = section === "negative-keywords" ? dedupeRowsById(r.items) : r.items;
6436
6461
  meta = r.meta;
6437
6462
  }
6438
6463
  } else if (ROOT_ARRAY_SECTIONS.has(section)) {
@@ -8120,9 +8145,10 @@ var init_sections = __esm({
8120
8145
  },
8121
8146
  {
8122
8147
  name: "negative-keywords",
8123
- description: "\u5F53\u524D\u8D26\u6237\u5426\u8BCD negativekeywordmanagement/v2/list\uFF08\u7CFB\u5217/\u5E7F\u544A\u7EC4/\u8D26\u6237\u4E09\u7EA7\uFF1B\u540D\u5355\u4E0D\u6309\u6D88\u8017\u8FC7\u6EE4\uFF0C0 \u6761\u4E5F\u843D\u76D8\uFF09",
8148
+ description: "\u5F53\u524D\u8D26\u6237\u5426\u8BCD negativekeywordmanagement/v2/list/{id}/slim\uFF08id/\u8BCD\u9762/\u5339\u914D/\u7EA7\u522B/\u7CFB\u5217 id/\u5E7F\u544A\u7EC4 id\uFF1B\u4E0D\u6309\u6D88\u8017\u548C\u65E5\u671F\u8FC7\u6EE4\uFF0C0 \u6761\u4E5F\u843D\u76D8\uFF09",
8149
+ // 接口忽略日期。保留 range 是为了快照文件名仍带报告区间,避免和上一次落盘互相覆盖。
8124
8150
  dateMode: "range",
8125
- path: (id) => `/negativekeywordmanagement/v2/list/${id}`
8151
+ path: (id) => `/negativekeywordmanagement/v2/list/${id}/slim`
8126
8152
  },
8127
8153
  {
8128
8154
  name: "search-terms",
@@ -104342,6 +104368,8 @@ function mapNegativeKeywordRows(rows) {
104342
104368
  if (id && seen.has(id)) continue;
104343
104369
  if (id) seen.add(id);
104344
104370
  const level = String(row.negativeLevel ?? "").trim();
104371
+ const campaignId = String(row.campaignId ?? "").trim();
104372
+ const adGroupId = String(row.adGroupId ?? "").trim();
104345
104373
  const campaignName = String(row.campaignName ?? "").trim();
104346
104374
  const adGroupName = String(row.adGroupName ?? "").trim();
104347
104375
  mapped.push({
@@ -104349,6 +104377,8 @@ function mapNegativeKeywordRows(rows) {
104349
104377
  matchType: String(row.matchTypeV2 ?? row.matchType ?? "\u2014"),
104350
104378
  level: level || "\u2014",
104351
104379
  levelZh: NEGATIVE_LEVEL_ZH[level] ?? (level || "\u2014"),
104380
+ campaignId: campaignId || "\u2014",
104381
+ adGroupId: adGroupId || "\u2014",
104352
104382
  campaignName: campaignName || "\u2014",
104353
104383
  adGroupName: adGroupName || "\u2014"
104354
104384
  });
@@ -115257,6 +115287,7 @@ function resolveKeywordTexts(opts) {
115257
115287
  }
115258
115288
  function firstKeywordTextFromRecord(k) {
115259
115289
  const kt = k["keywordText"];
115290
+ if (typeof kt === "string") return kt;
115260
115291
  if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
115261
115292
  return kt[0];
115262
115293
  }
@@ -115266,9 +115297,27 @@ function firstKeywordTextFromRecord(k) {
115266
115297
  }
115267
115298
  return "";
115268
115299
  }
115300
+ function negativeKeywordWriteBody(row) {
115301
+ const text3 = firstKeywordTextFromRecord(row);
115302
+ const body = {
115303
+ id: row["id"],
115304
+ keywordText: text3 ? [text3] : [],
115305
+ matchTypeV2: row["matchTypeV2"],
115306
+ negativeLevel: row["negativeLevel"]
115307
+ };
115308
+ const campaignId = row["campaignId"];
115309
+ const adGroupId = row["adGroupId"];
115310
+ if (typeof campaignId === "string" && campaignId) body["campaignId"] = campaignId;
115311
+ if (typeof adGroupId === "string" && adGroupId) body["adGroupId"] = adGroupId;
115312
+ return body;
115313
+ }
115269
115314
  function toNegativeKeywordAppliedLevelQuery(level) {
115270
115315
  return level === "AdGroup" ? "Ad group" : level;
115271
115316
  }
115317
+ function filterNegativeKeywordsByMatchType(items3, matchType) {
115318
+ const want = matchType.toLowerCase();
115319
+ return items3.filter((item) => String(item["matchTypeV2"] ?? "").toLowerCase() === want);
115320
+ }
115272
115321
  function keywordRowToYuan(item) {
115273
115322
  const { maxCPC: rawMaxCpc, ...rest } = item;
115274
115323
  if (rawMaxCpc == null) return item;
@@ -115278,14 +115327,20 @@ async function runAdKeywords(opts) {
115278
115327
  opts = withResolvedAdAccount(opts);
115279
115328
  const config = loadConfig(opts.token);
115280
115329
  const googleApiUrl = requireGoogleApi(config);
115281
- const params = new URLSearchParams();
115282
- params.set("startDate", toGoogleDate(opts.startDate, -30));
115283
- params.set("endDate", toGoogleDate(opts.endDate, 0));
115284
- if (opts.negative && opts.level) {
115285
- params.set("appliedlevel", toNegativeKeywordAppliedLevelQuery(opts.level));
115330
+ let url;
115331
+ if (opts.negative) {
115332
+ url = negativeKeywordSlimListUrl(googleApiUrl, opts.account, {
115333
+ appliedLevel: opts.level ? toNegativeKeywordAppliedLevelQuery(opts.level) : void 0,
115334
+ keywordText: opts.keywordText,
115335
+ excludePaused: opts.excludePaused,
115336
+ readDeleted: opts.readDeleted
115337
+ });
115338
+ } else {
115339
+ const params = new URLSearchParams();
115340
+ params.set("startDate", toGoogleDate(opts.startDate, -30));
115341
+ params.set("endDate", toGoogleDate(opts.endDate, 0));
115342
+ url = `${googleApiUrl}/keywordmanagement/v2/list/${opts.account}?${params}`;
115286
115343
  }
115287
- const endpoint = opts.negative ? "negativekeywordmanagement" : "keywordmanagement";
115288
- const url = `${googleApiUrl}/${endpoint}/v2/list/${opts.account}?${params}`;
115289
115344
  let data;
115290
115345
  try {
115291
115346
  data = await apiFetch2(url, config, {}, opts.verbose);
@@ -115297,12 +115352,15 @@ async function runAdKeywords(opts) {
115297
115352
  }
115298
115353
  const rawItems = data.data ?? [];
115299
115354
  const seen = /* @__PURE__ */ new Set();
115300
- const items3 = rawItems.filter((item) => {
115355
+ let items3 = rawItems.filter((item) => {
115301
115356
  const key = item["id"];
115302
115357
  if (key !== void 0 && seen.has(key)) return false;
115303
115358
  if (key !== void 0) seen.add(key);
115304
115359
  return true;
115305
115360
  });
115361
+ if (opts.negative && opts.matchType) {
115362
+ items3 = filterNegativeKeywordsByMatchType(items3, opts.matchType);
115363
+ }
115306
115364
  const label2 = opts.negative ? "\u5426\u5B9A\u5173\u952E\u8BCD" : "\u5173\u952E\u8BCD";
115307
115365
  const n = items3.length;
115308
115366
  const displayItems = opts.negative ? items3 : items3.map((row) => keywordRowToYuan(row));
@@ -115310,7 +115368,7 @@ async function runAdKeywords(opts) {
115310
115368
  wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: displayItems })
115311
115369
  );
115312
115370
  if (await emitCliJsonOrSnapshot(opts, {
115313
- section: `ad-keywords-${opts.account}-${opts.negative ? "negative" : "positive"}`,
115371
+ section: `ad-keywords-${opts.negative ? "negative" : "positive"}`,
115314
115372
  commandLabel: "ad keywords",
115315
115373
  commandHint: opts.account,
115316
115374
  payload: kwPayload,
@@ -115332,7 +115390,11 @@ ${label2}\uFF08\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672
115332
115390
  const kwText = Array.isArray(item["keywordText"]) ? item["keywordText"].join(", ") : String(item["text"] ?? item["keywordText"] ?? item["id"] ?? "\u2014");
115333
115391
  const matchType = item["matchTypeV2"] ?? "\u2014";
115334
115392
  const level = item["negativeLevel"] ?? "\u2014";
115335
- console.log(` [${matchType}] ${kwText} id: ${String(item["id"] ?? "")} \u7EA7\u522B: ${level}`);
115393
+ const campaignId = item["campaignId"] ? ` \u7CFB\u5217: ${String(item["campaignId"])}` : "";
115394
+ const adGroupId = item["adGroupId"] ? ` \u5E7F\u544A\u7EC4: ${String(item["adGroupId"])}` : "";
115395
+ console.log(
115396
+ ` [${matchType}] ${kwText} id: ${String(item["id"] ?? "")} \u7EA7\u522B: ${level}${campaignId}${adGroupId}`
115397
+ );
115336
115398
  });
115337
115399
  } else {
115338
115400
  const kwW = Math.min(
@@ -115516,10 +115578,7 @@ async function runAdNegativeKeywordCreate(opts) {
115516
115578
  `);
115517
115579
  process.exit(1);
115518
115580
  }
115519
- const dateParams = new URLSearchParams();
115520
- dateParams.set("startDate", toGoogleDate(void 0, -30));
115521
- dateParams.set("endDate", toGoogleDate(void 0, 0));
115522
- const listUrl = `${googleApiUrl}/negativekeywordmanagement/v2/list/${opts.account}?${dateParams}`;
115581
+ const listUrl = negativeKeywordSlimListUrl(googleApiUrl, opts.account);
115523
115582
  const readList = async () => {
115524
115583
  try {
115525
115584
  const listed = await apiFetch2(
@@ -115793,10 +115852,7 @@ async function runAdNegativeKeywordEdit(opts) {
115793
115852
  opts = withResolvedAdAccount(opts);
115794
115853
  const config = loadConfig(opts.token);
115795
115854
  const googleApiUrl = requireGoogleApi(config);
115796
- const dateParams = new URLSearchParams();
115797
- dateParams.set("startDate", toGoogleDate(opts.startDate, -30));
115798
- dateParams.set("endDate", toGoogleDate(opts.endDate, 0));
115799
- const listUrl = `${googleApiUrl}/negativekeywordmanagement/v2/list/${opts.account}?${dateParams}`;
115855
+ const listUrl = negativeKeywordSlimListUrl(googleApiUrl, opts.account);
115800
115856
  let keyword;
115801
115857
  try {
115802
115858
  keyword = await findItemInList(listUrl, config, opts.id, opts.verbose);
@@ -115806,7 +115862,7 @@ async function runAdNegativeKeywordEdit(opts) {
115806
115862
  `);
115807
115863
  process.exit(1);
115808
115864
  }
115809
- const body = { ...keyword };
115865
+ const body = negativeKeywordWriteBody(keyword);
115810
115866
  if (opts.matchType !== void 0) {
115811
115867
  const base2 = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
115812
115868
  if (!String(base2).trim()) {
@@ -115849,10 +115905,7 @@ async function runAdNegativeKeywordDelete(opts) {
115849
115905
  opts = withResolvedAdAccount(opts);
115850
115906
  const config = loadConfig(opts.token);
115851
115907
  const googleApiUrl = requireGoogleApi(config);
115852
- const dateParams = new URLSearchParams();
115853
- dateParams.set("startDate", toGoogleDate(opts.startDate, -30));
115854
- dateParams.set("endDate", toGoogleDate(opts.endDate, 0));
115855
- const listUrl = `${googleApiUrl}/negativekeywordmanagement/v2/list/${opts.account}?${dateParams}`;
115908
+ const listUrl = negativeKeywordSlimListUrl(googleApiUrl, opts.account);
115856
115909
  let keyword;
115857
115910
  try {
115858
115911
  keyword = await findItemInList(listUrl, config, opts.id, opts.verbose);
@@ -115864,7 +115917,10 @@ async function runAdNegativeKeywordDelete(opts) {
115864
115917
  }
115865
115918
  const url = `${googleApiUrl}/negativekeywordmanagement/negativekeyword/${opts.account}/${opts.id}`;
115866
115919
  try {
115867
- await apiDeleteRaw(url, config, { body: JSON.stringify(keyword), verbose: opts.verbose });
115920
+ await apiDeleteRaw(url, config, {
115921
+ body: JSON.stringify(negativeKeywordWriteBody(keyword)),
115922
+ verbose: opts.verbose
115923
+ });
115868
115924
  } catch (err) {
115869
115925
  console.error(`
115870
115926
  \u274C \u5220\u9664\u5426\u5B9A\u5173\u952E\u8BCD\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -121142,7 +121198,7 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
121142
121198
  ),
121143
121199
  fetchList(
121144
121200
  config,
121145
- `${googleApiUrl}/negativekeywordmanagement/v2/list/${accountId}?${qs}`,
121201
+ negativeKeywordSlimListUrl(googleApiUrl, accountId),
121146
121202
  verbose
121147
121203
  ),
121148
121204
  fetchList(
@@ -127001,11 +127057,18 @@ function register21(program2) {
127001
127057
  }
127002
127058
  );
127003
127059
  adCmd.command("keywords").alias("keyword-list").description(
127004
- "\u67E5\u8BE2\u5173\u952E\u8BCD\u5217\u8868\uFF08--negative \u67E5\u8BE2\u5426\u5B9A\u5173\u952E\u8BCD\uFF0C\u53EF\u7528 --level \u6309\u7CFB\u5217/\u7EC4/\u8D26\u6237\u8FC7\u6EE4\uFF1B\u65E5\u671F\u9ED8\u8BA4\u8FD1 30 \u5929\uFF1B\u522B\u540D keyword-list\uFF09"
127005
- ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option("--negative", "\u67E5\u8BE2\u5426\u5B9A\u5173\u952E\u8BCD", false).option(
127060
+ "\u67E5\u8BE2\u5173\u952E\u8BCD\u5217\u8868\uFF08--negative \u8D70\u5426\u8BCD\u7CBE\u7B80\u5217\u8868\uFF0C\u53EF\u7528 --level/--keyword/--match-type/--exclude-paused/--read-deleted\uFF1B\u6B63\u5411\u5173\u952E\u8BCD\u65E5\u671F\u9ED8\u8BA4\u8FD1 30 \u5929\uFF1B\u522B\u540D keyword-list\uFF09"
127061
+ ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").option("--negative", "\u67E5\u8BE2\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u7CBE\u7B80\u5217\u8868\uFF0C\u4E0D\u6309\u65E5\u671F\u8FC7\u6EE4\uFF09", false).option(
127006
127062
  "--level <level>",
127007
127063
  "\u5426\u5B9A\u5173\u952E\u8BCD\u5C42\u7EA7\u8FC7\u6EE4\uFF1AAccount | Campaign | AdGroup\uFF08\u4EC5\u914D\u5408 --negative\uFF1B\u4E0D\u4F20\u5219\u4E09\u7EA7\u90FD\u663E\u793A\uFF09"
127008
- ).option("--start <date>", "\u5F00\u59CB\u65E5\u671F\uFF08YYYY-MM-DD\uFF0C\u9ED8\u8BA4 30 \u5929\u524D\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F\uFF08YYYY-MM-DD\uFF0C\u9ED8\u8BA4\u4ECA\u5929\uFF09").option("-t, --token <token>", "Auth Token").option(
127064
+ ).option("--keyword <text>", "\u5426\u5B9A\u5173\u952E\u8BCD\u6587\u672C\uFF0C\u5305\u542B\u5373\u547D\u4E2D\uFF08\u4EC5\u914D\u5408 --negative\uFF0C\u4E0D\u533A\u5206\u5927\u5C0F\u5199\uFF09").option(
127065
+ "--match-type <type>",
127066
+ "\u5426\u5B9A\u5173\u952E\u8BCD\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u4EC5\u914D\u5408 --negative\uFF1B\u6309\u8FD4\u56DE\u7684 matchTypeV2 \u672C\u5730\u8FC7\u6EE4\uFF09"
127067
+ ).option("--exclude-paused", "\u53BB\u6389\u6682\u505C\u7684\u5426\u8BCD\uFF0C\u4EE5\u53CA\u6682\u505C\u7CFB\u5217\u3001\u6682\u505C\u5E7F\u544A\u7EC4\u4E0A\u7684\u5426\u8BCD\uFF08\u4EC5\u914D\u5408 --negative\uFF09", false).option(
127068
+ "--read-deleted",
127069
+ "\u5305\u542B\u5DF2\u79FB\u9664\u7684\u7CFB\u5217\u3001\u5E7F\u544A\u7EC4\uFF08\u5426\u8BCD\u672C\u8EAB\u88AB\u79FB\u9664\u7684\u4ECD\u4E0D\u8FD4\u56DE\uFF1B\u4EC5\u914D\u5408 --negative\uFF09",
127070
+ false
127071
+ ).option("--start <date>", "\u5F00\u59CB\u65E5\u671F\uFF08YYYY-MM-DD\uFF0C\u9ED8\u8BA4 30 \u5929\u524D\uFF1B\u5426\u8BCD\u7CBE\u7B80\u5217\u8868\u5FFD\u7565\u65E5\u671F\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F\uFF08YYYY-MM-DD\uFF0C\u9ED8\u8BA4\u4ECA\u5929\uFF1B\u5426\u8BCD\u7CBE\u7B80\u5217\u8868\u5FFD\u7565\u65E5\u671F\uFF09").option("-t, --token <token>", "Auth Token").option(
127009
127072
  "--json-out <path>",
127010
127073
  "\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
127011
127074
  void 0
@@ -127015,11 +127078,26 @@ function register21(program2) {
127015
127078
  console.error("\n\u274C --level \u53EA\u63A5\u53D7 Account | Campaign | AdGroup\n");
127016
127079
  process.exit(1);
127017
127080
  }
127081
+ const negativeOnly = opts.level || opts.keyword || opts.matchType || opts.excludePaused || opts.readDeleted;
127082
+ if (negativeOnly && !opts.negative) {
127083
+ console.error(
127084
+ "\n\u274C --level / --keyword / --match-type / --exclude-paused / --read-deleted \u53EA\u7528\u4E8E\u5426\u5B9A\u5173\u952E\u8BCD\uFF0C\u8BF7\u52A0\u4E0A --negative\n"
127085
+ );
127086
+ process.exit(1);
127087
+ }
127088
+ if (opts.matchType && !["Broad", "Phrase", "Exact"].includes(opts.matchType)) {
127089
+ console.error("\n\u274C --match-type \u53EA\u63A5\u53D7 Broad | Phrase | Exact\n");
127090
+ process.exit(1);
127091
+ }
127018
127092
  await runAdKeywords({
127019
127093
  token: opts.token,
127020
127094
  account: opts.account,
127021
127095
  negative: opts.negative,
127022
127096
  level: opts.level,
127097
+ keywordText: opts.keyword,
127098
+ matchType: opts.matchType,
127099
+ excludePaused: opts.excludePaused,
127100
+ readDeleted: opts.readDeleted,
127023
127101
  startDate: opts.start,
127024
127102
  endDate: opts.end,
127025
127103
  jsonOut: opts.jsonOut,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.64-beta.1",
4
- "publishedAt": 1793609207587
3
+ "version": "1.1.64-beta.2",
4
+ "publishedAt": 1793612804179
5
5
  }
@@ -78,7 +78,7 @@ CLI 出口的所有 JSON / 表格金额已统一为**元**,关键字段:
78
78
 
79
79
  ## Google 账户分析:`google-analysis` 命令
80
80
 
81
- > **重要**:`google-analysis` 是统一入口,所有维度都通过 `--sections` 选取。关键词维度用 `--sections keywords`,**不要**用 `ad keywords` 代替。否词名单用 `--sections negative-keywords`,**不要**另跑 `ad keywords --negative`。涉及「今天/当天」消耗的口径见本文顶部「数据时效性」表。
81
+ > **重要**:`google-analysis` 是统一入口,所有维度都通过 `--sections` 选取。关键词维度用 `--sections keywords`,**不要**用 `ad keywords` 代替。否词名单用 `--sections negative-keywords`,**不要**另跑 `ad keywords --negative`。这份否词是精简列表:`keywordText`、`matchTypeV2`、`negativeLevel`、`campaignId`、`adGroupId`,没有系列名,也不按 `--start` / `--end` 过滤。涉及「今天/当天」消耗的口径见本文顶部「数据时效性」表。
82
82
 
83
83
  ```bash
84
84
  # 单维度
@@ -186,10 +186,10 @@ siluzan-tso ad ad-delete -a <accountId> --id <adId>
186
186
  ### 查询
187
187
 
188
188
  ```bash
189
- siluzan-tso ad keywords -a <accountId> [--negative] [--start/--end <date>] [--json-out ./snap]
189
+ siluzan-tso ad keywords -a <accountId> [--start/--end <date>] [--json-out ./snap]
190
190
  ```
191
191
 
192
- 带日期时行上有 `conversionsValue`(账户币种的转化价值,不是 `conversions` 次数)。`--json-out` 里直接读这个字段;终端表「转化价值」列在字段缺失时为「—」。否定关键词名单没有这个指标。出价读 `maxCPCYuan`(元)。本列表按账户和日期缓存约 60 秒,**没有** `--fresh`,也**没有** `--id`。`keyword-edit` 回读报「列表仍是」时,等约 60 秒再看 `maxCPCYuan`,不要立刻重跑编辑。
192
+ 带日期时行上有 `conversionsValue`(账户币种的转化价值,不是 `conversions` 次数)。`--json-out` 里直接读这个字段;终端表「转化价值」列在字段缺失时为「—」。出价读 `maxCPCYuan`(元)。本列表按账户和日期缓存约 60 秒,**没有** `--fresh`,也**没有** `--id`。`keyword-edit` 回读报「列表仍是」时,等约 60 秒再看 `maxCPCYuan`,不要立刻重跑编辑。否定关键词不要用这条带日期的列表,见下方。
193
193
 
194
194
  ### 添加关键词
195
195
 
@@ -207,6 +207,18 @@ siluzan-tso ad keyword-create \
207
207
 
208
208
  ### 否定关键词
209
209
 
210
+ 查询走精简列表 `GET /negativekeywordmanagement/v2/list/{id}/slim`。一条对应一条否词,字段只有 `id`、`keywordText`(字符串)、`matchTypeV2`、`negativeLevel`、`campaignId`、`adGroupId`。没有分页、没有总条数、没有系列名、没有消耗。不传参数就是三级都查,含暂停,不含已移除的系列和广告组。`--start` / `--end` 会被忽略。
211
+
212
+ ```bash
213
+ siluzan-tso ad keywords -a <accountId> --negative [--level Account|Campaign|AdGroup] \
214
+ [--keyword <文本>] [--match-type Broad|Phrase|Exact] [--exclude-paused] [--read-deleted] \
215
+ [--json-out ./snap]
216
+ ```
217
+
218
+ `--exclude-paused` 去掉暂停的否词,以及暂停系列、暂停广告组上的否词。账户级没有暂停,不受影响。`--read-deleted` 把已移除的系列、广告组也算进来;否词本身被移除的仍然不返回。两个开关可以一起用。`--keyword` 是包含匹配,不区分大小写。`--match-type` 按返回的 `matchTypeV2` 在本地过滤,不要自己给这个接口拼 `matchtype=BROAD`:网关用旧枚举去比,会得到空列表。
219
+
220
+ 改、删先用这条精简记录组请求体:`keywordText` 要写成单元素数组,再带 `id`、`matchTypeV2`、`negativeLevel`,系列级加 `campaignId`,广告组级再加 `adGroupId`。
221
+
210
222
  **支持空格多词短语**(如 `how to make`、`home cooking`)。匹配类型与 Web 一致:裸词=广泛,`"短语"`=词组,`[词]`=完全;也可用 `--match-type`。多词/带引号时推荐 `--keywords-file`,避免 shell 转义。
211
223
 
212
224
  **三级层级:** Campaign(系列)/ AdGroup(组)/ **Account(账户级,Sammamish 2026-08 新增)**。不传 `--level` 时按是否传 `--adgroup-id` 自动判断 Campaign / AdGroup(兼容旧用法);**账户级必须显式传 `--level Account`**,且不能传 `--campaign-id` / `--adgroup-id`。账户级否词一次生效于整个账户下所有系列(对应 Google Ads「Account Settings → Negative keywords」),**无需再去 Google 后台手工应用**——这是本次新增前的旧限制,现已由接口直接支持。
@@ -74,7 +74,7 @@ siluzan-tso google-analysis render \
74
74
  | 6 | 地域分布(国家/地区 消耗占比) | `google-analysis --sections geographic` |
75
75
  | 7 | 受众分布(意向/兴趣/再营销/客户匹配等列表表现) | `google-analysis --sections audience`(默认不传 `--audience-type`,系统+自定义一起拉) |
76
76
  | 8 | 关键词表现(词/消耗/CTR/CPC 排行) | `google-analysis --sections keywords`(这个命令获取的关键词数据会来自多个系列,不能合并去重) |
77
- | 9 | 否词(当前账户已有否定关键词:词面 / 匹配 / 级别 / 系列 / 广告组) | `google-analysis --sections negative-keywords`(0 条也要落盘;名单不按本周期消耗过滤) |
77
+ | 9 | 否词(当前账户已有否定关键词:词面 / 匹配 / 级别 / 系列 id / 广告组 id) | `google-analysis --sections negative-keywords`(精简列表;0 条也要落盘;不按本周期消耗过滤) |
78
78
  | 10 | 优化建议(根据以上数据给出可执行改进建议) | 不额外拉数,基于已有数据撰写 |
79
79
 
80
80
  **在执行任何数据拉取之前**,先向用户展示以下可选维度,询问是否需要追加:
@@ -143,9 +143,9 @@ siluzan-tso google-analysis -a <id> --start <s> --end <e> --json-out ./snap-goog
143
143
 
144
144
  ## 否词(必写)
145
145
 
146
- `negative-keywords-*.json` → `items[]`,合并后是 `tables.negativeKeywords[]`。这是**当前账户已经存在的否定关键词**(系列 / 广告组 / 账户三级),不是本周期的消耗或搜索词。
146
+ `negative-keywords-*.json` → `items[]`,合并后是 `tables.negativeKeywords[]`。这是**当前账户已经存在的否定关键词**(系列 / 广告组 / 账户三级),来自精简列表,不是本周期的消耗或搜索词。
147
147
 
148
- - **呈现**:词面、匹配方式、级别(`levelZh`:账户 / 系列 / 广告组)、广告系列、广告组。0 条时表格写「当前账户否词 0 条」。
148
+ - **呈现**:词面、匹配方式、级别(`levelZh`:账户 / 系列 / 广告组)、`campaignId`、`adGroupId`。精简列表没有系列名和广告组名。账户级没有 `campaignId`,只有广告组级才有 `adGroupId`。旧快照若还有 `campaignName` / `adGroupName` 可以照用。0 条时表格写「当前账户否词 0 条」。
149
149
  - **分析** → `narrative.sections.negativeKeywords.{analysis,suggestions}`(≥2 / ≥1):
150
150
  - 先写总条数,再按级别拆开。有词就点名几条;0 条就写「当前否词 0 条」。
151
151
  - 其他章节如果建议加否词、出否词清单,必须引用这张表的条数。禁止在没拉到名单时写「账户没有否词」或只在建议里提否词。
@@ -74,7 +74,7 @@ siluzan-tso google-analysis render \
74
74
  | 6 | 地域分布(国家/地区 消耗占比) | `google-analysis --sections geographic` |
75
75
  | 7 | 受众分布(意向/兴趣/再营销/客户匹配等列表表现) | `google-analysis --sections audience`(默认不传 `--audience-type`,系统+自定义一起拉) |
76
76
  | 8 | 关键词表现(词/消耗/CTR/CPC 排行) | `google-analysis --sections keywords`(这个命令获取的关键词数据会来自多个系列,不能合并去重) |
77
- | 9 | 否词(当前账户已有否定关键词:词面 / 匹配 / 级别 / 系列 / 广告组) | `google-analysis --sections negative-keywords`(0 条也要落盘;名单不按本周期消耗过滤) |
77
+ | 9 | 否词(当前账户已有否定关键词:词面 / 匹配 / 级别 / 系列 id / 广告组 id) | `google-analysis --sections negative-keywords`(精简列表;0 条也要落盘;不按本周期消耗过滤) |
78
78
  | 10 | 优化建议(根据以上数据给出可执行改进建议) | 不额外拉数,基于已有数据撰写 |
79
79
 
80
80
  **在执行任何数据拉取之前**,先向用户展示以下可选维度,询问是否需要追加:
@@ -143,9 +143,9 @@ siluzan-tso google-analysis -a <id> --start <s> --end <e> --json-out ./snap-goog
143
143
 
144
144
  ## 否词(必写)
145
145
 
146
- `negative-keywords-*.json` → `items[]`,合并后是 `tables.negativeKeywords[]`。这是**当前账户已经存在的否定关键词**(系列 / 广告组 / 账户三级),不是本周期的消耗或搜索词。
146
+ `negative-keywords-*.json` → `items[]`,合并后是 `tables.negativeKeywords[]`。这是**当前账户已经存在的否定关键词**(系列 / 广告组 / 账户三级),来自精简列表,不是本周期的消耗或搜索词。
147
147
 
148
- - **呈现**:词面、匹配方式、级别(`levelZh`:账户 / 系列 / 广告组)、广告系列、广告组。0 条时表格写「当前账户否词 0 条」。
148
+ - **呈现**:词面、匹配方式、级别(`levelZh`:账户 / 系列 / 广告组)、`campaignId`、`adGroupId`。精简列表没有系列名和广告组名。账户级没有 `campaignId`,只有广告组级才有 `adGroupId`。旧快照若还有 `campaignName` / `adGroupName` 可以照用。0 条时表格写「当前账户否词 0 条」。
149
149
  - **分析** → `narrative.sections.negativeKeywords.{analysis,suggestions}`(≥2 / ≥1):
150
150
  - 先写总条数,再按级别拆开。有词就点名几条;0 条就写「当前否词 0 条」。
151
151
  - 其他章节如果建议加否词、出否词清单,必须引用这张表的条数。禁止在没拉到名单时写「账户没有否词」或只在建议里提否词。
@@ -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.64-beta.1'
12
+ $PKG_VERSION = '1.1.64-beta.2'
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.64-beta.1"
12
+ readonly PKG_VERSION="1.1.64-beta.2"
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.64-beta.1",
3
+ "version": "1.1.64-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",