siluzan-tso-cli 1.1.18-beta.6 → 1.1.18-beta.7

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.18-beta.6),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.18-beta.7),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -2412,23 +2412,28 @@ function createVersionNotifier(opts) {
2412
2412
  mergeWriteConfig,
2413
2413
  readConfigRaw
2414
2414
  } = opts;
2415
- const KEY_LAST_CHECK = `${cachePrefix}LastVersionCheck`;
2416
2415
  const KEY_LATEST_STABLE = `${cachePrefix}LatestStable`;
2417
2416
  const KEY_LATEST_BETA = `${cachePrefix}LatestBeta`;
2418
2417
  const KEY_MIN_STABLE = `${cachePrefix}MinRequiredStable`;
2419
2418
  const KEY_MIN_BETA = `${cachePrefix}MinRequiredBeta`;
2420
2419
  const KEY_LAST_NOTIFIED = `${cachePrefix}LastNotified`;
2420
+ const KEY_FETCH_AT_MAIN = `${cachePrefix}VersionFetchAtMain`;
2421
+ const KEY_FETCH_AT_MIN = `${cachePrefix}VersionFetchAtMin`;
2421
2422
  const HOURS_24 = 24 * 60 * 60 * 1e3;
2422
- async function fetchVersionByTag(tag, cacheKey, cfg) {
2423
- const lastCheck = cfg[KEY_LAST_CHECK];
2424
- if (typeof lastCheck === "string" && cacheKey in cfg) {
2425
- const lastMs = new Date(lastCheck).getTime();
2426
- if (Date.now() - lastMs < HOURS_24) {
2423
+ const TTL_MAIN_TAG_MS = 60 * 60 * 1e3;
2424
+ const TTL_MIN_REQUIRED_MS = HOURS_24;
2425
+ async function fetchVersionByTag(tag, cacheKey, fetchAtKey, cfg, maxAgeMs) {
2426
+ const lastAt = cfg[fetchAtKey];
2427
+ if (typeof lastAt === "string" && cacheKey in cfg) {
2428
+ const lastMs = new Date(lastAt).getTime();
2429
+ if (Date.now() - lastMs < maxAgeMs) {
2427
2430
  const v = cfg[cacheKey];
2428
- return typeof v === "string" && v ? v : null;
2431
+ const sv = typeof v === "string" && v ? v : null;
2432
+ return { version: sv, hitNetwork: false };
2429
2433
  }
2430
2434
  }
2431
- return fetchNpmVersion(pkgName, tag);
2435
+ const version = await fetchNpmVersion(pkgName, tag);
2436
+ return { version, hitNetwork: true };
2432
2437
  }
2433
2438
  async function notifyIfOutdated2() {
2434
2439
  try {
@@ -2439,15 +2444,20 @@ function createVersionNotifier(opts) {
2439
2444
  const minCacheKey = isBeta ? KEY_MIN_BETA : KEY_MIN_STABLE;
2440
2445
  const minTag = npmMinRequiredTagForBuildEnv(isBeta ? "test" : "production");
2441
2446
  const cfg = readConfigRaw();
2442
- const [latest, minRequired] = await Promise.all([
2443
- fetchVersionByTag(tag, latestCacheKey, cfg),
2444
- fetchVersionByTag(minTag, minCacheKey, cfg)
2447
+ const [mainRes, minRes] = await Promise.all([
2448
+ fetchVersionByTag(tag, latestCacheKey, KEY_FETCH_AT_MAIN, cfg, TTL_MAIN_TAG_MS),
2449
+ fetchVersionByTag(minTag, minCacheKey, KEY_FETCH_AT_MIN, cfg, TTL_MIN_REQUIRED_MS)
2445
2450
  ]);
2446
- await mergeWriteConfig({
2447
- [KEY_LAST_CHECK]: (/* @__PURE__ */ new Date()).toISOString(),
2451
+ const latest = mainRes.version;
2452
+ const minRequired = minRes.version;
2453
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2454
+ const cacheUpdates = {
2448
2455
  [latestCacheKey]: latest ?? "",
2449
2456
  [minCacheKey]: minRequired ?? ""
2450
- });
2457
+ };
2458
+ if (mainRes.hitNetwork) cacheUpdates[KEY_FETCH_AT_MAIN] = nowIso;
2459
+ if (minRes.hitNetwork) cacheUpdates[KEY_FETCH_AT_MIN] = nowIso;
2460
+ await mergeWriteConfig(cacheUpdates);
2451
2461
  const lastNotified = typeof cfg[KEY_LAST_NOTIFIED] === "string" ? new Date(cfg[KEY_LAST_NOTIFIED]).getTime() : 0;
2452
2462
  if (Date.now() - lastNotified < HOURS_24) return;
2453
2463
  const tagLabel = isBeta ? "\uFF08\u6D4B\u8BD5\u7248\uFF09" : "\uFF08\u6B63\u5F0F\u7248\uFF09";
@@ -5965,6 +5975,11 @@ async function fetchJson(config, pathWithQuery, verbose) {
5965
5975
  function assertNever(x, ctx) {
5966
5976
  throw new Error(`${ctx}\uFF1A\u672A\u5904\u7406\u7684\u5206\u652F ${String(x)}`);
5967
5977
  }
5978
+ function rowsFromAccountDailyReportsEnvelope(raw, mediaCustomerId) {
5979
+ const block = raw.accounts?.[mediaCustomerId];
5980
+ const list = block?.data;
5981
+ return Array.isArray(list) ? list : [];
5982
+ }
5968
5983
  async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name) {
5969
5984
  switch (name) {
5970
5985
  case "overview":
@@ -5995,8 +6010,6 @@ async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name) {
5995
6010
  return fetchJson(config, fullPath, verbose);
5996
6011
  case "conversion-actions":
5997
6012
  return fetchJson(config, fullPath, verbose);
5998
- case "daily-metrics":
5999
- return fetchJson(config, fullPath, verbose);
6000
6013
  case "gold-account":
6001
6014
  return fetchJson(config, fullPath, verbose);
6002
6015
  case "ads-index":
@@ -6049,6 +6062,18 @@ async function fetchSectionPayload(def, opts, config, id) {
6049
6062
  const merged = { images, videos };
6050
6063
  return stripLegacyGoogleFieldsIfV2Present(merged);
6051
6064
  }
6065
+ if (def.name === "daily-metrics") {
6066
+ const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
6067
+ const params = new URLSearchParams({
6068
+ mediaCustomerIds: id,
6069
+ startDate: `${startDate}T00:00:00+08:00`,
6070
+ endDate: `${endDate}T23:59:59+08:00`
6071
+ });
6072
+ const url = `${config.apiBaseUrl}/report/media-account/google/account-daily-reports?${params.toString()}`;
6073
+ const raw = await apiFetch2(url, config, {}, !!opts.verbose);
6074
+ const rows = rowsFromAccountDailyReportsEnvelope(raw, id);
6075
+ return stripLegacyGoogleFieldsIfV2Present(rows);
6076
+ }
6052
6077
  const sectionPath = def.path(id);
6053
6078
  const query = buildSearchParams(def, opts.start, opts.end, extras);
6054
6079
  const data = await fetchGoogleAnalysisSectionJson(
@@ -6457,9 +6482,10 @@ var init_google_analysis2 = __esm({
6457
6482
  },
6458
6483
  {
6459
6484
  name: "daily-metrics",
6460
- description: "\u6309\u65E5\u6307\u6807\u66F2\u7EBF reports\uFF08\u542B\u8F6C\u5316\u6210\u672C\u7B49\uFF09",
6485
+ description: "\u6309\u65E5\u6307\u6807\uFF08\u4E3B\u5E73\u53F0 /report/media-account/google/account-daily-reports\uFF0C\u542B\u641C\u7D22\u4EFD\u989D\u7B49\uFF09",
6461
6486
  dateMode: "range",
6462
- path: (id) => `/reporting/media-account/${id}/reports`
6487
+ /** 仅用于 manifest endpointHint;实际请求走 fetchSectionPayload 专用分支(apiBaseUrl + 东八区起止时刻) */
6488
+ path: () => "/report/media-account/google/account-daily-reports"
6463
6489
  },
6464
6490
  {
6465
6491
  name: "gold-account",
@@ -14368,6 +14394,42 @@ init_auth();
14368
14394
  init_cli_json_snapshot();
14369
14395
  init_strip_legacy_google_fields();
14370
14396
  init_cli_table();
14397
+ function unwrapKeywordDisplayTextForEdit(raw) {
14398
+ const t = raw.trim();
14399
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) {
14400
+ return t.slice(1, -1);
14401
+ }
14402
+ if (t.length >= 2 && t.startsWith("[") && t.endsWith("]")) {
14403
+ return t.slice(1, -1);
14404
+ }
14405
+ return t;
14406
+ }
14407
+ function formatKeywordTextForMatchType(rawCoreOrDisplay, matchType) {
14408
+ const core = unwrapKeywordDisplayTextForEdit(rawCoreOrDisplay);
14409
+ switch (matchType) {
14410
+ case "Broad":
14411
+ return core;
14412
+ case "Phrase":
14413
+ return `"${core}"`;
14414
+ case "Exact":
14415
+ return `[${core}]`;
14416
+ default: {
14417
+ const _x = matchType;
14418
+ return _x;
14419
+ }
14420
+ }
14421
+ }
14422
+ function firstKeywordTextFromRecord(k) {
14423
+ const kt = k["keywordText"];
14424
+ if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
14425
+ return kt[0];
14426
+ }
14427
+ const t = k["text"];
14428
+ if (typeof t === "string") {
14429
+ return t;
14430
+ }
14431
+ return "";
14432
+ }
14371
14433
  async function runAdKeywords(opts) {
14372
14434
  const config = loadConfig(opts.token);
14373
14435
  const googleApiUrl = requireGoogleApi(config);
@@ -14559,8 +14621,17 @@ async function runAdKeywordEdit(opts) {
14559
14621
  process.exit(1);
14560
14622
  }
14561
14623
  const body = { ...keyword };
14562
- if (opts.text !== void 0) body["keywordText"] = [opts.text];
14563
- if (opts.matchType !== void 0) body["matchTypeV2"] = opts.matchType;
14624
+ if (opts.matchType !== void 0) {
14625
+ const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
14626
+ if (!String(base).trim()) {
14627
+ console.error("\n\u274C \u65E0\u6CD5\u89E3\u6790\u5F53\u524D\u5173\u952E\u8BCD\u6587\u6848\uFF0C\u8BF7\u540C\u65F6\u4F20 --text <\u8BCD\u5E72>\n");
14628
+ process.exit(1);
14629
+ }
14630
+ body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
14631
+ body["matchTypeV2"] = opts.matchType;
14632
+ } else if (opts.text !== void 0) {
14633
+ body["keywordText"] = [opts.text];
14634
+ }
14564
14635
  if (opts.maxCpc !== void 0) body["maxCPC"] = opts.maxCpc;
14565
14636
  if (opts.finalUrl !== void 0) body["finalURL"] = opts.finalUrl;
14566
14637
  const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
@@ -14607,8 +14678,17 @@ async function runAdNegativeKeywordEdit(opts) {
14607
14678
  process.exit(1);
14608
14679
  }
14609
14680
  const body = { ...keyword };
14610
- if (opts.text !== void 0) body["keywordText"] = [opts.text];
14611
- if (opts.matchType !== void 0) body["matchTypeV2"] = opts.matchType;
14681
+ if (opts.matchType !== void 0) {
14682
+ const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
14683
+ if (!String(base).trim()) {
14684
+ console.error("\n\u274C \u65E0\u6CD5\u89E3\u6790\u5F53\u524D\u5426\u5B9A\u5173\u952E\u8BCD\u6587\u6848\uFF0C\u8BF7\u540C\u65F6\u4F20 --text <\u8BCD\u5E72>\n");
14685
+ process.exit(1);
14686
+ }
14687
+ body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
14688
+ body["matchTypeV2"] = opts.matchType;
14689
+ } else if (opts.text !== void 0) {
14690
+ body["keywordText"] = [opts.text];
14691
+ }
14612
14692
  const url = `${googleApiUrl}/negativekeywordmanagement/negativekeyword/${opts.account}/${opts.id}`;
14613
14693
  try {
14614
14694
  await apiFetch2(url, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
@@ -15910,7 +15990,10 @@ function register20(program2) {
15910
15990
  });
15911
15991
  }
15912
15992
  );
15913
- adCmd.command("keyword-edit").description("\u7F16\u8F91\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u6570\u7EC4 body\uFF0C\u5148 list \u518D\u5408\u5E76\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C\uFF08\u5199\u5165 keywordText \u6570\u7EC4\uFF09").option("--match-type <type>", "\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199\u5165 matchTypeV2\uFF09").option(
15993
+ adCmd.command("keyword-edit").description("\u7F16\u8F91\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u6570\u7EC4 body\uFF0C\u5148 list \u518D\u5408\u5E76\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C\uFF08\u5199\u5165 keywordText \u6570\u7EC4\uFF09").option(
15994
+ "--match-type <type>",
15995
+ '\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199 matchTypeV2\uFF0C\u5E76\u9ED8\u8BA4\u540C\u6B65\u6539\u5199 keywordText \u4E3A\u8BCD\u5E72/"\u8BCD"/[\u8BCD] \u4EE5\u7B26\u5408\u7F51\u5173\u63A8\u65AD\uFF09'
15996
+ ).option(
15914
15997
  "--max-cpc <n>",
15915
15998
  "\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528 maxCPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 5 \u8868\u793A \xA55\uFF1B\u26A0\uFE0F \u8FD9\u4E2A\u5B57\u6BB5\u540E\u7AEF\u5355\u4F4D\u5C31\u662F\u300C\u4E3B\u5E01\u79CD\u5143\u300D\uFF0CCLI \u76F4\u63A5\u900F\u4F20\u4E0D\u505A \xD7100\uFF0C\u4E0E budget / \u7EC4 maxCPCAmount \u4E0D\u540C\uFF1B0 \u8868\u793A\u6309\u5E73\u53F0/\u8BA1\u5212\u9ED8\u8BA4\uFF09"
15916
15999
  ).option("--final-url <url>", "\u5173\u952E\u8BCD\u7EA7\u6700\u7EC8\u5230\u8FBE\u7F51\u5740 finalURL").option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
@@ -15941,7 +16024,10 @@ function register20(program2) {
15941
16024
  });
15942
16025
  }
15943
16026
  );
15944
- adCmd.command("keyword-negative-edit").description("\u7F16\u8F91\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u6587\u672C\u6216\u5339\u914D\u7C7B\u578B\uFF0C\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5426\u5B9A\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --negative --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C").option("--match-type <type>", "\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact").option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
16027
+ adCmd.command("keyword-negative-edit").description("\u7F16\u8F91\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u6587\u672C\u6216\u5339\u914D\u7C7B\u578B\uFF0C\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5426\u5B9A\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --negative --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C").option(
16028
+ "--match-type <type>",
16029
+ '\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199 matchTypeV2\uFF0C\u5E76\u9ED8\u8BA4\u540C\u6B65\u6539\u5199 keywordText \u4E3A\u8BCD\u5E72/"\u8BCD"/[\u8BCD]\uFF09'
16030
+ ).option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
15945
16031
  async (opts) => {
15946
16032
  if (opts.matchType && !["Broad", "Phrase", "Exact"].includes(opts.matchType)) {
15947
16033
  console.error("\n\u274C --match-type \u53EA\u63A5\u53D7 Broad | Phrase | Exact\n");
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.18-beta.6",
4
- "publishedAt": 1778324246028
3
+ "version": "1.1.18-beta.7",
4
+ "publishedAt": 1778465751958
5
5
  }
@@ -33,10 +33,10 @@
33
33
  2. 确定报告维度(默认含:执行摘要、每日趋势、月度汇总、系列表现、设备分布、地域分布、关键词表现、优化建议),详见 `report-templates/README.md`。
34
34
  3. **拉数**:使用 `google-analysis … --json-out <dir>`(Google)或对应 `report <media>-*` 命令落盘。
35
35
  4. **编写并执行代码**从磁盘读取 `manifest-<accountId>.json` 与各 `<section>-<accountId>.json` 来完成筛选、聚合、排序等计算;**禁止**用 `Read` 看 JSON 后在对话里心算或手填报告数字。
36
- - **写脚本前先读 `<section>-<accountId>.outline.txt`**(与 JSON 同 stem 的纯文本,最后一行是 TS 式类型字面量)了解字段结构;它体积只有几百字节、不含数据,比直接 `Read` 整个 `*.json`(动辄几 MB / 几万行)省**两到三个数量级**的上下文。**注意是 `.outline.txt` 不是 `.outline.json`**,不要 `require()`,用 `fs.readFileSync(outlineFile,'utf8')` 读最后一行即可。
37
- - 真实数据始终从 `<section>-<accountId>.json` 由脚本读,**不要**把 outline 当作业务数据贴给用户。
38
- 5. **由代码写出最终文件**(HTML/Excel/PDF/PPT/Markdown 等)。**禁止**在报告脚本中以源码字面量写死应从 JSON 读取的业务数据(消耗金额、系列名、日期区间等)。允许的常量仅限:快照目录路径、JSON 字段键名、版式/结构占位。
39
- 6. **报告首行**须标注:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`,由脚本从 JSON 拼接写入,不得手写常量冒充。
36
+ - **写脚本前先读 `<section>-<accountId>.outline.txt`**(与 JSON 同 stem 的纯文本,最后一行是 TS 式类型字面量)了解字段结构;它体积只有几百字节、不含数据,比直接 `Read` 整个 `*.json`(动辄几 MB / 几万行)省**两到三个数量级**的上下文,不要 `require()`,用 `fs.readFileSync(outlineFile,'utf8')` 读最后一行即可。
37
+ - 真实数据始终从 `<section>-<accountId>.json` 通过代码获取,**不要**把 outline 当作业务数据贴给用户。
38
+ 5. **由代码写出最终文件**(HTML/Excel/PDF/PPT/Markdown/word 等)。**禁止**在报告脚本中以源码字面量写死应从 JSON 读取的业务数据(消耗金额、系列名、日期区间等)。允许的常量仅限:快照目录路径、JSON 字段键名、版式/结构占位。
39
+ 6. **报告首行**须标注:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`
40
40
  7. 交付后帮用户打开报告文件。
41
41
 
42
42
  ---
@@ -139,7 +139,7 @@ siluzan-tso google-analysis -a <id> --exclude materials,gold-account --json-out
139
139
  | `materials` | 合并图片+视频 `{ images, videos }` |
140
140
  | `resource-counts` | 结构统计 |
141
141
  | `conversion-actions` | 转化动作 |
142
- | `daily-metrics` | 按日报表 |
142
+ | `daily-metrics` | 按日指标(主平台 `GET …/report/media-account/google/account-daily-reports`,`--json` 根为按日数组) |
143
143
  | `gold-account` | 黄金账户 |
144
144
  | `ads-index` | 质量指标 |
145
145
  | `final-urls` | 最终到达网址(不传 `--start`/`--end`) |
@@ -980,6 +980,8 @@ siluzan-tso ad keyword-delete -a 6326027735 --id 2464982882313 --adgroup-id 1955
980
980
 
981
981
  **约束:** `--text`、`--match-type`、`--max-cpc`、`--final-url` 至少传一项。
982
982
 
983
+ **匹配类型与文案:** Google 网关 V2 根据 `keywordText` 上的 `"` / `[` `]` 推断实际 MatchType(会覆盖仅传的 `matchTypeV2`)。因此只要传 `--match-type`,CLI **默认**把 `keywordText` 规范为词干 / `"词干"` / `[词干]` 并写入 `matchTypeV2`,无需额外开关;仅改匹配时可不传 `--text`(用列表里的当前文案去外层括号后再包一层)。
984
+
983
985
  ```bash
984
986
  siluzan-tso ad keyword-edit \
985
987
  -a <accountId> \
@@ -1009,6 +1011,8 @@ siluzan-tso ad keyword-edit -a 6326027735 --id 2081924039951 \
1009
1011
 
1010
1012
  ## ad keyword-negative-edit — 否词编辑
1011
1013
 
1014
+ 与搜索词相同:传 `--match-type` 时 CLI 会默认同步改写 `keywordText` 外层括号/引号。
1015
+
1012
1016
  ```bash
1013
1017
  siluzan-tso ad keyword-negative-edit \
1014
1018
  -a <accountId> \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.18-beta.6",
3
+ "version": "1.1.18-beta.7",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",