siluzan-tso-cli 1.1.59-beta.2 → 1.1.59

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.
Files changed (31) hide show
  1. package/README.md +1 -2
  2. package/dist/index.js +245 -49
  3. package/dist/skill/_meta.json +2 -2
  4. package/dist/skill/assets/google-period-report.schema.json +23 -7
  5. package/dist/skill/assets/pmax-create-template.json +2 -1
  6. package/dist/skill/assets/pmax-create-template.md +15 -1
  7. package/dist/skill/references/accounts/accounts-list.md +1 -1
  8. package/dist/skill/references/accounts/accounts-permissions.md +2 -2
  9. package/dist/skill/references/accounts/finance.md +1 -1
  10. package/dist/skill/references/accounts/open-account-by-media.md +1 -1
  11. package/dist/skill/references/accounts/open-account-google-ui.md +1 -1
  12. package/dist/skill/references/analytics/account-analytics.md +5 -5
  13. package/dist/skill/references/analytics/rag.md +1 -1
  14. package/dist/skill/references/analytics/reporting.md +5 -5
  15. package/dist/skill/references/core/agent-conventions.md +2 -2
  16. package/dist/skill/references/core/intent-routing.md +1 -1
  17. package/dist/skill/references/core/playbooks.md +7 -7
  18. package/dist/skill/references/core/setup.md +5 -5
  19. package/dist/skill/references/core/subagent-orchestration.md +1 -1
  20. package/dist/skill/references/google-ads/google-ads-write.md +1 -1
  21. package/dist/skill/references/google-ads/pmax-api.md +3 -1
  22. package/dist/skill/references/misc/tso-home.md +2 -2
  23. package/dist/skill/references/report-templates/google-period-report.md +24 -10
  24. package/dist/skill/references/report-templates/okki-weekly-google-client.md +30 -13
  25. package/dist/skill/report-templates/google-period-report.html +56 -1
  26. package/dist/skill/report-templates/google-period-report.md +24 -10
  27. package/dist/skill/report-templates/okki-weekly-google-client.md +30 -13
  28. package/dist/skill/scripts/install.ps1 +3 -3
  29. package/dist/skill/scripts/install.sh +3 -3
  30. package/dist/skill/snippets/handoff-p6-okki.md +2 -2
  31. package/package.json +1 -1
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@beta
46
+ npm install -g siluzan-tso-cli
47
47
  siluzan-tso init --ai cursor # 写入 Cursor(默认)
48
48
  siluzan-tso init --ai cursor,claude # 同时写入多个平台
49
49
  siluzan-tso init --ai all # 写入所有支持的平台
@@ -51,7 +51,6 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.59-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
54
 
56
55
  | 助手 | 建议 `--ai` |
57
56
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -2570,7 +2570,9 @@ function buildAuthHeaders(config) {
2570
2570
  }
2571
2571
  async function parseApiResponse(status, text3, verbose) {
2572
2572
  if (status < 200 || status >= 300) {
2573
- throw new Error(formatHttpError(status, text3, { verbose }));
2573
+ const err = new Error(formatHttpError(status, text3, { verbose }));
2574
+ Object.assign(err, { status, rawBody: text3 });
2575
+ throw err;
2574
2576
  }
2575
2577
  if (!text3.trim()) return null;
2576
2578
  try {
@@ -4049,7 +4051,7 @@ var DEFAULT_API_BASE;
4049
4051
  var init_defaults = __esm({
4050
4052
  "src/config/defaults.ts"() {
4051
4053
  "use strict";
4052
- DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
4054
+ DEFAULT_API_BASE = "https://tso-api.siluzan.com";
4053
4055
  }
4054
4056
  });
4055
4057
 
@@ -6434,7 +6436,8 @@ var init_wrap_snapshot_envelope = __esm({
6434
6436
  ]);
6435
6437
  GATEWAY_LIST_SECTIONS = /* @__PURE__ */ new Set([
6436
6438
  "search-terms",
6437
- "ads"
6439
+ "ads",
6440
+ "negative-keywords"
6438
6441
  ]);
6439
6442
  }
6440
6443
  });
@@ -7670,6 +7673,12 @@ var init_sections = __esm({
7670
7673
  defaultQuery: { limit: 0, orderByCost: true, costGreater: 0 },
7671
7674
  keywordOptions: true
7672
7675
  },
7676
+ {
7677
+ name: "negative-keywords",
7678
+ 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",
7679
+ dateMode: "range",
7680
+ path: (id) => `/negativekeywordmanagement/v2/list/${id}`
7681
+ },
7673
7682
  {
7674
7683
  name: "search-terms",
7675
7684
  description: "\u641C\u7D22\u8BCD searchtermmanagement/v2/list\uFF08\u9ED8\u8BA4 limit=0 \u4E0D\u5C01\u9876\uFF0CCLI \u843D\u76D8\u524D\u8FC7\u6EE4 spend>0\uFF09",
@@ -103462,6 +103471,8 @@ async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name2,
103462
103471
  return fetchSection();
103463
103472
  case "keywords":
103464
103473
  return fetchSection();
103474
+ case "negative-keywords":
103475
+ return fetchSection();
103465
103476
  case "search-terms":
103466
103477
  return fetchSection();
103467
103478
  case "campaigns":
@@ -103867,6 +103878,37 @@ async function loadSectionFile(snapshotDir, file) {
103867
103878
  return null;
103868
103879
  }
103869
103880
  }
103881
+ function negativeKeywordText(row) {
103882
+ const text3 = row.keywordText;
103883
+ if (Array.isArray(text3)) {
103884
+ const parts = text3.map((part) => String(part).trim()).filter(Boolean);
103885
+ if (parts.length > 0) return parts.join(", ");
103886
+ }
103887
+ if (typeof text3 === "string" && text3.trim()) return text3.trim();
103888
+ if (typeof row.keyword === "string" && row.keyword.trim()) return row.keyword.trim();
103889
+ return "\u2014";
103890
+ }
103891
+ function mapNegativeKeywordRows(rows) {
103892
+ const seen = /* @__PURE__ */ new Set();
103893
+ const mapped = [];
103894
+ for (const row of rows) {
103895
+ const id = row.id == null ? "" : String(row.id);
103896
+ if (id && seen.has(id)) continue;
103897
+ if (id) seen.add(id);
103898
+ const level = String(row.negativeLevel ?? "").trim();
103899
+ const campaignName = String(row.campaignName ?? "").trim();
103900
+ const adGroupName = String(row.adGroupName ?? "").trim();
103901
+ mapped.push({
103902
+ keyword: negativeKeywordText(row),
103903
+ matchType: String(row.matchTypeV2 ?? row.matchType ?? "\u2014"),
103904
+ level: level || "\u2014",
103905
+ levelZh: NEGATIVE_LEVEL_ZH[level] ?? (level || "\u2014"),
103906
+ campaignName: campaignName || "\u2014",
103907
+ adGroupName: adGroupName || "\u2014"
103908
+ });
103909
+ }
103910
+ return mapped;
103911
+ }
103870
103912
  function dateTagFromRange(dateRange) {
103871
103913
  if (!dateRange || dateRange.mode !== "range") return void 0;
103872
103914
  return `${dateRange.start.replace(/-/g, "")}-${dateRange.end.replace(/-/g, "")}`;
@@ -104095,6 +104137,9 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
104095
104137
  );
104096
104138
  }
104097
104139
  }
104140
+ if (sectionMap.has("negative-keywords")) {
104141
+ tables.negativeKeywords = mapNegativeKeywordRows(items(sectionMap.get("negative-keywords")));
104142
+ }
104098
104143
  return {
104099
104144
  ...payload,
104100
104145
  meta,
@@ -104103,7 +104148,7 @@ async function mergeGoogleAnalysisSnapshotIntoReport(payload, snapshotDir) {
104103
104148
  tables
104104
104149
  };
104105
104150
  }
104106
- var REPORT_MERGE_SECTIONS;
104151
+ var REPORT_MERGE_SECTIONS, NEGATIVE_LEVEL_ZH;
104107
104152
  var init_merge_snapshot = __esm({
104108
104153
  "src/commands/google-analysis/merge-snapshot.ts"() {
104109
104154
  "use strict";
@@ -104116,8 +104161,15 @@ var init_merge_snapshot = __esm({
104116
104161
  "devices",
104117
104162
  "geographic",
104118
104163
  "audience",
104119
- "keywords"
104164
+ "keywords",
104165
+ "negative-keywords"
104120
104166
  ];
104167
+ NEGATIVE_LEVEL_ZH = {
104168
+ Account: "\u8D26\u6237",
104169
+ Campaign: "\u7CFB\u5217",
104170
+ AdGroup: "\u5E7F\u544A\u7EC4",
104171
+ "Ad group": "\u5E7F\u544A\u7EC4"
104172
+ };
104121
104173
  }
104122
104174
  });
104123
104175
 
@@ -104992,6 +105044,16 @@ function validateGooglePeriodReportContent(data) {
104992
105044
  "\u81F3\u5C11\u4E00\u9879\u987B\u4E3A\u6709\u6548\u6570\u5B57\uFF1B\u8BF7\u52A0 --snapshot-dir \u4ECE google-analysis \u5FEB\u7167\u5408\u5E76\uFF08\u52FF\u624B\u586B HTML\uFF09"
104993
105045
  );
104994
105046
  }
105047
+ const negativeRows = asRecord22(data.tables)?.negativeKeywords;
105048
+ if (!Array.isArray(negativeRows)) {
105049
+ pushMissing2(
105050
+ missing,
105051
+ "tables-negative-keywords",
105052
+ "\u2468\u5426\u8BCD",
105053
+ "tables.negativeKeywords",
105054
+ "\u987B\u4E3A\u6570\u7EC4\uFF080 \u6761\u4E5F\u8981\u662F []\uFF09\u3002\u9ED8\u8BA4\u62C9\u6570\u52A0\u4E0A --sections negative-keywords\uFF0Crender \u5E26 --snapshot-dir"
105055
+ );
105056
+ }
104995
105057
  const sectionAnalysisCounts = {};
104996
105058
  const sectionSuggestionCounts = {};
104997
105059
  for (const key of SECTION_KEYS) {
@@ -105028,7 +105090,7 @@ function validateGooglePeriodReportContent(data) {
105028
105090
  pushMissing2(
105029
105091
  missing,
105030
105092
  "narrative-recommendations",
105031
- "\u2468\u4F18\u5316\u5EFA\u8BAE",
105093
+ "\u2469\u4F18\u5316\u5EFA\u8BAE",
105032
105094
  "narrative.recommendations",
105033
105095
  `\u22653 \u6761\u8DE8\u7EF4\u5EA6\u4F18\u5316\u5EFA\u8BAE\uFF08\u5F53\u524D ${recommendations.length} \u6761\uFF09`
105034
105096
  );
@@ -105067,7 +105129,8 @@ var init_report_content = __esm({
105067
105129
  "devices",
105068
105130
  "geographic",
105069
105131
  "audience",
105070
- "keywords"
105132
+ "keywords",
105133
+ "negativeKeywords"
105071
105134
  ];
105072
105135
  SECTION_LABELS = {
105073
105136
  overview: "\u2460\u6267\u884C\u6458\u8981",
@@ -105077,7 +105140,8 @@ var init_report_content = __esm({
105077
105140
  devices: "\u2464\u8BBE\u5907\u5206\u5E03",
105078
105141
  geographic: "\u2465\u5730\u57DF\u5206\u5E03",
105079
105142
  audience: "\u2466\u53D7\u4F17\u5206\u5E03",
105080
- keywords: "\u2467\u5173\u952E\u8BCD\u8868\u73B0"
105143
+ keywords: "\u2467\u5173\u952E\u8BCD\u8868\u73B0",
105144
+ negativeKeywords: "\u2468\u5426\u8BCD"
105081
105145
  };
105082
105146
  }
105083
105147
  });
@@ -105165,7 +105229,7 @@ async function runGoogleAnalysisRender(opts) {
105165
105229
  \u2705 Google Ads ${cover}\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
105166
105230
  `);
105167
105231
  console.log(
105168
- ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C8 \u4E2A\u6570\u636E\u7EF4\u5206\u6790+\u5EFA\u8BAE\u5747\u5DF2\u9F50\u5168
105232
+ ` \u5FC5\u542B\u5B57\u6BB5\u5DF2\u5168\u90E8\u8986\u76D6\uFF5C\u6267\u884C\u6458\u8981 ${stats.executiveSummaryCount} \u6BB5\uFF5C\u4F18\u5316\u5EFA\u8BAE ${stats.recommendationCount} \u6761\uFF5C9 \u4E2A\u6570\u636E\u7EF4\u5206\u6790+\u5EFA\u8BAE\u5747\u5DF2\u9F50\u5168
105169
105233
  `
105170
105234
  );
105171
105235
  console.log(
@@ -122330,6 +122394,119 @@ function assertPmaxImageSlotsResolved(slots, requiredKinds = ["marketing", "squa
122330
122394
  import { readFileSync as readFileSync11 } from "fs";
122331
122395
  init_shared();
122332
122396
 
122397
+ // src/commands/ad/pmax-create-http-error.ts
122398
+ function normalizePmaxResumeId(value) {
122399
+ if (value == null) return null;
122400
+ const text3 = String(value).trim();
122401
+ if (!text3 || text3.toLowerCase() === "null") return null;
122402
+ return text3;
122403
+ }
122404
+ function quoteLongResumeIds(rawBody) {
122405
+ return rawBody.replace(
122406
+ /"(budgetId|campaignId|assetGroupId)"\s*:\s*(-?\d+)/g,
122407
+ '"$1":"$2"'
122408
+ );
122409
+ }
122410
+ function parsePmaxCreateFailureBody(rawBody) {
122411
+ const text3 = rawBody.trim();
122412
+ if (!text3.startsWith("{")) return null;
122413
+ let parsed;
122414
+ try {
122415
+ parsed = JSON.parse(quoteLongResumeIds(text3));
122416
+ } catch {
122417
+ return null;
122418
+ }
122419
+ if (!parsed || typeof parsed !== "object") return null;
122420
+ const rec = parsed;
122421
+ if (typeof rec["message"] !== "string" || !rec["message"].trim()) return null;
122422
+ if (!("budgetId" in rec) && !("campaignId" in rec) && !("assetGroupId" in rec)) return null;
122423
+ return {
122424
+ message: rec["message"].trim(),
122425
+ budgetId: normalizePmaxResumeId(rec["budgetId"]),
122426
+ campaignId: normalizePmaxResumeId(rec["campaignId"]),
122427
+ assetGroupId: normalizePmaxResumeId(rec["assetGroupId"])
122428
+ };
122429
+ }
122430
+ function readHttpRawBody(err) {
122431
+ if (!err || typeof err !== "object" || !("rawBody" in err)) return "";
122432
+ const raw = err.rawBody;
122433
+ return typeof raw === "string" ? raw : "";
122434
+ }
122435
+ function sameResumeId(left, right) {
122436
+ const a = left?.trim() || null;
122437
+ const b = right?.trim() || null;
122438
+ return a === b;
122439
+ }
122440
+ function pmaxCreateRetryAdvice(requested, failure) {
122441
+ const got = [failure.budgetId, failure.campaignId, failure.assetGroupId];
122442
+ if (!got.some(Boolean)) return "fix-without-ids";
122443
+ const sent = [
122444
+ normalizePmaxResumeId(requested.budgetId),
122445
+ normalizePmaxResumeId(requested.campaignId),
122446
+ normalizePmaxResumeId(requested.assetGroupId)
122447
+ ];
122448
+ if (sent.every((id, index) => sameResumeId(id, got[index]))) return "stop-same-step";
122449
+ for (let index = 0; index < sent.length; index++) {
122450
+ const previous = sent[index]?.trim() || null;
122451
+ if (previous && previous !== got[index]) return "stop-same-step";
122452
+ }
122453
+ return "resume";
122454
+ }
122455
+ function pmaxCreateResumeError(cfg) {
122456
+ const budgetId = normalizePmaxResumeId(cfg.budgetId) ?? "";
122457
+ const campaignId = normalizePmaxResumeId(cfg.campaignId) ?? "";
122458
+ const assetGroupId = normalizePmaxResumeId(cfg.assetGroupId) ?? "";
122459
+ if (campaignId && !budgetId) return "BudgetId is required when CampaignId is provided.";
122460
+ if (assetGroupId && (!budgetId || !campaignId)) {
122461
+ return "CampaignId is required when AssetGroupId is provided.";
122462
+ }
122463
+ const brandGuidelinesOn = cfg.brandGuidelinesEnabled !== false;
122464
+ if (brandGuidelinesOn && campaignId && !assetGroupId) {
122465
+ return "AssetGroupId is required when CampaignId is provided and brand guidelines are enabled.";
122466
+ }
122467
+ return null;
122468
+ }
122469
+ function classifyPmaxCreateHttpError(raw) {
122470
+ const msg = raw.trim() || "\u672A\u77E5\u9519\u8BEF";
122471
+ const lower = msg.toLowerCase();
122472
+ const hints = [];
122473
+ if (/^HTTP 400\b/i.test(msg) || lower.includes("bad request")) {
122474
+ if (/budget|日预算|budgetamount|micros/i.test(msg)) {
122475
+ hints.push("\u68C0\u67E5 campaign.budget / \u65E5\u9884\u7B97\u91D1\u989D\u4E0E\u5E01\u79CD\u662F\u5426\u5408\u7406");
122476
+ }
122477
+ if (/bidding|target.?cpa|target.?roas|maximize|出价/i.test(msg)) {
122478
+ hints.push("\u68C0\u67E5 BiddingStrategyTypeV2 \u4E0E\u914D\u5957\u91D1\u989D\u5B57\u6BB5\uFF08tCPA/tROAS\uFF09\u662F\u5426\u9F50\u5168\u4E14\u517C\u5BB9");
122479
+ }
122480
+ if (/image|asset|marketing|logo|图片/i.test(msg)) {
122481
+ hints.push("\u68C0\u67E5\u6A2A\u56FE/\u65B9\u56FE/Logo \u662F\u5426\u5DF2\u4E0A\u4F20\u6210\u529F\u3001\u89C4\u683C\u662F\u5426\u8FBE\u6807\uFF08\u5148\u8DD1 ad pmax-validate\uFF09");
122482
+ }
122483
+ if (/final.?url|landing|落地页|url/i.test(msg)) {
122484
+ hints.push("\u68C0\u67E5 finalUrls \u662F\u5426\u4E3A\u53EF\u8BBF\u95EE\u7684 http(s) \u843D\u5730\u9875");
122485
+ }
122486
+ if (/name|duplicate|已存在|同名/i.test(msg)) {
122487
+ hints.push("\u7CFB\u5217\u540D\u53EF\u80FD\u91CD\u590D\uFF1B\u6362\u4E00\u4E2A name \u518D\u8BD5");
122488
+ }
122489
+ if (/geo|location|language|地域|语言/i.test(msg)) {
122490
+ hints.push("\u68C0\u67E5 geo / language id \u662F\u5426\u6765\u81EA ad geo resolve / \u6587\u6863\u679A\u4E3E");
122491
+ }
122492
+ if (hints.length === 0) {
122493
+ hints.push(
122494
+ "\u5E38\u89C1\uFF1A\u9884\u7B97/\u51FA\u4EF7\u4E0D\u517C\u5BB9\u3001\u56FE\u7247\u8D44\u4EA7\u65E0\u6548\u3001finalUrl \u65E0\u6548\u3001\u7CFB\u5217\u91CD\u540D\uFF1B\u5148 ad pmax-validate\uFF0C\u4ECD\u5931\u8D25\u52A0 --verbose \u770B\u7F51\u5173\u539F\u6587"
122495
+ );
122496
+ }
122497
+ } else if (/^HTTP 403\b/i.test(msg)) {
122498
+ hints.push("\u8D26\u6237\u53EF\u80FD\u65E0\u6743\u521B\u5EFA\uFF1B\u5148 account check-access -a <id>\uFF0C\u786E\u8BA4\u5728\u5F53\u524D\u4E1D\u8DEF\u8D5E\u8D26\u53F7\u4E0B");
122499
+ } else if (/^HTTP 401\b/i.test(msg)) {
122500
+ hints.push("\u9274\u6743\u5931\u6548\uFF1B\u6267\u884C siluzan-tso login \u540E\u91CD\u8BD5");
122501
+ }
122502
+ if (hints.length === 0) return msg;
122503
+ return `${msg}
122504
+ \u63D0\u793A\uFF1A${hints.join("\uFF1B")}`;
122505
+ }
122506
+ function pmaxCreateFollowUpFailed(input) {
122507
+ return Boolean(input.uploadError || input.linkError || input.extensionsAllOk === false);
122508
+ }
122509
+
122333
122510
  // src/commands/ad/pmax-campaign-extensions.ts
122334
122511
  init_auth();
122335
122512
  var PMAX_POLICY_CALLOUT_MIN = 20;
@@ -122912,7 +123089,12 @@ function parseLocationLanguageIds(items3) {
122912
123089
  id: Number(item.id)
122913
123090
  }));
122914
123091
  }
123092
+ function trimmedId(value) {
123093
+ return normalizePmaxResumeId(value) ?? void 0;
123094
+ }
122915
123095
  function buildPmaxCreateApiBody(cfg, imageSlots) {
123096
+ const resumeError = pmaxCreateResumeError(cfg);
123097
+ if (resumeError) throw new Error(resumeError);
122916
123098
  assertPmaxImageSlotsResolved(imageSlots);
122917
123099
  const body = {
122918
123100
  name: cfg.name.trim(),
@@ -122943,6 +123125,12 @@ function buildPmaxCreateApiBody(cfg, imageSlots) {
122943
123125
  if (cfg.brandGuidelinesEnabled !== void 0) {
122944
123126
  body.brandGuidelinesEnabled = cfg.brandGuidelinesEnabled;
122945
123127
  }
123128
+ const budgetId = trimmedId(cfg.budgetId);
123129
+ const campaignId = trimmedId(cfg.campaignId);
123130
+ const assetGroupId = trimmedId(cfg.assetGroupId);
123131
+ if (budgetId) body.budgetId = budgetId;
123132
+ if (campaignId) body.campaignId = campaignId;
123133
+ if (assetGroupId) body.assetGroupId = assetGroupId;
122946
123134
  return body;
122947
123135
  }
122948
123136
  function buildPmaxCreateUrl(googleApiUrl, accountId) {
@@ -123344,51 +123532,13 @@ async function linkPmaxVideoAfterCreate(opts) {
123344
123532
  }
123345
123533
  }
123346
123534
 
123347
- // src/commands/ad/pmax-create-http-error.ts
123348
- function classifyPmaxCreateHttpError(raw) {
123349
- const msg = raw.trim() || "\u672A\u77E5\u9519\u8BEF";
123350
- const lower = msg.toLowerCase();
123351
- const hints = [];
123352
- if (/^HTTP 400\b/i.test(msg) || lower.includes("bad request")) {
123353
- if (/budget|日预算|budgetamount|micros/i.test(msg)) {
123354
- hints.push("\u68C0\u67E5 campaign.budget / \u65E5\u9884\u7B97\u91D1\u989D\u4E0E\u5E01\u79CD\u662F\u5426\u5408\u7406");
123355
- }
123356
- if (/bidding|target.?cpa|target.?roas|maximize|出价/i.test(msg)) {
123357
- hints.push("\u68C0\u67E5 BiddingStrategyTypeV2 \u4E0E\u914D\u5957\u91D1\u989D\u5B57\u6BB5\uFF08tCPA/tROAS\uFF09\u662F\u5426\u9F50\u5168\u4E14\u517C\u5BB9");
123358
- }
123359
- if (/image|asset|marketing|logo|图片/i.test(msg)) {
123360
- hints.push("\u68C0\u67E5\u6A2A\u56FE/\u65B9\u56FE/Logo \u662F\u5426\u5DF2\u4E0A\u4F20\u6210\u529F\u3001\u89C4\u683C\u662F\u5426\u8FBE\u6807\uFF08\u5148\u8DD1 ad pmax-validate\uFF09");
123361
- }
123362
- if (/final.?url|landing|落地页|url/i.test(msg)) {
123363
- hints.push("\u68C0\u67E5 finalUrls \u662F\u5426\u4E3A\u53EF\u8BBF\u95EE\u7684 http(s) \u843D\u5730\u9875");
123364
- }
123365
- if (/name|duplicate|已存在|同名/i.test(msg)) {
123366
- hints.push("\u7CFB\u5217\u540D\u53EF\u80FD\u91CD\u590D\uFF1B\u6362\u4E00\u4E2A name \u518D\u8BD5");
123367
- }
123368
- if (/geo|location|language|地域|语言/i.test(msg)) {
123369
- hints.push("\u68C0\u67E5 geo / language id \u662F\u5426\u6765\u81EA ad geo resolve / \u6587\u6863\u679A\u4E3E");
123370
- }
123371
- if (hints.length === 0) {
123372
- hints.push(
123373
- "\u5E38\u89C1\uFF1A\u9884\u7B97/\u51FA\u4EF7\u4E0D\u517C\u5BB9\u3001\u56FE\u7247\u8D44\u4EA7\u65E0\u6548\u3001finalUrl \u65E0\u6548\u3001\u7CFB\u5217\u91CD\u540D\uFF1B\u5148 ad pmax-validate\uFF0C\u4ECD\u5931\u8D25\u52A0 --verbose \u770B\u7F51\u5173\u539F\u6587"
123374
- );
123375
- }
123376
- } else if (/^HTTP 403\b/i.test(msg)) {
123377
- hints.push("\u8D26\u6237\u53EF\u80FD\u65E0\u6743\u521B\u5EFA\uFF1B\u5148 account check-access -a <id>\uFF0C\u786E\u8BA4\u5728\u5F53\u524D\u4E1D\u8DEF\u8D5E\u8D26\u53F7\u4E0B");
123378
- } else if (/^HTTP 401\b/i.test(msg)) {
123379
- hints.push("\u9274\u6743\u5931\u6548\uFF1B\u6267\u884C siluzan-tso login \u540E\u91CD\u8BD5");
123380
- }
123381
- if (hints.length === 0) return msg;
123382
- return `${msg}
123383
- \u63D0\u793A\uFF1A${hints.join("\uFF1B")}`;
123384
- }
123385
-
123386
123535
  // src/commands/ad/pmax-create.ts
123387
123536
  function printVideoLinkRecoveryHint(accountId, assetGroupId, campaignId, youtubeTarget) {
123388
123537
  const cid = campaignId != null ? ` --campaign-id ${campaignId}` : "";
123389
123538
  const yt = youtubeTarget ? ` --youtube "${youtubeTarget}"` : ` --video-path "<path>"`;
123390
123539
  console.error(
123391
123540
  ` \u53EF\u624B\u52A8\uFF1Asiluzan-tso ad pmax-youtube-link -a ${accountId} --asset-group-id ${assetGroupId}${cid}${yt}
123541
+ \u6D3B\u52A8\u5DF2\u7ECF\u521B\u5EFA\u6210\u529F\uFF0C\u4E0D\u8981\u518D\u6267\u884C pmax-create\u3002
123392
123542
  `
123393
123543
  );
123394
123544
  }
@@ -123471,6 +123621,39 @@ async function runAdPmaxCreate(opts) {
123471
123621
  opts.verbose
123472
123622
  );
123473
123623
  } catch (err) {
123624
+ const failure = parsePmaxCreateFailureBody(readHttpRawBody(err));
123625
+ if (failure) {
123626
+ const advice = pmaxCreateRetryAdvice(cfg, failure);
123627
+ console.error(`
123628
+ \u274C \u521B\u5EFA PMax \u5E7F\u544A\u7CFB\u5217\u5931\u8D25\uFF1A${failure.message}`);
123629
+ console.error(` budgetId\uFF1A${failure.budgetId ?? "null"}`);
123630
+ console.error(` campaignId\uFF1A${failure.campaignId ?? "null"}`);
123631
+ console.error(` assetGroupId\uFF1A${failure.assetGroupId ?? "null"}`);
123632
+ if (advice === "resume") {
123633
+ console.error(
123634
+ " \u7EED\u8DD1\uFF1A\u628A\u975E\u7A7A\u7F16\u53F7\u6309\u540C\u540D\u5199\u56DE\u8FD9\u4EFD JSON\uFF0C\u5176\u4F59\u5185\u5BB9\u4E0D\u8981\u6539\uFF0C\u518D\u6267\u884C\u4E00\u6B21 pmax-create\u3002\u7F16\u53F7\u6309\u9884\u7B97\u3001\u7CFB\u5217\u3001\u7D20\u6750\u7EC4\u5E26\u3002\u540C\u4E00\u6B65\u518D\u5931\u8D25\u5C31\u505C\u3002\n"
123635
+ );
123636
+ } else if (advice === "stop-same-step") {
123637
+ console.error(" \u540C\u4E00\u6B65\u518D\u5931\u8D25\uFF0C\u505C\u6B62\u3002\u4E0D\u8981\u518D\u6267\u884C pmax-create\u3002\n");
123638
+ } else {
123639
+ console.error(" \u8FD9\u6B21\u8FD8\u6CA1\u6709\u5EFA\u6210\u9884\u7B97\u3001\u7CFB\u5217\u6216\u7D20\u6750\u7EC4\u3002\u4FEE\u6B63\u539F\u56E0\u540E\u7528\u539F JSON \u518D\u63D0\u4EA4\uFF0C\u4E0D\u8981\u586B\u8FD9\u4E09\u4E2A\u7F16\u53F7\u3002\n");
123640
+ }
123641
+ await emitCliJsonOrSnapshot(opts, {
123642
+ section: `ad-pmax-create-${accountId}`,
123643
+ commandLabel: "ad pmax-create",
123644
+ commandHint: accountId,
123645
+ payload: { ok: false, retryCreate: advice === "resume", ...failure },
123646
+ idSuffix: accountId,
123647
+ summaryExtras: {
123648
+ ok: false,
123649
+ retryCreate: advice === "resume",
123650
+ budgetId: failure.budgetId,
123651
+ campaignId: failure.campaignId,
123652
+ assetGroupId: failure.assetGroupId
123653
+ }
123654
+ });
123655
+ process.exit(1);
123656
+ }
123474
123657
  const raw = err instanceof Error ? err.message : String(err);
123475
123658
  console.error(`
123476
123659
  \u274C \u521B\u5EFA PMax \u5E7F\u544A\u7CFB\u5217\u5931\u8D25\uFF1A${classifyPmaxCreateHttpError(raw)}
@@ -123534,6 +123717,7 @@ async function runAdPmaxCreate(opts) {
123534
123717
  void 0
123535
123718
  );
123536
123719
  }
123720
+ console.error(" \u6D3B\u52A8\u5DF2\u7ECF\u521B\u5EFA\u6210\u529F\uFF0C\u4E0D\u8981\u518D\u6267\u884C pmax-create\u3002\n");
123537
123721
  } else if (videoResult.linkError) {
123538
123722
  console.error(`
123539
123723
  \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46 YouTube \u94FE\u63A5\u5931\u8D25\uFF1A${videoResult.linkError}`);
@@ -123545,6 +123729,7 @@ async function runAdPmaxCreate(opts) {
123545
123729
  videoResult.youtubeTarget
123546
123730
  );
123547
123731
  }
123732
+ console.error(" \u6D3B\u52A8\u5DF2\u7ECF\u521B\u5EFA\u6210\u529F\uFF0C\u4E0D\u8981\u518D\u6267\u884C pmax-create\u3002\n");
123548
123733
  } else if (videoResult.skippedReason) {
123549
123734
  console.error(`
123550
123735
  \u26A0\uFE0F \u6D3B\u52A8\u5DF2\u521B\u5EFA\uFF0C\u4F46\u89C6\u9891\u672A\u94FE\u63A5\uFF1A${videoResult.skippedReason}
@@ -123557,6 +123742,7 @@ async function runAdPmaxCreate(opts) {
123557
123742
  }
123558
123743
  console.error(
123559
123744
  ` \u53EF\u624B\u52A8\u8865\u6302\uFF1Asiluzan-tso ad extension callout|snippet|lead-form -a ${accountId} --campaign-id ${campaignId} \u2026
123745
+ \u6D3B\u52A8\u5DF2\u7ECF\u521B\u5EFA\u6210\u529F\uFF0C\u4E0D\u8981\u518D\u6267\u884C pmax-create\u3002
123560
123746
  `
123561
123747
  );
123562
123748
  } else if (extensionsResult?.allOk && extensionsResult.callouts.length + extensionsResult.structuredSnippets.length + extensionsResult.sitelinks.length + (extensionsResult.leadForm ? 1 : 0) + (extensionsResult.whatsapp ? 1 : 0) > 0) {
@@ -123564,12 +123750,22 @@ async function runAdPmaxCreate(opts) {
123564
123750
  console.log("\n\u2705 Campaign \u9644\u52A0\u8D44\u4EA7\u5DF2\u6302\u8F7D");
123565
123751
  }
123566
123752
  }
123753
+ const followUpFailed = pmaxCreateFollowUpFailed({
123754
+ uploadError: videoResult.uploadError,
123755
+ linkError: videoResult.linkError,
123756
+ extensionsAllOk: extensionsResult ? extensionsResult.allOk : void 0
123757
+ });
123758
+ if (followUpFailed) {
123759
+ payload["created"] = true;
123760
+ payload["retryCreate"] = false;
123761
+ }
123567
123762
  if (await emitCliJsonOrSnapshot(opts, {
123568
123763
  section: `ad-pmax-create-${accountId}`,
123569
123764
  commandLabel: "ad pmax-create",
123570
123765
  commandHint: accountId,
123571
123766
  payload,
123572
- idSuffix: accountId
123767
+ idSuffix: accountId,
123768
+ summaryExtras: followUpFailed ? { created: true, retryCreate: false } : void 0
123573
123769
  })) {
123574
123770
  return;
123575
123771
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.59-beta.2",
4
- "publishedAt": 1793093338610
3
+ "version": "1.1.59",
4
+ "publishedAt": 1793103761036
5
5
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "siluzan-tso/google-period-report.schema.json",
4
4
  "title": "GooglePeriodReport",
5
- "description": "Google Ads 周期分析报告 JSON(对齐 google-period-report.md 9 章:8 个数据维 + 优化建议;供 google-analysis render 注入 HTML)。Agent 只需撰写 narrative;kpis/charts/tables 由 --snapshot-dir 从 google-analysis 快照合并。render 校验要求合并后 kpis.cost/impressions/clicks 至少一项为有效数字,否则拒绝出 HTML(避免执行摘要卡片全「—」)。",
5
+ "description": "Google Ads 周期分析报告 JSON(对齐 google-period-report.md 10 章:9 个数据维 + 优化建议;供 google-analysis render 注入 HTML)。Agent 只需撰写 narrative;kpis/charts/tables 由 --snapshot-dir 从 google-analysis 快照合并。render 校验要求合并后 kpis.cost/impressions/clicks 至少一项为有效数字,且 tables.negativeKeywords 为数组(0 条也要是 []),否则拒绝出 HTML。",
6
6
  "type": "object",
7
7
  "required": ["meta", "narrative"],
8
8
  "properties": {
@@ -93,7 +93,8 @@
93
93
  "devices": { "$ref": "#/$defs/deviceRows" },
94
94
  "geographic": { "$ref": "#/$defs/geographicRows" },
95
95
  "audience": { "$ref": "#/$defs/audienceRows" },
96
- "keywords": { "$ref": "#/$defs/keywordRows" }
96
+ "keywords": { "$ref": "#/$defs/keywordRows" },
97
+ "negativeKeywords": { "$ref": "#/$defs/negativeKeywordRows" }
97
98
  }
98
99
  },
99
100
  "narrative": {
@@ -109,7 +110,7 @@
109
110
  },
110
111
  "sections": {
111
112
  "type": "object",
112
- "description": "8 个数据维度各自的分析+建议(对应报告 §1~§8);每维度 analysis ≥2 条、suggestions ≥1 条",
113
+ "description": "9 个数据维度各自的分析+建议(对应报告 §1~§9);每维度 analysis ≥2 条、suggestions ≥1 条",
113
114
  "required": [
114
115
  "overview",
115
116
  "dailyTrend",
@@ -118,7 +119,8 @@
118
119
  "devices",
119
120
  "geographic",
120
121
  "audience",
121
- "keywords"
122
+ "keywords",
123
+ "negativeKeywords"
122
124
  ],
123
125
  "properties": {
124
126
  "overview": { "$ref": "#/$defs/sectionNarrative" },
@@ -128,12 +130,13 @@
128
130
  "devices": { "$ref": "#/$defs/sectionNarrative" },
129
131
  "geographic": { "$ref": "#/$defs/sectionNarrative" },
130
132
  "audience": { "$ref": "#/$defs/sectionNarrative" },
131
- "keywords": { "$ref": "#/$defs/sectionNarrative" }
133
+ "keywords": { "$ref": "#/$defs/sectionNarrative" },
134
+ "negativeKeywords": { "$ref": "#/$defs/sectionNarrative" }
132
135
  }
133
136
  },
134
137
  "recommendations": {
135
138
  "type": "array",
136
- "description": "报告 §9 优化建议;≥3 条,可跨维度综合",
139
+ "description": "报告 §10 优化建议;≥3 条,可跨维度综合",
137
140
  "minItems": 3,
138
141
  "items": {
139
142
  "oneOf": [
@@ -249,6 +252,19 @@
249
252
  "costPerConversion": { "type": "number" }
250
253
  }
251
254
  },
252
- "keywordRows": { "type": "array", "items": { "$ref": "#/$defs/keywordRow" } }
255
+ "keywordRows": { "type": "array", "items": { "$ref": "#/$defs/keywordRow" } },
256
+ "negativeKeywordRow": {
257
+ "type": "object",
258
+ "description": "当前账户否词一行。0 条时数组为空,不要省略字段。",
259
+ "properties": {
260
+ "keyword": { "type": "string" },
261
+ "matchType": { "type": "string" },
262
+ "level": { "type": "string" },
263
+ "levelZh": { "type": "string" },
264
+ "campaignName": { "type": "string" },
265
+ "adGroupName": { "type": "string" }
266
+ }
267
+ },
268
+ "negativeKeywordRows": { "type": "array", "items": { "$ref": "#/$defs/negativeKeywordRow" } }
253
269
  }
254
270
  }
@@ -16,7 +16,8 @@
16
16
  "勿用 Search 的 ExtensionsForBatchJob;PMax 用 campaignExtensions(CLI 会尝试自动转换 CALLOUT/SNIPPET)",
17
17
  "Lead Gen/B2B 询盘方案:默认保留 campaignExtensions.leadForm(勿只写 callouts/snippets);用户明确不要表单才删 leadForm",
18
18
  "审查稿须单列「潜在客户表单」节(标题/描述/字段/privacyPolicyUrl);结构见 google-ads-pmax-launch-plan-template.md",
19
- "不需要任何附加资产时设 skipCampaignExtensions: true(勿仅删除 campaignExtensions 块)"
19
+ "不需要任何附加资产时设 skipCampaignExtensions: true(勿仅删除 campaignExtensions 块)",
20
+ "创建失败:只有 400 且编号变多时,才把非空 budgetId / campaignId / assetGroupId 写回本 JSON 再跑一次,内容不改。首次创建不填。已经 200 只是扩展失败就不要再 pmax-create"
20
21
  ]
21
22
  },
22
23
 
@@ -22,6 +22,7 @@
22
22
  | Lead Gen 方案 | **默认**在 `campaignExtensions` 含 **`leadForm`**(B2B/询盘/留资);仅 callouts/snippets 不算完整方案;用户明确不要才省略 |
23
23
  | 存量补表单 | 活动已创建时用 `ad extension lead-form`(见 `pmax-lead-form-template.md` 方式 B) |
24
24
  | 改已上线 PMax | 先 `ad pmax-get` 看 `_brandGuidelinesActive`;改品牌见 `pmax-api.md` § Brand Guidelines |
25
+ | 创建失败 | 只有创建接口 400,且编号比这次请求多,才把非空的 `budgetId` / `campaignId` / `assetGroupId` 写回同一份 JSON,其余内容不要改,再 `pmax-create` 一次。首次创建不填这三个字段。同一步再失败就停。已经 200、只是扩展或视频失败:不要再 `pmax-create` |
25
26
  | 列表复核 | `ad campaigns -a <id> --json-out ./snap`,`channelTypeV2` 应为 `PERFORMANCE_MAX` |
26
27
 
27
28
  ---
@@ -81,6 +82,9 @@ siluzan-tso ad campaigns -a <accountId> --json-out ./snap
81
82
  | `targetCpa_BidingAmount` | number | | `TARGET_CPA` 或带目标 CPA 的 `MAXIMIZE_CONVERSIONS` 时必填(**元**) |
82
83
  | `targetRoas` | number | | `TARGET_ROAS` 时必填(如 `2.5` = 250%) |
83
84
  | `brandGuidelinesEnabled` | boolean | | 省略 = 网关默认 `true`(Campaign 级 BG);`false` = 品牌在 AssetGroup(旧路径) |
85
+ | `budgetId` | string | | 上次创建失败后已经建成的预算编号。空着就新建。续跑时和下面两个编号一起写回,其余字段不要改 |
86
+ | `campaignId` | string | | 上次创建失败后已经建成的系列编号。必须和 `budgetId` 一起传 |
87
+ | `assetGroupId` | string | | 上次创建失败后已经建成的素材组编号。必须和 `budgetId`、`campaignId` 一起传。默认 BG 开启时不能只写到系列 |
84
88
  | `videoPath` | string | | YOUTUBE_VIDEO(可选):本地视频 ≥10s;与 `youtubeUrlOrId` 二选一,创建时最多 1 条;更多视频创建后用 `pmax-youtube-link` / `pmax-assets-update` 追加 |
85
89
  | `videoTitle` | string | | PyAPI 上传标题(默认文件名) |
86
90
  | `videoDescription` | string | | PyAPI 上传描述(可选) |
@@ -130,7 +134,17 @@ siluzan-tso ad campaigns -a <accountId> --json-out ./snap
130
134
  | -------------- | -------------------------------------------------- |
131
135
  | `campaignId` | 活动 ID |
132
136
  | `assetGroupId` | 首个资产组 ID |
133
- | `budgetId` | 日预算 ID(后续 PATCH 改预算时需与 `budget` 成对) |
137
+ | `budgetId` | 日预算 ID(后续 PATCH 改预算时需与 `budget` 成对;创建失败后续跑也用它) |
138
+
139
+ ---
140
+
141
+ ## 创建失败后续跑
142
+
143
+ `POST .../campaign/pmax` 失败时返回 **JSON**,不再是纯文本。`message` 是原因。`budgetId`、`campaignId`、`assetGroupId` 里非空的是已经建成的编号,没建成的是 `null`。
144
+
145
+ 把非空编号按同名写回**同一份** JSON,其余内容不要改,再执行一次 `pmax-create`。CLI 会原样转发这三个字段,不会改已有预算金额或系列名称。首次创建不要填它们。不带编号再提交会新建一套。编号按预算、系列、素材组带。默认品牌手册开启时不能只写 `campaignId`。同一步再失败就停(stdout 摘要 `retryCreate=false`)。
146
+
147
+ 创建接口已经 **200**,只是附加资产或视频没挂上:活动已经在,**不要再 `pmax-create`**。扩展用 `ad extension`,视频用 `ad pmax-youtube-link`。这时 stdout 摘要是 `created=true`、`retryCreate=false`。
134
148
 
135
149
  ---
136
150
 
@@ -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-ci.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google`;Yandex 当前没有对应充值界面) |
173
+ | `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `https://www.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-ci.siluzan.com/v3/foreign_trade/tso/manageAccounts` |
493
+ | **账户激活**(邀请他人激活 / 充值激活) | Google | `https://www.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-ci.siluzan.com/v3/foreign_trade/tso/manageAccounts
502
+ # https://www.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-ci.siluzan.com/v3/foreign_trade/tso/recharge/<page>?mediaType=<mediaType>`;丝路赞钱包:`https://www-ci.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet`(无媒体参数)。
129
+ 链接模式:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/<page>?mediaType=<mediaType>`;丝路赞钱包:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet`(无媒体参数)。
130
130
 
131
131
  | 充值类型 | `<page>` | 支持媒体(`mediaType` 参数) |
132
132
  | ------------------------- | --------------------- | ------------------------------------------- |