siluzan-tso-cli 1.1.63 → 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
@@ -43,7 +43,7 @@ HTML 报告模板外部资源:**ECharts** 统一使用 `https://staticpn.siluz
43
43
  在**用户的目标项目根目录**执行(根据用户使用的助手选择 `--ai`):
44
44
 
45
45
  ```bash
46
- npm install -g siluzan-tso-cli
46
+ npm install -g siluzan-tso-cli@beta
47
47
  siluzan-tso init --ai cursor # 写入 Cursor(默认)
48
48
  siluzan-tso init --ai cursor,claude # 同时写入多个平台
49
49
  siluzan-tso init --ai all # 写入所有支持的平台
@@ -51,6 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
+ > **注意**:当前为测试版(1.1.64-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
55
 
55
56
  | 助手 | 建议 `--ai` |
56
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -2337,7 +2337,7 @@ function rawRequest(url, options) {
2337
2337
  const parsed = new URL(url);
2338
2338
  const transport = parsed.protocol === "https:" ? https : http;
2339
2339
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2340
- const maxBytes = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
2340
+ const maxBytes = options.maxResponseBytes ?? 0;
2341
2341
  const isHttps = parsed.protocol === "https:";
2342
2342
  const headers = {
2343
2343
  ...buildSiluzanClientIdentityHeaders(),
@@ -2375,7 +2375,10 @@ function rawRequest(url, options) {
2375
2375
  } else if (encoding === "br") {
2376
2376
  stream = res.pipe(zlib.createBrotliDecompress());
2377
2377
  }
2378
- const handleStreamError = (err) => {
2378
+ let failed = false;
2379
+ const fail5 = (err) => {
2380
+ if (failed) return;
2381
+ failed = true;
2379
2382
  res.destroy();
2380
2383
  if (perfOn) {
2381
2384
  emitPerf({
@@ -2385,43 +2388,37 @@ function rawRequest(url, options) {
2385
2388
  elapsedMs: performance.now() - t0,
2386
2389
  bytes: 0,
2387
2390
  ok: false,
2388
- error: `decompress error: ${err.message}`
2391
+ error: err.message
2389
2392
  });
2390
2393
  }
2391
- reject(new Error(`\u54CD\u5E94\u89E3\u538B\u5931\u8D25\uFF08${encoding || "identity"}\uFF09\uFF1A${err.message}`));
2394
+ reject(err);
2392
2395
  };
2393
- if (stream !== res) stream.on("error", handleStreamError);
2394
- res.on("error", handleStreamError);
2395
- let data = "";
2396
+ const onStreamError = (err) => {
2397
+ fail5(new Error(`\u54CD\u5E94\u89E3\u538B\u5931\u8D25\uFF08${encoding || "identity"}\uFF09\uFF1A${err.message}`));
2398
+ };
2399
+ if (stream !== res) stream.on("error", onStreamError);
2400
+ res.on("error", onStreamError);
2401
+ const chunks = [];
2396
2402
  let byteLen = 0;
2397
- stream.setEncoding("utf8");
2398
2403
  stream.on("data", (chunk) => {
2399
- byteLen += Buffer.byteLength(chunk, "utf8");
2400
- if (maxBytes && byteLen > maxBytes) {
2401
- res.destroy();
2402
- if (perfOn) {
2403
- emitPerf({
2404
- method,
2405
- url,
2406
- status: res.statusCode ?? 0,
2407
- elapsedMs: performance.now() - t0,
2408
- bytes: byteLen,
2409
- ok: false,
2410
- error: "response body exceeded limit"
2411
- });
2412
- }
2413
- reject(
2404
+ if (failed) return;
2405
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
2406
+ byteLen += buf.length;
2407
+ if (maxBytes > 0 && byteLen > maxBytes) {
2408
+ fail5(
2414
2409
  new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u4E0A\u9650\uFF08${(maxBytes / 1024 / 1024).toFixed(0)} MB\uFF09\uFF0C\u5DF2\u4E2D\u65AD\u8FDE\u63A5`)
2415
2410
  );
2416
2411
  return;
2417
2412
  }
2418
- data += chunk;
2413
+ chunks.push(buf);
2419
2414
  });
2420
2415
  stream.on("end", () => {
2421
- const headers2 = {};
2416
+ if (failed) return;
2417
+ const text3 = Buffer.concat(chunks).toString("utf8");
2418
+ const responseHeaders = {};
2422
2419
  for (const [key, val] of Object.entries(res.headers)) {
2423
2420
  if (val !== void 0) {
2424
- headers2[key.toLowerCase()] = Array.isArray(val) ? val[0] : val;
2421
+ responseHeaders[key.toLowerCase()] = Array.isArray(val) ? val[0] : val;
2425
2422
  }
2426
2423
  }
2427
2424
  const status = res.statusCode ?? 0;
@@ -2435,7 +2432,7 @@ function rawRequest(url, options) {
2435
2432
  ok: status >= 200 && status < 300
2436
2433
  });
2437
2434
  }
2438
- resolve52({ status, text: data, headers: headers2 });
2435
+ resolve52({ status, text: text3, headers: responseHeaders });
2439
2436
  });
2440
2437
  });
2441
2438
  req.on("timeout", () => {
@@ -3924,7 +3921,7 @@ async function issueApiKeyWithPhoneCode(opts) {
3924
3921
  });
3925
3922
  return apiKey;
3926
3923
  }
3927
- var import_semver, SILUZAN_DIR, CONFIG_FILE, inProcessLockChain, ALLOWED_HOSTNAME_SUFFIXES, EXACT_ID_FIELD_NAMES, SILUZAN_AGENT_COMPANY_SOURCE_TYPE, DEERFLOW_AGENT_ENV, _clientProduct, _resolveVersion, DEFAULT_TIMEOUT_MS, MAX_RESPONSE_BYTES, httpsAgent, httpAgent, PERF_PREFIX, DEFAULT_DETAIL_MAX, VERBOSE_DETAIL_MAX, GET_RETRY_DELAYS_MS, TABLE_CELL_PIPE, ASCII_TABLE_CHARS, CLI_SNAPSHOT_MANIFEST_FILE, SCHEMA_VERSION, FALLBACK_FIELD_GUIDE_REFS, _cliPackage, _resolveCliVersion, _fieldGuideRefs, OUTLINE_AGENT_HINT, TS_LIKE_ARRAY_SAMPLE_MAX, MAX_INVOCATION_CHARS, currentInvocation, MAX_COMMIT_CHARS, WRITE_AUDIT_AGENT_AUTO_COMMIT, cliCommitOverride, WRITE_AUDIT_SCHEMA_VERSION, MUTATING, BEIJING_TZ, AUDIT_LOG_FILENAME_RE, DEFAULT_MAX_FILE_MB, MIN_SEGMENT_BYTES, MAX_SEGMENT_BYTES, DEFAULT_RETENTION_DAYS, MIN_RETENTION_DAYS, MAX_RETENTION_DAYS, PRUNE_THROTTLE_MS, lastPruneAtMs, MAX_BODY_CHARS, MAX_ERROR_CHARS, DEFAULT_FIND_AUDIT_MAX_DAYS, MAX_SNAPSHOT_FILE_BYTES, API_KEY_SERVICE_VALUES;
3924
+ var import_semver, SILUZAN_DIR, CONFIG_FILE, inProcessLockChain, ALLOWED_HOSTNAME_SUFFIXES, EXACT_ID_FIELD_NAMES, SILUZAN_AGENT_COMPANY_SOURCE_TYPE, DEERFLOW_AGENT_ENV, _clientProduct, _resolveVersion, DEFAULT_TIMEOUT_MS, httpsAgent, httpAgent, PERF_PREFIX, DEFAULT_DETAIL_MAX, VERBOSE_DETAIL_MAX, GET_RETRY_DELAYS_MS, TABLE_CELL_PIPE, ASCII_TABLE_CHARS, CLI_SNAPSHOT_MANIFEST_FILE, SCHEMA_VERSION, FALLBACK_FIELD_GUIDE_REFS, _cliPackage, _resolveCliVersion, _fieldGuideRefs, OUTLINE_AGENT_HINT, TS_LIKE_ARRAY_SAMPLE_MAX, MAX_INVOCATION_CHARS, currentInvocation, MAX_COMMIT_CHARS, WRITE_AUDIT_AGENT_AUTO_COMMIT, cliCommitOverride, WRITE_AUDIT_SCHEMA_VERSION, MUTATING, BEIJING_TZ, AUDIT_LOG_FILENAME_RE, DEFAULT_MAX_FILE_MB, MIN_SEGMENT_BYTES, MAX_SEGMENT_BYTES, DEFAULT_RETENTION_DAYS, MIN_RETENTION_DAYS, MAX_RETENTION_DAYS, PRUNE_THROTTLE_MS, lastPruneAtMs, MAX_BODY_CHARS, MAX_ERROR_CHARS, DEFAULT_FIND_AUDIT_MAX_DAYS, MAX_SNAPSHOT_FILE_BYTES, API_KEY_SERVICE_VALUES;
3928
3925
  var init_dist = __esm({
3929
3926
  "../common/dist/index.js"() {
3930
3927
  "use strict";
@@ -3974,7 +3971,6 @@ var init_dist = __esm({
3974
3971
  DEERFLOW_AGENT_ENV = "DEERFLOW_AGENT";
3975
3972
  _clientProduct = "siluzan-cli";
3976
3973
  DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
3977
- MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
3978
3974
  httpsAgent = new https.Agent({
3979
3975
  keepAlive: true,
3980
3976
  keepAliveMsecs: 3e4,
@@ -4051,7 +4047,7 @@ var DEFAULT_API_BASE;
4051
4047
  var init_defaults = __esm({
4052
4048
  "src/config/defaults.ts"() {
4053
4049
  "use strict";
4054
- DEFAULT_API_BASE = "https://tso-api.siluzan.com";
4050
+ DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
4055
4051
  }
4056
4052
  });
4057
4053
 
@@ -5564,6 +5560,17 @@ function resolveAdAccountId(account, label2 = "-a, --account") {
5564
5560
  function withResolvedAdAccount(opts) {
5565
5561
  return { ...opts, account: resolveAdAccountId(opts.account) };
5566
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
+ }
5567
5574
  async function findItemInList(listUrl, config, id, verbose) {
5568
5575
  const res = await apiFetch2(listUrl, config, {}, verbose);
5569
5576
  const item = (res.data ?? []).find((x) => x["id"] === id);
@@ -6402,6 +6409,20 @@ function coerceToRowArray(value) {
6402
6409
  }
6403
6410
  return [];
6404
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
+ }
6405
6426
  function asRecord(value) {
6406
6427
  if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
6407
6428
  return value;
@@ -6436,7 +6457,7 @@ function wrapGoogleAnalysisSnapshotPayload(section, payload) {
6436
6457
  const bag = asRecord(payload);
6437
6458
  if (bag) {
6438
6459
  const r = readGatewayList(bag);
6439
- items3 = r.items;
6460
+ items3 = section === "negative-keywords" ? dedupeRowsById(r.items) : r.items;
6440
6461
  meta = r.meta;
6441
6462
  }
6442
6463
  } else if (ROOT_ARRAY_SECTIONS.has(section)) {
@@ -8124,9 +8145,10 @@ var init_sections = __esm({
8124
8145
  },
8125
8146
  {
8126
8147
  name: "negative-keywords",
8127
- 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 是为了快照文件名仍带报告区间,避免和上一次落盘互相覆盖。
8128
8150
  dateMode: "range",
8129
- path: (id) => `/negativekeywordmanagement/v2/list/${id}`
8151
+ path: (id) => `/negativekeywordmanagement/v2/list/${id}/slim`
8130
8152
  },
8131
8153
  {
8132
8154
  name: "search-terms",
@@ -104346,6 +104368,8 @@ function mapNegativeKeywordRows(rows) {
104346
104368
  if (id && seen.has(id)) continue;
104347
104369
  if (id) seen.add(id);
104348
104370
  const level = String(row.negativeLevel ?? "").trim();
104371
+ const campaignId = String(row.campaignId ?? "").trim();
104372
+ const adGroupId = String(row.adGroupId ?? "").trim();
104349
104373
  const campaignName = String(row.campaignName ?? "").trim();
104350
104374
  const adGroupName = String(row.adGroupName ?? "").trim();
104351
104375
  mapped.push({
@@ -104353,6 +104377,8 @@ function mapNegativeKeywordRows(rows) {
104353
104377
  matchType: String(row.matchTypeV2 ?? row.matchType ?? "\u2014"),
104354
104378
  level: level || "\u2014",
104355
104379
  levelZh: NEGATIVE_LEVEL_ZH[level] ?? (level || "\u2014"),
104380
+ campaignId: campaignId || "\u2014",
104381
+ adGroupId: adGroupId || "\u2014",
104356
104382
  campaignName: campaignName || "\u2014",
104357
104383
  adGroupName: adGroupName || "\u2014"
104358
104384
  });
@@ -115261,6 +115287,7 @@ function resolveKeywordTexts(opts) {
115261
115287
  }
115262
115288
  function firstKeywordTextFromRecord(k) {
115263
115289
  const kt = k["keywordText"];
115290
+ if (typeof kt === "string") return kt;
115264
115291
  if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
115265
115292
  return kt[0];
115266
115293
  }
@@ -115270,9 +115297,27 @@ function firstKeywordTextFromRecord(k) {
115270
115297
  }
115271
115298
  return "";
115272
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
+ }
115273
115314
  function toNegativeKeywordAppliedLevelQuery(level) {
115274
115315
  return level === "AdGroup" ? "Ad group" : level;
115275
115316
  }
115317
+ function filterNegativeKeywordsByMatchType(items3, matchType) {
115318
+ const want = matchType.toLowerCase();
115319
+ return items3.filter((item) => String(item["matchTypeV2"] ?? "").toLowerCase() === want);
115320
+ }
115276
115321
  function keywordRowToYuan(item) {
115277
115322
  const { maxCPC: rawMaxCpc, ...rest } = item;
115278
115323
  if (rawMaxCpc == null) return item;
@@ -115282,14 +115327,20 @@ async function runAdKeywords(opts) {
115282
115327
  opts = withResolvedAdAccount(opts);
115283
115328
  const config = loadConfig(opts.token);
115284
115329
  const googleApiUrl = requireGoogleApi(config);
115285
- const params = new URLSearchParams();
115286
- params.set("startDate", toGoogleDate(opts.startDate, -30));
115287
- params.set("endDate", toGoogleDate(opts.endDate, 0));
115288
- if (opts.negative && opts.level) {
115289
- 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}`;
115290
115343
  }
115291
- const endpoint = opts.negative ? "negativekeywordmanagement" : "keywordmanagement";
115292
- const url = `${googleApiUrl}/${endpoint}/v2/list/${opts.account}?${params}`;
115293
115344
  let data;
115294
115345
  try {
115295
115346
  data = await apiFetch2(url, config, {}, opts.verbose);
@@ -115301,12 +115352,15 @@ async function runAdKeywords(opts) {
115301
115352
  }
115302
115353
  const rawItems = data.data ?? [];
115303
115354
  const seen = /* @__PURE__ */ new Set();
115304
- const items3 = rawItems.filter((item) => {
115355
+ let items3 = rawItems.filter((item) => {
115305
115356
  const key = item["id"];
115306
115357
  if (key !== void 0 && seen.has(key)) return false;
115307
115358
  if (key !== void 0) seen.add(key);
115308
115359
  return true;
115309
115360
  });
115361
+ if (opts.negative && opts.matchType) {
115362
+ items3 = filterNegativeKeywordsByMatchType(items3, opts.matchType);
115363
+ }
115310
115364
  const label2 = opts.negative ? "\u5426\u5B9A\u5173\u952E\u8BCD" : "\u5173\u952E\u8BCD";
115311
115365
  const n = items3.length;
115312
115366
  const displayItems = opts.negative ? items3 : items3.map((row) => keywordRowToYuan(row));
@@ -115314,7 +115368,7 @@ async function runAdKeywords(opts) {
115314
115368
  wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items: displayItems })
115315
115369
  );
115316
115370
  if (await emitCliJsonOrSnapshot(opts, {
115317
- section: `ad-keywords-${opts.account}-${opts.negative ? "negative" : "positive"}`,
115371
+ section: `ad-keywords-${opts.negative ? "negative" : "positive"}`,
115318
115372
  commandLabel: "ad keywords",
115319
115373
  commandHint: opts.account,
115320
115374
  payload: kwPayload,
@@ -115336,7 +115390,11 @@ ${label2}\uFF08\u8D26\u6237\uFF1A${opts.account}\uFF0C\u7B2C 1 \u9875\uFF0C\u672
115336
115390
  const kwText = Array.isArray(item["keywordText"]) ? item["keywordText"].join(", ") : String(item["text"] ?? item["keywordText"] ?? item["id"] ?? "\u2014");
115337
115391
  const matchType = item["matchTypeV2"] ?? "\u2014";
115338
115392
  const level = item["negativeLevel"] ?? "\u2014";
115339
- 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
+ );
115340
115398
  });
115341
115399
  } else {
115342
115400
  const kwW = Math.min(
@@ -115520,10 +115578,7 @@ async function runAdNegativeKeywordCreate(opts) {
115520
115578
  `);
115521
115579
  process.exit(1);
115522
115580
  }
115523
- const dateParams = new URLSearchParams();
115524
- dateParams.set("startDate", toGoogleDate(void 0, -30));
115525
- dateParams.set("endDate", toGoogleDate(void 0, 0));
115526
- const listUrl = `${googleApiUrl}/negativekeywordmanagement/v2/list/${opts.account}?${dateParams}`;
115581
+ const listUrl = negativeKeywordSlimListUrl(googleApiUrl, opts.account);
115527
115582
  const readList = async () => {
115528
115583
  try {
115529
115584
  const listed = await apiFetch2(
@@ -115797,10 +115852,7 @@ async function runAdNegativeKeywordEdit(opts) {
115797
115852
  opts = withResolvedAdAccount(opts);
115798
115853
  const config = loadConfig(opts.token);
115799
115854
  const googleApiUrl = requireGoogleApi(config);
115800
- const dateParams = new URLSearchParams();
115801
- dateParams.set("startDate", toGoogleDate(opts.startDate, -30));
115802
- dateParams.set("endDate", toGoogleDate(opts.endDate, 0));
115803
- const listUrl = `${googleApiUrl}/negativekeywordmanagement/v2/list/${opts.account}?${dateParams}`;
115855
+ const listUrl = negativeKeywordSlimListUrl(googleApiUrl, opts.account);
115804
115856
  let keyword;
115805
115857
  try {
115806
115858
  keyword = await findItemInList(listUrl, config, opts.id, opts.verbose);
@@ -115810,7 +115862,7 @@ async function runAdNegativeKeywordEdit(opts) {
115810
115862
  `);
115811
115863
  process.exit(1);
115812
115864
  }
115813
- const body = { ...keyword };
115865
+ const body = negativeKeywordWriteBody(keyword);
115814
115866
  if (opts.matchType !== void 0) {
115815
115867
  const base2 = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
115816
115868
  if (!String(base2).trim()) {
@@ -115853,10 +115905,7 @@ async function runAdNegativeKeywordDelete(opts) {
115853
115905
  opts = withResolvedAdAccount(opts);
115854
115906
  const config = loadConfig(opts.token);
115855
115907
  const googleApiUrl = requireGoogleApi(config);
115856
- const dateParams = new URLSearchParams();
115857
- dateParams.set("startDate", toGoogleDate(opts.startDate, -30));
115858
- dateParams.set("endDate", toGoogleDate(opts.endDate, 0));
115859
- const listUrl = `${googleApiUrl}/negativekeywordmanagement/v2/list/${opts.account}?${dateParams}`;
115908
+ const listUrl = negativeKeywordSlimListUrl(googleApiUrl, opts.account);
115860
115909
  let keyword;
115861
115910
  try {
115862
115911
  keyword = await findItemInList(listUrl, config, opts.id, opts.verbose);
@@ -115868,7 +115917,10 @@ async function runAdNegativeKeywordDelete(opts) {
115868
115917
  }
115869
115918
  const url = `${googleApiUrl}/negativekeywordmanagement/negativekeyword/${opts.account}/${opts.id}`;
115870
115919
  try {
115871
- 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
+ });
115872
115924
  } catch (err) {
115873
115925
  console.error(`
115874
115926
  \u274C \u5220\u9664\u5426\u5B9A\u5173\u952E\u8BCD\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
@@ -121146,7 +121198,7 @@ async function fetchLiveSnapshotForCampaign(config, accountId, campaignId, campa
121146
121198
  ),
121147
121199
  fetchList(
121148
121200
  config,
121149
- `${googleApiUrl}/negativekeywordmanagement/v2/list/${accountId}?${qs}`,
121201
+ negativeKeywordSlimListUrl(googleApiUrl, accountId),
121150
121202
  verbose
121151
121203
  ),
121152
121204
  fetchList(
@@ -127005,11 +127057,18 @@ function register21(program2) {
127005
127057
  }
127006
127058
  );
127007
127059
  adCmd.command("keywords").alias("keyword-list").description(
127008
- "\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"
127009
- ).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(
127010
127062
  "--level <level>",
127011
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"
127012
- ).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(
127013
127072
  "--json-out <path>",
127014
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",
127015
127074
  void 0
@@ -127019,11 +127078,26 @@ function register21(program2) {
127019
127078
  console.error("\n\u274C --level \u53EA\u63A5\u53D7 Account | Campaign | AdGroup\n");
127020
127079
  process.exit(1);
127021
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
+ }
127022
127092
  await runAdKeywords({
127023
127093
  token: opts.token,
127024
127094
  account: opts.account,
127025
127095
  negative: opts.negative,
127026
127096
  level: opts.level,
127097
+ keywordText: opts.keyword,
127098
+ matchType: opts.matchType,
127099
+ excludePaused: opts.excludePaused,
127100
+ readDeleted: opts.readDeleted,
127027
127101
  startDate: opts.start,
127028
127102
  endDate: opts.end,
127029
127103
  jsonOut: opts.jsonOut,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.63",
4
- "publishedAt": 1793607684861
3
+ "version": "1.1.64-beta.2",
4
+ "publishedAt": 1793612804179
5
5
  }
@@ -170,7 +170,7 @@ siluzan-tso account-history --start 2026-03-01 --end 2026-03-31 --json-out ./sna
170
170
  | 状态 | 含义 | 下一步操作 |
171
171
  | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
172
172
  | `Pending` | 审核中 | 等待,可反复运行此命令轮询;审核周期因媒体而异 |
173
- | `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `https://www.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google`;Yandex 当前没有对应充值界面) |
173
+ | `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google`;Yandex 当前没有对应充值界面) |
174
174
  | `Rejected` | 被拒 | 查看 `--json-out` 落盘中的 `reason` 字段了解拒绝原因;修改资料后重新提交;若原因不明,引导用户联系丝路赞客服 |
175
175
 
176
176
  ---
@@ -490,7 +490,7 @@ siluzan-tso account email-deauth -c <mediaCustomerId> --invitation-id <id> --res
490
490
 
491
491
  | 功能 | 媒体 | 网页路径 |
492
492
  | --------------------------------------- | ------ | ------------------------------------------------- |
493
- | **账户激活**(邀请他人激活 / 充值激活) | Google | `https://www.siluzan.com/v3/foreign_trade/tso/manageAccounts` |
493
+ | **账户激活**(邀请他人激活 / 充值激活) | Google | `https://www-ci.siluzan.com/v3/foreign_trade/tso/manageAccounts` |
494
494
 
495
495
  **Agent 建议话术**:
496
496
 
@@ -499,5 +499,5 @@ siluzan-tso account email-deauth -c <mediaCustomerId> --invitation-id <id> --res
499
499
  siluzan-tso config show # 查看 webUrl 字段
500
500
 
501
501
  # 账户激活(Google)→ 引导至账户管理页
502
- # https://www.siluzan.com/v3/foreign_trade/tso/manageAccounts
502
+ # https://www-ci.siluzan.com/v3/foreign_trade/tso/manageAccounts
503
503
  ```
@@ -126,7 +126,7 @@ siluzan-tso config show
126
126
 
127
127
  ### 充值页链接(按媒体 × 类型)
128
128
 
129
- 链接模式:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/<page>?mediaType=<mediaType>`;丝路赞钱包:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet`(无媒体参数)。
129
+ 链接模式:`https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/<page>?mediaType=<mediaType>`;丝路赞钱包:`https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet`(无媒体参数)。
130
130
 
131
131
  | 充值类型 | `<page>` | 支持媒体(`mediaType` 参数) |
132
132
  | ------------------------- | --------------------- | ------------------------------------------- |
@@ -1,6 +1,6 @@
1
1
  # 各媒体开户
2
2
 
3
- > 网页链接:`https://www.siluzan.com/v3/foreign_trade/tso/accountOpeningHistory?tso=%2Fv3umijs%2Ftso%2FaccountOpeningHistory`
3
+ > 网页链接:`https://www-ci.siluzan.com/v3/foreign_trade/tso/accountOpeningHistory?tso=%2Fv3umijs%2Ftso%2FaccountOpeningHistory`
4
4
  > 多命令串联见 `references/core/workflows.md` § 流程一。
5
5
 
6
6
  ## Contents
@@ -65,7 +65,7 @@ siluzan-tso open-account google-wizard
65
65
 
66
66
  ```bash
67
67
  siluzan-tso account-history -m Google
68
- # 审核通过后:config show → https://www.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google
68
+ # 审核通过后:config show → https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google
69
69
  ```
70
70
 
71
71
  ---
@@ -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
  # 单维度
@@ -1,6 +1,6 @@
1
1
  # rag:知识库检索(TSO 广告投放辅助)
2
2
 
3
- 知识库管理页面在 https://www.siluzan.com/knowledge-base/
3
+ 知识库管理页面在 https://www-ci.siluzan.com/knowledge-base/
4
4
 
5
5
  为 **广告投放、账户分析、拓词、诊断报告** 等 TSO 业务提供**企业已入库**的产品、行业、客户背景事实依据。
6
6
 
@@ -190,9 +190,9 @@ siluzan-tso report push receive-emails -m Google [--json-out ./snap]
190
190
 
191
191
  | 媒体 | 报告类型 | URL 模板 |
192
192
  | ------ | ---------------- | ----------------------------------------------------------- |
193
- | Google | 日报(Daily) | `https://www.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
194
- | Google | 小时报(Hourly) | `https://www.siluzan.com/media-report/hour/{entityId}?culture=zh-CN` |
195
- | TikTok | 日报 | `https://www.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
193
+ | Google | 日报(Daily) | `https://www-ci.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
194
+ | Google | 小时报(Hourly) | `https://www-ci.siluzan.com/media-report/hour/{entityId}?culture=zh-CN` |
195
+ | TikTok | 日报 | `https://www-ci.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
196
196
 
197
197
  `entityId` 来自 `siluzan-tso report list --json-out ./snap` 中每条记录的 `entityId` 字段。
198
198
 
@@ -205,8 +205,8 @@ siluzan-tso report list -m Google --json-out ./snap
205
205
 
206
206
  # 第二步:查看 webUrl
207
207
  siluzan-tso config show
208
- # webUrl: https://www.siluzan.com
208
+ # webUrl: https://www-ci.siluzan.com
209
209
 
210
210
  # 第三步:拼接链接(Google 日报)
211
- # https://www.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
211
+ # https://www-ci.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
212
212
  ```
@@ -10,7 +10,7 @@
10
10
  ## 安装 CLI
11
11
 
12
12
  ```bash
13
- npm install -g siluzan-tso-cli
13
+ npm install -g siluzan-tso-cli@beta
14
14
  ```
15
15
 
16
16
  ---
@@ -64,7 +64,7 @@ siluzan-tso config set --api-key <Key> # 或 config 直接写入
64
64
  siluzan-tso config set --token <Token> # 备用:设置 JWT Token
65
65
  ```
66
66
 
67
- API Key 获取入口:`https://www.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
67
+ API Key 获取入口:`https://www-ci.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
68
68
 
69
69
  ```bash
70
70
  # 第 1 步:让用户报出手机号后,立刻发码(命令立即返回,不会等待输入)
@@ -127,9 +127,9 @@ siluzan-tso config show
127
127
 
128
128
  ```
129
129
  构建环境 : production
130
- apiBaseUrl : https://tso-api.siluzan.com
131
- googleApiUrl : https://googleapi.mysiluzan.com
132
- webUrl : https://www.siluzan.com
130
+ apiBaseUrl : https://tso-api-ci.siluzan.com
131
+ googleApiUrl : https://googleapi-ci.mysiluzan.com
132
+ webUrl : https://www-ci.siluzan.com
133
133
  apiKey : abcd****1234
134
134
  ```
135
135
 
@@ -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 后台手工应用**——这是本次新增前的旧限制,现已由接口直接支持。
@@ -6,7 +6,7 @@
6
6
  siluzan-tso config show # 取 webUrl
7
7
  ```
8
8
 
9
- 首页地址:`https://www.siluzan.com/v3/foreign_trade/tso/home`
9
+ 首页地址:`https://www-ci.siluzan.com/v3/foreign_trade/tso/home`
10
10
 
11
11
  ---
12
12
 
@@ -25,7 +25,7 @@ siluzan-tso config show # 取 webUrl
25
25
 
26
26
  ## 推荐话术
27
27
 
28
- 1. **「和首页一样的总览」** → 打开 `https://www.siluzan.com/v3/foreign_trade/tso/home`。
28
+ 1. **「和首页一样的总览」** → 打开 `https://www-ci.siluzan.com/v3/foreign_trade/tso/home`。
29
29
  2. **「某个 Google 账户昨天花了多少」** → `list-accounts -m Google` + `stats -m Google -a <id>`。
30
30
  3. **「有待充值账户」** → 说明聚合数据在首页;CLI 可 `list-accounts` + `balance` 逐户排查,或引导充值页。
31
31
 
@@ -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,11 +9,11 @@ $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.63'
12
+ $PKG_VERSION = '1.1.64-beta.2'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
- $INSTALL_CMD = 'npm install -g siluzan-tso-cli'
16
- $WEB_BASE = 'https://www.siluzan.com'
15
+ $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
16
+ $WEB_BASE = 'https://www-ci.siluzan.com'
17
17
 
18
18
  # -- Constants ----------------------------------------------------------------
19
19
  $NODE_MAJOR_MIN = 18
@@ -9,11 +9,11 @@ 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.63"
12
+ readonly PKG_VERSION="1.1.64-beta.2"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
- readonly INSTALL_CMD="npm install -g siluzan-tso-cli"
16
- readonly WEB_BASE="https://www.siluzan.com"
15
+ readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
16
+ readonly WEB_BASE="https://www-ci.siluzan.com"
17
17
 
18
18
  # -- Constants ----------------------------------------------------------------
19
19
  readonly NODE_MAJOR_MIN=18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.63",
3
+ "version": "1.1.64-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",