siluzan-tso-cli 1.1.62-beta.2 → 1.1.62-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.62-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.62-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -7570,6 +7570,7 @@ var init_phrases_en = __esm({
7570
7570
  "\u5C0F\u5E45\u4F18\u5316\u540E\u53EF\u76F4\u63A5\u6295\u653E\u5E7F\u544A": "Ready for ads after a few fixes",
7571
7571
  "\u53EF\u76F4\u63A5\u6295\u653E\u5E7F\u544A": "Ready for ads",
7572
7572
  "\u6025\u9700\u4F18\u5316\uFF01": "Needs urgent work",
7573
+ "\u672C\u6B21\u8BCA\u65AD\u672A\u5199\u5165\u884C\u52A8\u8BA1\u5212\u3002\u8BF7\u5728 diagnosis.json \u7684 priorityPlan \u4E2D\u6309\u672C\u7AD9\u672A\u6EE1\u5206\u9879\u7F16\u5199\u540E\u518D\u751F\u6210\u62A5\u544A\u3002": "This report has no action plan. Fill priorityPlan from this site's unfinished items, then render again.",
7573
7574
  "\u9700\u8981\u91CD\u70B9\u4F18\u5316": "Needs focused work",
7574
7575
  "\u7F51\u7AD9\u8868\u73B0\u4F18\u79C0\uFF0C\u8FDC\u9AD8\u4E8E\u884C\u4E1A\u5E73\u5747\u503C\uFF0C\u53EF\u653E\u5FC3\u5F00\u542F\u5E7F\u544A\u6295\u653E": "The site is well above the industry average and is ready for ads",
7575
7576
  "\u7F51\u7AD9\u52A0\u8F7D\u901F\u5EA6\u5FEB\uFF0C\u9875\u9762\u54CD\u5E94\u53CA\u65F6\uFF0C\u8FBE\u5230\u884C\u4E1A\u9886\u5148\u6C34\u5E73": "The site loads quickly and is ahead of the industry",
@@ -115606,8 +115607,10 @@ function findKeywordEditMismatches(requested, returned, changed) {
115606
115607
  );
115607
115608
  }
115608
115609
  if (changed.maxCpc !== void 0 && typeof returned["maxCPC"] === "number" && returned["maxCPC"] !== Number(requested["maxCPC"])) {
115610
+ const expectedCents = Number(requested["maxCPC"]);
115611
+ const actualCents = returned["maxCPC"];
115609
115612
  mismatches.push(
115610
- `maxCPC\uFF08\u5206\uFF09\uFF1A\u671F\u671B ${String(requested["maxCPC"])}\uFF0C\u5B9E\u9645 ${String(returned["maxCPC"])}`
115613
+ `maxCPC\uFF1A\u671F\u671B ${expectedCents / 100} \u5143\uFF0C\u5217\u8868\u4ECD\u662F ${actualCents / 100} \u5143\u3002\u5173\u952E\u8BCD\u5217\u8868\u6309\u8D26\u6237\u548C\u65E5\u671F\u7F13\u5B58\u7EA6 60 \u79D2\uFF0C\u4E14\u6CA1\u6709 --fresh\uFF0C\u8FD9\u6B21\u56DE\u8BFB\u53EF\u80FD\u4ECD\u662F\u5199\u5165\u524D\u7684\u503C\u3002\u4E0D\u8981\u7ACB\u523B\u91CD\u8DD1 keyword-edit\uFF0C\u4E5F\u4E0D\u8981\u5220\u8BCD\u91CD\u5EFA\u3002\u7EA6 60 \u79D2\u540E\u7528 ad keywords --json-out \u770B\u8BE5 id \u7684 maxCPCYuan\uFF0C\u5DF2\u7ECF\u662F\u76EE\u6807\u503C\u5C31\u5F53\u6210\u529F\u3002`
115611
115614
  );
115612
115615
  }
115613
115616
  if (changed.status !== void 0 && typeof returned["userStatusV2"] === "string" && str3(returned["userStatusV2"]).toLowerCase() !== changed.status.toLowerCase()) {
@@ -117350,10 +117353,11 @@ async function runAdCampaigns(opts) {
117350
117353
  }
117351
117354
  const rawItems = data.data ?? [];
117352
117355
  const items3 = rawItems.map((item) => {
117353
- const budgetYuan = toDisplayMoney(item.budget);
117354
117356
  return {
117355
117357
  ...item,
117356
- budget: budgetYuan,
117358
+ budget: toDisplayMoney(item.budget),
117359
+ targetSpendBidCeilingYuan: toDisplayMoney(item["targetSpend_BidCeilingAmount"]),
117360
+ targetCpaAmountYuan: toDisplayMoney(item["targetCpa_BidingAmount"]),
117357
117361
  statusDisplay: formatGoogleCampaignListStatus(item)
117358
117362
  };
117359
117363
  });
@@ -121496,9 +121500,9 @@ function buildCampaignCreateStatusMarkdown(result, lang = "zh") {
121496
121500
  ``
121497
121501
  );
121498
121502
  }
121499
- const planItems = result.items.length > 0 ? result.items : result.missing.map((m) => ({ ...m, status: "missing" }));
121503
+ const planItems2 = result.items.length > 0 ? result.items : result.missing.map((m) => ({ ...m, status: "missing" }));
121500
121504
  for (const layer of LAYER_ORDER) {
121501
- const layerItems = planItems.filter((i) => i.layer === layer);
121505
+ const layerItems = planItems2.filter((i) => i.layer === layer);
121502
121506
  if (layerItems.length === 0) continue;
121503
121507
  if (layer === "keyword" || layer === "ad") {
121504
121508
  const byGroup = /* @__PURE__ */ new Map();
@@ -121627,11 +121631,11 @@ function formatBatchDiffGrouped(result) {
121627
121631
  ` \u9010\u9879\uFF1A\u2705 ${sc.ok} | \u274C ${sc.missing} | \u23ED ${sc.skipped}\uFF08\u7236\u7EA7\u7F3A\u5931\u8DF3\u8FC7\uFF09 | \u5408\u8BA1 ${result.items.length}`
121628
121632
  );
121629
121633
  console.log();
121630
- const planItems = result.items.length > 0 ? result.items : result.missing.map((m) => ({ ...m, status: "missing" }));
121631
- console.log(`\u2500\u2500 \u8BA1\u5212\u660E\u7EC6\uFF08\u5171 ${planItems.length} \u6761\uFF09\u2500\u2500`);
121634
+ const planItems2 = result.items.length > 0 ? result.items : result.missing.map((m) => ({ ...m, status: "missing" }));
121635
+ console.log(`\u2500\u2500 \u8BA1\u5212\u660E\u7EC6\uFF08\u5171 ${planItems2.length} \u6761\uFF09\u2500\u2500`);
121632
121636
  const configFile = result.configFile;
121633
121637
  for (const layer of LAYER_ORDER) {
121634
- const layerItems = planItems.filter((m) => m.layer === layer);
121638
+ const layerItems = planItems2.filter((m) => m.layer === layer);
121635
121639
  if (layerItems.length === 0) continue;
121636
121640
  if (layer === "keyword" || layer === "ad") {
121637
121641
  printPlanItemsByAdGroup(configFile, layer, layerItems);
@@ -126572,9 +126576,7 @@ function register21(program2) {
126572
126576
  verbose: opts.verbose
126573
126577
  });
126574
126578
  });
126575
- adCmd.command("campaign-edit").description(
126576
- "\u7F16\u8F91\u641C\u7D22\u5E7F\u544A\u7CFB\u5217\u3002\u540D\u79F0\u3001\u9884\u7B97\u3001\u51FA\u4EF7\u3001\u6295\u653E\u7F51\u7EDC\u90FD\u8D70\u5B57\u6BB5\u7EA7 PATCH\u3002\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879"
126577
- ).requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <campaignId>", "\u5E7F\u544A\u7CFB\u5217 ID").option("--name <name>", "\u65B0\u540D\u79F0").option(
126579
+ adCmd.command("campaign-edit").description("\u7F16\u8F91\u641C\u7D22\u5E7F\u544A\u7CFB\u5217\u3002\u540D\u79F0\u3001\u9884\u7B97\u3001\u51FA\u4EF7\u3001\u6295\u653E\u7F51\u7EDC\u90FD\u8D70\u5B57\u6BB5\u7EA7 PATCH\u3002\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <campaignId>", "\u5E7F\u544A\u7CFB\u5217 ID").option("--name <name>", "\u65B0\u540D\u79F0").option(
126578
126580
  "--budget <amount>",
126579
126581
  "\u65B0\u9884\u7B97\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 100 \u8868\u793A \xA5100\uFF1B\u652F\u6301\u5C0F\u6570\uFF1BCLI \u5185\u90E8\u81EA\u52A8 \xD7100 \u5199\u5165\u300C\u5206\u300D\u5B57\u6BB5\uFF09",
126580
126582
  parseFloat
@@ -126708,7 +126710,7 @@ function register21(program2) {
126708
126710
  '\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'
126709
126711
  ).option(
126710
126712
  "--max-cpc <n>",
126711
- "\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528 maxCPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 10 \u8868\u793A \xA510\uFF1BCLI \u5185\u90E8 \xD7100 \u5199\u5165\u300C\u5206\u300D\u5B57\u6BB5\uFF0C\u4E0E adgroup-edit --max-cpc \u540C\u53E3\u5F84\uFF1B0 \u8868\u793A\u6309\u5E73\u53F0/\u8BA1\u5212\u9ED8\u8BA4\uFF09"
126713
+ "\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 10 \u8868\u793A \xA510\uFF1BCLI \xD7100 \u5199\u5165\u30020 \u6E05\u7A7A\uFF0C\u56DE\u5230\u5E7F\u544A\u7EC4\u51FA\u4EF7\uFF0C\u4E0D\u662F\u5199\u6210 0 \u5143\uFF09"
126712
126714
  ).option("--final-url <url>", "\u5173\u952E\u8BCD\u7EA7\u6700\u7EC8\u5230\u8FBE\u7F51\u5740 finalURL").option("--status <Enabled|Paused>", "\u7528\u6237\u72B6\u6001\uFF08\u5199\u5165 userStatusV2\uFF0C\u4E0E Web \u5173\u952E\u8BCD\u5F00\u5173\u4E00\u81F4\uFF09").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(
126713
126715
  async (opts) => {
126714
126716
  if (opts.matchType && !["Broad", "Phrase", "Exact"].includes(opts.matchType)) {
@@ -134764,12 +134766,13 @@ function buildDiagnosisScaffold(signals) {
134764
134766
  };
134765
134767
  }
134766
134768
  );
134767
- const agentHint = `\u811A\u624B\u67B6\u5DF2\u9884\u586B\u6570\u636E\u4E0D\u53EF\u7528\u9879\uFF08needsAgent=false \u52FF\u6539\u6210\u865A\u6784\u5206\uFF09\u3002\u8BF7\u4EC5\u8865\u5168 needsAgent=true \u7684\u6761\u76EE\uFF1A\u4F9D\u636E signals \u5199 issue/suggestion/score/status\uFF1B\u8865\u5168\u540E\u5220\u6389\u5404 item \u7684 needsAgent/_scaffold \u5B57\u6BB5\uFF08\u6216\u4FDD\u7559\u4EA6\u53EF\uFF0Crender \u5FFD\u7565\u672A\u77E5\u5B57\u6BB5\uFF09\uFF0C\u8BBE\u7F6E ratingId \u4E0E coreIssuesIds\uFF0C\u518D\u6267\u884C website-diagnosis render\u3002 ${signals.dataAvailability.agentHint}`;
134769
+ const agentHint = `\u811A\u624B\u67B6\u5DF2\u9884\u586B\u6570\u636E\u4E0D\u53EF\u7528\u9879\uFF08needsAgent=false \u52FF\u6539\u6210\u865A\u6784\u5206\uFF09\u3002\u8BF7\u4EC5\u8865\u5168 needsAgent=true \u7684\u6761\u76EE\uFF1A\u4F9D\u636E signals \u5199 issue/suggestion/score/status\uFF1B\u8865\u5168\u540E\u5220\u6389\u5404 item \u7684 needsAgent/_scaffold \u5B57\u6BB5\uFF08\u6216\u4FDD\u7559\u4EA6\u53EF\uFF0Crender \u5FFD\u7565\u672A\u77E5\u5B57\u6BB5\uFF09\uFF0C\u8BBE\u7F6E ratingId \u4E0E coreIssuesIds\u3002priorityPlan \u5FC5\u987B\u6309\u672C\u7AD9\u672A\u6EE1\u5206\u9879\u548C coreIssuesIds \u5199\u6210\u9AD8/\u4E2D/\u4F4E\u4E09\u6863\uFF08\u53EF\u7F3A\u6863\uFF09\uFF0C\u6BCF\u6761\u542B direction\u3001task\u3001adImpact\uFF0C\u7981\u6B62\u5957\u7528\u901A\u7528\u884C\u52A8\u6E05\u5355\uFF1B\u4E3A\u7A7A\u65F6 render \u4F1A\u5931\u8D25\u3002\u7136\u540E\u518D\u6267\u884C website-diagnosis render\u3002 ${signals.dataAvailability.agentHint}`;
134768
134770
  return {
134769
134771
  url: signals.url,
134770
134772
  analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
134771
134773
  ratingId: "s3",
134772
134774
  coreIssuesIds: [],
134775
+ priorityPlan: [],
134773
134776
  modules,
134774
134777
  signals,
134775
134778
  ...warn ? { lighthouseWarning: warn } : {},
@@ -135225,6 +135228,42 @@ function mergeCollectLighthouse(data, collect) {
135225
135228
  ...collect.lighthouseWarning && !data.lighthouseWarning ? { lighthouseWarning: collect.lighthouseWarning } : {}
135226
135229
  };
135227
135230
  }
135231
+ var PRIORITY_RANKS = /* @__PURE__ */ new Set(["\u9AD8", "\u4E2D", "\u4F4E", "High", "Medium", "Low", "high", "medium", "low"]);
135232
+ function planItems(group) {
135233
+ const items3 = group.items ?? group["\u4F18\u5316\u9879"];
135234
+ return Array.isArray(items3) ? items3 : [];
135235
+ }
135236
+ function priorityPlanError(data) {
135237
+ const raw = data.priorityPlan ?? data["\u4F18\u5316\u4F18\u5148\u7EA7"];
135238
+ if (!Array.isArray(raw) || raw.length === 0) {
135239
+ return "diagnosis.json \u7F3A\u5C11 priorityPlan\u3002\u7B2C\u56DB\u8282\u6309\u672C\u7AD9\u60C5\u51B5\u7531 Agent \u7F16\u5199\uFF0C\u4E0D\u80FD\u7559\u7A7A\u3002\u53EA\u6536\u5F55\u672A\u6EE1\u5206\u9879\u548C coreIssuesIds\uFF1A\u9AD8 / \u4E2D / \u4F4E \u53EF\u7F3A\u6863\uFF0C\u6BCF\u6761\u5199 direction\u3001task\u3001adImpact\u3002";
135240
+ }
135241
+ let itemCount2 = 0;
135242
+ for (const group of raw) {
135243
+ if (!group || typeof group !== "object" || Array.isArray(group)) {
135244
+ return "priorityPlan \u7684\u6BCF\u4E00\u6863\u5FC5\u987B\u662F\u5BF9\u8C61\uFF0C\u542B priority \u4E0E items\u3002";
135245
+ }
135246
+ const row = group;
135247
+ const priority = String(row.priority ?? row["\u4F18\u5148\u7EA7"] ?? "").trim();
135248
+ if (!PRIORITY_RANKS.has(priority)) {
135249
+ return `priorityPlan.priority \u53EA\u63A5\u53D7 \u9AD8|\u4E2D|\u4F4E\uFF0C\u6536\u5230\u300C${priority || "(\u7A7A)"}\u300D\u3002`;
135250
+ }
135251
+ const items3 = planItems(row);
135252
+ if (items3.length === 0) return `priorityPlan \u91CC\u300C${priority}\u300D\u4E00\u6863\u6CA1\u6709 items\uFF0C\u7A7A\u6863\u4E0D\u8981\u8F93\u51FA\u3002`;
135253
+ for (const item of items3) {
135254
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
135255
+ return "priorityPlan.items \u7684\u6BCF\u4E00\u6761\u5FC5\u987B\u542B direction \u4E0E task\u3002";
135256
+ }
135257
+ const card = item;
135258
+ const direction = String(card.direction ?? card["\u4F18\u5316\u65B9\u5411"] ?? "").trim();
135259
+ const task = String(card.task ?? card["\u5177\u4F53\u4EFB\u52A1"] ?? "").trim();
135260
+ if (!direction || !task) return "priorityPlan \u6BCF\u6761\u90FD\u8981\u5199 direction \u548C task\uFF0C\u4E14\u4E0D\u80FD\u662F\u7A7A\u5B57\u7B26\u4E32\u3002";
135261
+ itemCount2 += 1;
135262
+ }
135263
+ }
135264
+ if (itemCount2 === 0) return "priorityPlan \u6CA1\u6709\u4EFB\u4F55\u884C\u52A8\u9879\u3002";
135265
+ return null;
135266
+ }
135228
135267
  function injectReportData(html, payload, lang) {
135229
135268
  const localized = localizeReportHtml(html, lang);
135230
135269
  const json = JSON.stringify(stampMetaLang(payload, lang)).replace(/</g, "\\u003c");
@@ -135242,6 +135281,13 @@ async function runWebsiteDiagnosisRender(opts) {
135242
135281
  const collectRaw = await readJsonFile(path21.resolve(opts.collectFile));
135243
135282
  data = mergeCollectLighthouse(data, asRecord5(collectRaw));
135244
135283
  }
135284
+ const planError = priorityPlanError(data);
135285
+ if (planError) {
135286
+ console.error(`
135287
+ \u274C ${planError}
135288
+ `);
135289
+ process.exit(1);
135290
+ }
135245
135291
  const lang = readReportLangOrExit({ flag: opts.lang, meta: data.meta });
135246
135292
  const templatePath = websiteDiagnosisReportTemplatePath();
135247
135293
  let html;
@@ -146682,10 +146728,8 @@ function playbookCodeFatal(cmd) {
146682
146728
  };
146683
146729
  const replacement = known[key];
146684
146730
  if (replacement) {
146685
- return fatal(
146686
- `\u274C \`${cmd}\` \u662F\u8DEF\u7531\u7F16\u53F7\uFF0C\u4E0D\u662F\u547D\u4EE4\u3002\u8BF7\u6539\u8DD1\uFF1A
146687
- ${replacement}`
146688
- );
146731
+ return fatal(`\u274C \`${cmd}\` \u662F\u8DEF\u7531\u7F16\u53F7\uFF0C\u4E0D\u662F\u547D\u4EE4\u3002\u8BF7\u6539\u8DD1\uFF1A
146732
+ ${replacement}`);
146689
146733
  }
146690
146734
  return fatal(
146691
146735
  `\u274C \`${cmd}\` \u662F\u8DEF\u7531\u7F16\u53F7\uFF0C\u4E0D\u662F\u547D\u4EE4\u3002\u8D26\u6237\u5217\u8868\u7528 list-accounts\uFF0C\u591A\u6237\u4F59\u989D\u7528 balance-scan\uFF0C\u591A\u6237\u6D88\u8017/CPA \u7528 accounts-digest\u3002\u5176\u5B83\u7F16\u53F7\u5148 Read SKILL \u8DEF\u7531\u8868\u3002`
@@ -146718,6 +146762,36 @@ function loginCustomerFlagIssue(args) {
146718
146762
  }
146719
146763
  return null;
146720
146764
  }
146765
+ var EDIT_WITHOUT_JSON_OUT = {
146766
+ "adgroup-edit": "ad groups -a <id> --fresh --json-out <dir>\uFF08\u8BFB maxCPCAmountYuan / targetCpaAmountYuan\uFF09",
146767
+ "keyword-edit": "ad keywords -a <id> --json-out <dir>\uFF08\u8BFB maxCPCYuan\u3002\u5217\u8868\u6CA1\u6709 --fresh\uFF0C\u7F13\u5B58\u7EA6 60 \u79D2\uFF09",
146768
+ "ad-edit": "ad list -a <id> --json-out <dir>"
146769
+ };
146770
+ function adEditJsonOutIssue(args) {
146771
+ if (args[0] !== "ad") return null;
146772
+ const sub = args[1];
146773
+ const verify = sub ? EDIT_WITHOUT_JSON_OUT[sub] : void 0;
146774
+ if (!verify) return null;
146775
+ const hasJsonOut = args.some((a) => a === "--json-out" || a.startsWith("--json-out="));
146776
+ if (!hasJsonOut) return null;
146777
+ return fatal(
146778
+ `\u274C \`${sub}\` \u6CA1\u6709 --json-out\u3002\u5E26\u4E0A\u5B83\u4F1A\u5728\u8BF7\u6C42\u53D1\u51FA\u524D\u5931\u8D25\uFF0C\u6539\u52A8\u6CA1\u6709\u63D0\u4EA4\u3002
146779
+ \u53BB\u6389 --json-out \u518D\u8DD1\u540C\u4E00\u6761\uFF08\u5199\u64CD\u4F5C\u4ECD\u8981 --commit\uFF09\u3002\u6838\u5BF9\uFF1Asiluzan-tso ${verify}`
146780
+ );
146781
+ }
146782
+ var LIST_WITHOUT_ID = /* @__PURE__ */ new Set(["campaigns", "groups", "keywords", "list"]);
146783
+ function adListIdFilterIssue(args) {
146784
+ if (args[0] !== "ad") return null;
146785
+ const sub = args[1];
146786
+ if (!sub || !LIST_WITHOUT_ID.has(sub)) return null;
146787
+ const hasId = args.some(
146788
+ (a) => a === "--id" || a.startsWith("--id=") || a === "--campaign-id" || a.startsWith("--campaign-id=")
146789
+ );
146790
+ if (!hasId) return null;
146791
+ return fatal(
146792
+ `\u274C \`ad ${sub}\` \u4E0D\u80FD\u6309 --id \u8FC7\u6EE4\u3002\u53BB\u6389 --id\uFF0C--json-out <dir> \u843D\u76D8\u540E\u5728 JSON \u91CC\u6309 id \u6311\u4E00\u6761\u3002`
146793
+ );
146794
+ }
146721
146795
  function agentCompatFatalMessage(argv) {
146722
146796
  const args = argv.slice(2);
146723
146797
  if (args.includes("--period") || args.some((a) => a.startsWith("--period="))) {
@@ -146752,6 +146826,10 @@ function agentCompatFatalMessage(argv) {
146752
146826
  "\u274C `ad \u2026` \u5B50\u547D\u4EE4\u6CA1\u6709 `-m/--media`\uFF0C\u8D26\u6237\u7528 `-a/--account <mediaCustomerId>`\u3002\n \u5355\u6237\uFF1Asiluzan-tso ad campaigns -a <id> --start <\u65E5> --end <\u65E5> --json-out ./snap\n \u5168\u6237\u5F53\u65E5\u8D85\u9884\u7B97/\u7A7A\u8017\u7194\u65AD\uFF1Asiluzan-tso guard budget-circuit|zero-conv -m Google --json-out ./snap"
146753
146827
  );
146754
146828
  }
146829
+ const editWithoutJsonOut = adEditJsonOutIssue(args);
146830
+ if (editWithoutJsonOut) return editWithoutJsonOut;
146831
+ const listIdFilter = adListIdFilterIssue(args);
146832
+ if (listIdFilter) return listIdFilter;
146755
146833
  if (cmd === "list" && (args.length === 1 || args[1]?.startsWith("-"))) {
146756
146834
  return fatal(
146757
146835
  "\u274C \u6CA1\u6709\u9876\u5C42 `list` \u547D\u4EE4\u3002\u67E5\u8BE2\u5E7F\u544A\u8D26\u6237\u5217\u8868\u8BF7\u7528\uFF1A\n siluzan-tso list-accounts [-m Google] [-k <id\u6216\u5173\u952E\u5B57>] --json-out <dir>"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.62-beta.2",
4
- "publishedAt": 1793277242701
3
+ "version": "1.1.62-beta.4",
4
+ "publishedAt": 1793329501451
5
5
  }
@@ -7,6 +7,7 @@
7
7
  - `url`:被诊断 URL
8
8
  - `ratingId`:`s1`–`s5`
9
9
  - `coreIssuesIds`:`ci1`–`ci10` 子集
10
+ - `priorityPlan[]`:第四节行动计划,**必填**。按本站未满分项编写,禁止套用固定清单
10
11
  - `modules[]`:`{ id, score, items[] }`
11
12
  - `items[]`:`{ id, score, status, issue, suggestion }`
12
13
  - `status` 枚举:`Excellent` | `Good` | `Normal` | `Poor` | `Full` | `NeedImprove` | `Absent`
@@ -37,6 +38,39 @@
37
38
  | ci9 | 缺少社媒分享入口 |
38
39
  | ci10 | 品牌风格不统一 |
39
40
 
41
+ ## priorityPlan(第四节行动计划,必写)
42
+
43
+ `website-diagnosis render` 在 `priorityPlan` 为空时直接失败,不会再用固定的「核心 Banner / 转化跟踪 / 移动端适配」清单。
44
+
45
+ 只收录本站 `status` 为 `Poor` / `NeedImprove` / `Absent` / `Normal` 且未满分的子项,以及已勾选的 `coreIssuesIds`。满分项不要写进去。没有未满分项时,只写一档 `低`,说明保持现状,不要编造问题。
46
+
47
+ | `priority` | 放什么 | `period` |
48
+ | ---------- | ------------------------------ | -------- |
49
+ | `高` | Poor / Absent,或影响转化、广告 | `1-2周` |
50
+ | `中` | NeedImprove,或未满分的结构问题 | `2-4周` |
51
+ | `低` | 其余未满分的体验、品牌项 | `1-3个月` |
52
+
53
+ 空档不要输出。每一档:
54
+
55
+ ```json
56
+ {
57
+ "priority": "高",
58
+ "period": "1-2周",
59
+ "impact": "首页没有表单,广告点击无法变成询盘",
60
+ "items": [
61
+ {
62
+ "itemId": "m1i8",
63
+ "direction": "首页补上询盘表单",
64
+ "task": "signals.hasForm=false。在首页首屏下增加不超过 4 个必填项的表单,并接到现有联系邮箱。",
65
+ "adImpact": "没有表单时,落地页点击无法计成转化,自动出价没有信号。"
66
+ }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ - `direction` / `task` / `adImpact` 必须写本站事实(页面、signals 字段或第三节的 issue),禁止「优化体验」「提升质量分」这类空话。
72
+ - `meta.lang=en` 时这三个字段用英文。`priority` 仍用 `高` / `中` / `低`。
73
+
40
74
  ## m1 · 网站内容及结构(9 项)
41
75
 
42
76
  | ID | 诊断项 | 满分 |
@@ -51,12 +51,13 @@ siluzan-tso website-diagnosis render --data ./diagnosis.json --collect ./snap-we
51
51
  3. 读落盘 `signals` / `dataAvailability`(CLI 已抽好:HTTPS、表单、CTA、GTM、社媒、Lighthouse 分数等)。
52
52
  4. `website-diagnosis prepare --collect <collect.json>` → `diagnosis.scaffold.json`(**推荐**)。
53
53
  5. Read `assets/website-diagnosis-rules.md`;**只补全**脚手架里 `needsAgent=true` 的子项(依据 `signals` 写 issue/suggestion/score);`needsAgent=false`(含 Lighthouse 不可用项)**禁止**改成虚构分。
54
- 6. 落盘完整 `diagnosis.json` 后:
54
+ 6. 写 `priorityPlan`:按本站未满分项和 `coreIssuesIds` 分高/中/低(规则见 `website-diagnosis-rules.md`)。禁止套用固定行动清单,禁止留空。
55
+ 7. 落盘完整 `diagnosis.json` 后:
55
56
  ```bash
56
57
  siluzan-tso website-diagnosis render --data ./diagnosis.json --collect ./snap-web/<collect>.json --out ./snap-web/website-diagnosis-report.html
57
58
  ```
58
- **禁止** Agent 手写/拼接 HTML;**禁止**只给 Markdown 或纯 JSON 充当终稿。
59
- 7. **禁止编造**未在 `signals` / HTML / Lighthouse 中出现的指标。见下节「数据缺失降级」。
59
+ **禁止** Agent 手写/拼接 HTML;**禁止**只给 Markdown 或纯 JSON 充当终稿。`priorityPlan` 为空时 render 失败,先补再跑。
60
+ 8. **禁止编造**未在 `signals` / HTML / Lighthouse 中出现的指标。见下节「数据缺失降级」。
60
61
 
61
62
  **在 TSO Copilot 网页内**:工具返回 `rendered: true` 时,前端已渲染卡片 +「查看详情」全页 HTML,Agent 只需简短确认,**勿重复贴报告正文**(见 tso_agent `prompt.py`)。
62
63
 
@@ -95,6 +96,21 @@ siluzan-tso website-diagnosis render --data ./diagnosis.json --collect ./snap-we
95
96
  "url": "https://www.example.com",
96
97
  "ratingId": "s1",
97
98
  "coreIssuesIds": ["ci1", "ci2"],
99
+ "priorityPlan": [
100
+ {
101
+ "priority": "高",
102
+ "period": "1-2周",
103
+ "impact": "本站这项对转化的影响",
104
+ "items": [
105
+ {
106
+ "itemId": "m4i2",
107
+ "direction": "本站现象的短标题",
108
+ "task": "依据 signals 或第三节 issue 写的具体动作",
109
+ "adImpact": "对这个站广告的影响"
110
+ }
111
+ ]
112
+ }
113
+ ],
98
114
  "modules": [
99
115
  {
100
116
  "id": "m1",
@@ -118,6 +134,7 @@ siluzan-tso website-diagnosis render --data ./diagnosis.json --collect ./snap-we
118
134
  | -------------------- | ------------------------------------------------------------------------------ |
119
135
  | `ratingId` | `s1` 优秀 … `s5` 不建议投放(见规则文档评分等级) |
120
136
  | `coreIssuesIds` | 从 `ci1`–`ci10` 选取适用项 |
137
+ | `priorityPlan` | 第四节。按本站未满分项写,空数组时 render 失败 |
121
138
  | `modules[].id` | `m1`–`m6` 六板块均须出现 |
122
139
  | `items[].suggestion` | 较差/需优化/缺失类 status → **「强烈建议:」** 开头;其余 → **「推荐优化:」** |
123
140
 
@@ -176,7 +176,7 @@
176
176
  1. 确认完整 URL(`https://` 可省略,CLI 自动补全)。
177
177
  2. 采集:`website-diagnosis collect --url <url> --json-out ./snap-web`(落盘含 `signals` / `dataAvailability`;`--include-html` 仅增大落盘,不改 signals)。
178
178
  3. 脚手架:`website-diagnosis prepare --collect ./snap-web/<collect>.json`(Lighthouse 失败时 m2i1/m5i1 预填 Absent)。
179
- 4. 按 `signals` + `website-diagnosis-rules.md` 补全 `needsAgent=true` 项 → 落盘 `diagnosis.json`。
179
+ 4. 按 `signals` + `website-diagnosis-rules.md` 补全 `needsAgent=true` 项,并写 `priorityPlan`(只根据本站未满分项,禁止套用固定行动清单)→ 落盘 `diagnosis.json`。
180
180
  5. 出 HTML:`website-diagnosis render --data ./diagnosis.json --collect ./snap-web/<collect>.json --out ./snap-web/website-diagnosis-report.html --lang <zh|en>`。
181
181
  - **产物**:交付 HTML 路径;Lighthouse 缺失时性能项标「未测到」、禁止编造分;仅需历史 ARIT 分时用 `website-diagnosis search --ids <websiteDiagnoseId>`。
182
182
 
@@ -46,6 +46,10 @@ siluzan-tso ad campaigns -a <accountId> [--start <YYYY-MM-DD>] [--end <YYYY-MM-D
46
46
 
47
47
  落盘 JSON 中 **`budget` 为日预算**(主币种「元」,CLI 已 ÷100),另有 **`statusV2`**(`Enabled`/`Paused`/`Removed`)与 **`statusDisplay`**(`{statusV2}·日程文案`,如 `Enabled·有效`、`Enabled·投放期已结束`、`Removed·已删除`)。
48
48
 
49
+ 同对象里 **`targetSpendBidCeilingYuan`**、**`targetCpaAmountYuan`** 也是元,与 `--bid-ceiling` / `--target-cpa` 同口径。`targetSpend_BidCeilingAmount`、`targetCpa_BidingAmount` 仍是分(`1` = 0.01 元),不要填回写参数。`targetRoas` 是倍数,不是金额。
50
+
51
+ 本命令**没有** `--id`。要看一条,落盘后按 `id` 过滤。
52
+
49
53
  > **消歧**:`statusDisplay` 含「投放期已结束」**不是**系列已删除——以 **`statusV2`** 为准;仅 `Removed` 才不可在其下新建组/广告。日程结束仍可 `ad-create`(是否投放另看预算与系列开关)。
50
54
 
51
55
  命令名:列表是 **`ad campaigns`**(兼容别名 `campaign-list`),**没有** `google-ads` 命名空间。`--json-out` **必须带路径**(落盘,勿管道 stdout)。
@@ -53,7 +57,7 @@ siluzan-tso ad campaigns -a <accountId> [--start <YYYY-MM-DD>] [--end <YYYY-MM-D
53
57
  带 `--start` / `--end` 时,同行的 **`spend` / `impressions` / `clicks` / `conversions` / `conversionsValue` 为该闭区间合计**(**不是**日消耗)。`conversionsValue` 是账户币种的转化价值,不是转化次数。报告写转化价值或 ROAS 时用它(ROAS = `conversionsValue` / `spend`)。`ad groups`、`ad list`、`ad keywords`、`ad search-terms` 在同一日期窗口下也返回该字段。
54
58
  与日预算比较「是否超预算」时,**必须** `start=end=统计日`;多日窗口须先 ÷ 天数或改用 `balance-scan.dailySpend` / `overview.averageDailyCost` 再谈「日均」。
55
59
 
56
- > **缓存**:网关侧此列表按 `账户 + 日期区间` 缓存 60 秒(Redis)。若刚 `campaign-create` / `campaign-edit` / `campaign-status` / `campaign-delete` 后立刻查询想看到最新结果,加 `--fresh` 跳过缓存(网关 `newest=true`);日常巡检/报表不需要,留空更省配额。
60
+ > **缓存**:网关侧此列表按 `账户 + 日期区间` 缓存 60 秒(Redis)。若刚 `campaign-create` / `campaign-edit` / `campaign-status` / `campaign-delete` 后立刻查询想看到最新结果,加 `--fresh` 跳过缓存(网关 `newest=true`);日常巡检/报表不需要,留空更省配额。区间不要拉到跨很多年:可能 500,正文含 `authToken` 也不是登录失效,缩短到近 30 天再查,不要据此 `reauth`。
57
61
 
58
62
  ### 启停
59
63
 
@@ -185,7 +189,7 @@ siluzan-tso ad ad-delete -a <accountId> --id <adId>
185
189
  siluzan-tso ad keywords -a <accountId> [--negative] [--start/--end <date>] [--json-out ./snap]
186
190
  ```
187
191
 
188
- 带日期时行上有 `conversionsValue`(账户币种的转化价值,不是 `conversions` 次数)。`--json-out` 里直接读这个字段;终端表「转化价值」列在字段缺失时为「—」。否定关键词名单没有这个指标。
192
+ 带日期时行上有 `conversionsValue`(账户币种的转化价值,不是 `conversions` 次数)。`--json-out` 里直接读这个字段;终端表「转化价值」列在字段缺失时为「—」。否定关键词名单没有这个指标。出价读 `maxCPCYuan`(元)。本列表按账户和日期缓存约 60 秒,**没有** `--fresh`,也**没有** `--id`。`keyword-edit` 回读报「列表仍是」时,等约 60 秒再看 `maxCPCYuan`,不要立刻重跑编辑。
189
193
 
190
194
  ### 添加关键词
191
195
 
@@ -23,8 +23,8 @@
23
23
  ## 写操作硬纪律(Agent 易踩)
24
24
 
25
25
  - **必须** `--commit "…"`;漏了会直接失败。
26
- - 列表命令:`ad campaigns` / `ad groups` / `ad list` / `ad keywords`(**没有** `google-ads`、没有必用 `campaign-list`;`campaign-list` 仅为 `campaigns` 别名)。
27
- - `--json-out <path>`:**路径必填**,JSON 落盘;禁止 `--json-out` 裸用或管道 stdout。
26
+ - 列表命令:`ad campaigns` / `ad groups` / `ad list` / `ad keywords`(**没有** `google-ads`、没有必用 `campaign-list`;`campaign-list` 仅为 `campaigns` 别名)。这四条**没有** `--id`,单条在落盘 JSON 里按 `id` 过滤。
27
+ - `--json-out <path>`:**路径必填**,JSON 落盘;禁止 `--json-out` 裸用或管道 stdout。`adgroup-edit`、`keyword-edit`、`ad-edit` **没有**这个参数,加上会在请求发出前失败。核对用对应列表。
28
28
  - 系列 `statusDisplay` 含「投放期已结束」≠ 已删除;以 JSON **`statusV2`** 为准(见 `google-ads-read.md`)。
29
29
  - **勿创建 Display**:`campaign-create` / `ad-create` 仅面向 Search(RSA);PMax 走 `pmax-*`。
30
30
 
@@ -71,7 +71,7 @@
71
71
 
72
72
  **PMax**:`ad pmax-create` **同步**返回 `campaignId`、`assetGroupId`、`budgetId`;无 `ad batch get/diff`。创建接口 400 才续跑:把比这次请求多出来的非空编号写回同一份 JSON,内容不改,再执行一次。首次创建不填这三个字段。已经 200、只是扩展或视频失败时不要再 `pmax-create`。详见 `pmax-api.md`。
73
73
 
74
- **金额**:JSON 中 `budget`、`targetCpa_BidingAmount` 填主币种**元**;CLI 提交前 ×100(与 `ad campaigns` 列表 `budget` 口径一致)。
74
+ **金额**:JSON 中 `budget`、`targetCpa_BidingAmount` 填主币种**元**;CLI 提交前 ×100(与 `ad campaigns` 列表 `budget` 口径一致)。`pmax-get` 返回的 `budget`、`targetCpa_BidingAmount` 仍是分(`500` = 5 元)。`pmax-edit --budget` / `--target-cpa` 填元,不要把 get 里的分填回去。`pmax-edit` 退出码 0 只表示请求被接受,改完用 `pmax-get` 核对。资产组 `statusV2` 为 `Removed` 时不要再 `pmax-asset-group-edit`。
75
75
 
76
76
  **图片**:配置 `imagePaths`(相对 JSON 文件目录)或 `marketingImageBase64` / `squareMarketingImageBase64` / `logoImageBase64`。`pmax-validate` 会自动校验图片尺寸(最小值 / 推荐值 / 宽高比 ±2% / 文件大小 ≤5120 KB)。如需将任意图片转为合规素材,用 `ad pmax-image-convert`(`marketing` / `square` / `logo` 三种格式,sharp 处理,居中裁切)。
77
77
 
@@ -260,7 +260,7 @@ siluzan-tso ad campaign-edit -a <accountId> --id <campaignId> \
260
260
  --bidding TARGET_CPA --target-cpa 80
261
261
  ```
262
262
 
263
- 相对运算:先 `ad campaigns --json-out ./snap` 读 `budget`(已为主币种元)、`biddingStrategyTypeV2`,再传入。
263
+ 相对运算:先 `ad campaigns --json-out ./snap` 读 `budget`、`targetSpendBidCeilingYuan`、`targetCpaAmountYuan`(都已是元)和 `biddingStrategyTypeV2`,再传入。不要读 `targetSpend_BidCeilingAmount` / `targetCpa_BidingAmount`(仍是分)。
264
264
 
265
265
  `campaign-edit` 的名称、预算、出价、投放网络,以及 `campaign-status`,走字段级 PATCH(`writeScope=search-field-patch`)。请求只带本次传入的字段,没传的字段保持原样。金额仍是主币种元,CLI ×100 写入网关「分」。`--search-network` / `--content-network` 传 `true` 或 `false`,关闭传 `false`,不能传空。`budgetShared=true` 时本命令直接拒绝 `--budget`,不会改共享预算。PMax 不要用本命令。
266
266
 
@@ -354,7 +354,7 @@ siluzan-tso ad keyword-delete -a <accountId> --id <keywordId> --adgroup-id <adGr
354
354
 
355
355
  ## ad keyword-edit — 搜索关键词编辑
356
356
 
357
- 只改 `--final-url` / `--max-cpc` / `--status` 时走字段级 PATCH,原地更新,`id` 与历史数据保留。`--final-url` 传空会清空关键词网址,回到广告自己的网址。改 `--text` / `--match-type`(Google 侧词面与匹配类型不可变)仍走批量 PUT,会新建并删除旧词,`id` 变化时 CLI 会提示。改 `--final-url` / `--max-cpc` / `--status` 后 CLI 会重新拉列表回读实际值,只有回读一致才输出 ✅;未生效或回读失败则报错退出。此时**禁止**自行改用 `keyword-delete` + `keyword-create`,也禁止直调网关删除重建(会丢失 id 与历史数据),哪怕只改一条也不行。应停下来告知用户「平台未生效、需反馈平台」,可另给 Google Ads Editor 导入清单作为替代方案。
357
+ 只改 `--final-url` / `--max-cpc` / `--status` 时走字段级 PATCH,原地更新,`id` 与历史数据保留。`--final-url` 传空会清空关键词网址,回到广告自己的网址。改 `--text` / `--match-type`(Google 侧词面与匹配类型不可变)仍走批量 PUT,会新建并删除旧词,`id` 变化时 CLI 会提示。改 `--final-url` / `--max-cpc` / `--status` 后 CLI 会重新拉列表回读。本命令没有 `--json-out`。关键词列表按账户和日期缓存约 60 秒,且没有 `--fresh`:回读报「列表仍是」时,先等约 60 秒再看 `maxCPCYuan`。已经是目标值就当成功,**不要**立刻重跑,**不要** `keyword-delete` 后重建。等过缓存仍是旧值,再告诉用户平台未生效。
358
358
 
359
359
  ```bash
360
360
  siluzan-tso ad keyword-edit \
@@ -31,7 +31,10 @@ Meta / Facebook 线索广告走 **`meta-ad`(W13)**,**禁止**用本命令
31
31
 
32
32
  - **广告类型**:仅 Search + PMax;**不支持 Display**(见上文「支持的广告类型」)。
33
33
  - **Search 改字段**:`campaign-status`、`campaign-edit` 的名称/预算/出价/投放网络、`adgroup-status` / `adgroup-rename` / `adgroup-edit`、`keyword-status`、`keyword-edit` 的状态/出价/网址、`ad-status` / `ad-edit` 都是字段级 PATCH,只写本次参数。词面和匹配类型仍是批量 PUT。关闭搜索合作伙伴或展示网络传 `--search-network false` / `--content-network false`。PMax 用 `pmax-edit`,不要用 `campaign-edit`。
34
- - **命令名**:系列列表是 `ad campaigns`(别名 `campaign-list`),**禁止** `ad-campaigns` / `campaign list`。`balance`/`stats` 须 `-m Google`。
34
+ - **命令名**:系列列表是 `ad campaigns`(别名 `campaign-list`),**禁止** `ad-campaigns` / `campaign list`。`balance`/`stats` 须 `-m Google`。`adgroup-edit` / `keyword-edit` / `ad-edit` 没有 `--json-out`。`ad campaigns|groups|keywords|list` 没有 `--id`。
35
+ - **列表金额**:`ad campaigns` 的 `budget`、`targetSpendBidCeilingYuan`、`targetCpaAmountYuan` 是元,与 `--budget` / `--bid-ceiling` / `--target-cpa` 同口径。同对象的 `targetSpend_BidCeilingAmount`、`targetCpa_BidingAmount` 仍是分(`1` = 0.01 元),禁止填回写参数。`ad groups` 只读 `maxCPCAmountYuan` / `targetCpaAmountYuan`。`pmax-get` 的 `budget` 仍是分(`500` = 5 元),`pmax-edit --budget` 填元。
36
+ - **关键词改价回读**:`keyword-edit` 报「列表仍是」时,列表缓存约 60 秒且没有 `--fresh`。不要立刻重跑,不要删词重建。等约 60 秒再看 `maxCPCYuan`。`--max-cpc 0` 是清空并回到广告组出价。广告组 `--target-cpa 0` 同样是清空;`--max-cpc` 必须大于 0。
37
+ - **日期 500**:列表默认近 30 天。区间过长返回 500 且正文含 `authToken` 时,不是登录失效,缩短区间再查,不要据此 `reauth`。
35
38
  - **Google 客户 ID(CID)带连字符**:UI / 用户口述常为 `123-456-7890`,平台 `mediaCustomerId` 与 Google 网关路径需要 `1234567890`。`ad campaigns` / `ad groups` / `ad list` 等 `-a` 与 JSON 顶层 `account` **可带连字符**(CLI 自动去掉)。若错误为 `HTTP 403:123-456-7890`(回显带横杠),先换纯数字重试,**禁止**据此直接判断 OAuth 过期或 `reauth`。
36
39
  - **禁止臆测授权过期**:`ad *` / geo / 拉数出现 403/500 时,**禁止**对用户说「授权可能过期」。须核验 ID 后执行 `account check-access -a <mediaCustomerId>`,仅当 `reauth_required`(或与 `invalidOAuthToken=true` 交叉确认)才走重授权;无激活字段时禁止谈套餐。见 [`accounts-permissions.md`](../accounts/accounts-permissions.md)。
37
40
  - **预算投放**:`BudgetBudgetDeliveryMethodV2` 默认用 `STANDARD`;`ACCELERATED` 多数账户会被 Google 拒(validate 警告)。
@@ -53,17 +53,15 @@ mkdir -p ./snap-scale && siluzan-tso google-analysis -a <mediaCustomerId> --sect
53
53
  ## 最终操作
54
54
 
55
55
  > **金额单位(关键)**:`ad campaign-edit` / `ad adgroup-edit` 所有金额参数均为 **主币种金额**(如 `100` 表示 ¥100,支持小数 `10.5`);CLI 内部自动 `Math.round(value × 100)` 写入「分」字段。详见 `references/google-ads/google-ads-write.md`「ad campaign-edit」金额单位说明。
56
- > **必须先用 `*Display` 字段取主币种当前值,加减后再传回**;不要把 `budget` / `targetCpaAmount` / `maxCPCAmount` 这些"分"字段当作主币种传给 CLI。
56
+ > 系列列表读 `budget`、`targetSpendBidCeilingYuan`、`targetCpaAmountYuan`(已是元)。组列表读 `maxCPCAmountYuan`、`targetCpaAmountYuan`。不要读 `targetSpend_BidCeilingAmount` / `targetCpa_BidingAmount`(分),也不要再 ÷100。
57
57
 
58
58
  **提高系列日预算**(在原值基础上 +10 元的写法):
59
59
 
60
60
  ```bash
61
- # 1) GET 取主币种当前值
62
- CUR=$(siluzan-tso ad campaigns -a <mediaCustomerId> --json-out ./snap | node -e '...筛选 budget(元)')
63
- # 2) 主币种 + 10
64
- NEW=$(node -e "console.log((${CUR} + 10).toFixed(2))")
65
- # 3) 主币种金额传回
66
- siluzan-tso ad campaign-edit -a <mediaCustomerId> --id <campaignId> --budget ${NEW}
61
+ # 1) 落盘后用代码读 budget(元)。不要把 --json-out 管道给 node
62
+ siluzan-tso ad campaigns -a <mediaCustomerId> --json-out ./snap
63
+ # 2) 主币种 + 10,再传回
64
+ siluzan-tso ad campaign-edit -a <mediaCustomerId> --id <campaignId> --budget <新预算元>
67
65
  ```
68
66
 
69
67
  **上调系列目标 CPA**(若策略为 tCPA 等):
@@ -78,7 +76,7 @@ siluzan-tso ad campaign-edit -a <mediaCustomerId> --id <campaignId> --target-cpa
78
76
  siluzan-tso ad adgroup-edit -a <mediaCustomerId> --id <adGroupId> --target-cpa <主币种金额>
79
77
  ```
80
78
 
81
- 写前务必先 **`ad campaigns --json-out ./snap` / `ad groups --json-out ./snap`** 取当前值,**读取 `*Display` 字段**(主币种)后按配置比例计算,再以主币种金额传回。
79
+ 写前务必先 **`ad campaigns --json-out ./snap` / `ad groups --json-out ./snap`** 取当前值。系列用 `budget` / `targetCpaAmountYuan`,组用 `targetCpaAmountYuan`(都是元),按配置比例计算后再传回。
82
80
 
83
81
  ---
84
82
 
@@ -89,7 +87,7 @@ siluzan-tso ad campaigns -a <mediaCustomerId> --start <…> --end <…> --json-o
89
87
  mkdir -p ./snap-scale && siluzan-tso google-analysis -a <mediaCustomerId> --sections campaigns --start <…> --end <…> --json-out ./snap-scale
90
88
  ```
91
89
 
92
- 确认 **`budget`**(元)、**`targetCpa_BidingAmount`**(或组上 **`targetCpaAmount`**)与 **`statusV2`** 符合预期。
90
+ 确认 **`budget`**、**`targetCpaAmountYuan`**(组上同名字段)与 **`statusV2`** 符合预期。刚改完的系列/组列表加 `--fresh`;关键词列表没有 `--fresh`。
93
91
 
94
92
  ---
95
93
 
@@ -43,7 +43,7 @@
43
43
  | 日预算与「当日」费用比 | **仅当** `--start` 与 `--end` 为**同一统计日**时,才可用 `spend` 对比 `budget`;**禁止**用多日 `spend` 去除以或对比日预算(会把周/月合计误当成「一天花了这么多」) |
44
44
  | 转化次数 | **`conversions`**(窗口内合计) |
45
45
  | 实际 CPA | 宿主侧 **`spend / conversions`**(同窗口合计;`conversions` 为 0 时不做 CPA 判断) |
46
- | 目标 CPA(系列 / 组) | 系列列表 JSON 中的 **`targetCpa_BidingAmount`** 等;组列表 **`ad groups --json-out ./snap`** 中的 **`targetCpaAmount`**(写入口径见 `references/google-ads/google-ads-write.md`「广告组编辑」) |
46
+ | 目标 CPA(系列 / 组) | 系列列表 JSON 的 **`targetCpaAmountYuan`**(元);组列表 **`targetCpaAmountYuan`**。不要读系列上的 `targetCpa_BidingAmount`(分) |
47
47
  | 日均消耗(续航用) | 用 `balance-scan` 的 **`dailySpend`**(近 7 日合计/7),或自算 `spend / 活跃天数`;**禁止**把多日 `spend` 直接当「日消耗」 |
48
48
 
49
49
  > **硬约束(自动化 / 预警必读)**:凡带 `--start` / `--end` 的读命令,返回的 `spend` / `impressions` / `clicks` / `conversions` 均为**该闭区间合计**。要「一天的消耗」必须 `start=end=该日`,或显式 `合计 ÷ 天数`;要「日均」用 `balance-scan.dailySpend` / `overview.averageDailyCost`。
@@ -129,7 +129,7 @@ siluzan-tso ad campaign-status -a <mediaCustomerId> --id <campaignId> --status E
129
129
  3. 对候选对象:
130
130
  - `conversions`(窗口内)**≥ 3**
131
131
  - `actual_cpa = spend / conversions`(`conversions > 0`)
132
- - 读取 **目标 CPA**(系列:`targetCpa_BidingAmount` 或 JSON 中实际键;组:`targetCpaAmount`);与 **`actual_cpa`** 同单位后再比。
132
+ - 读取 **目标 CPA**(系列:`targetCpaAmountYuan`;组:`targetCpaAmountYuan`,都是元)。`spend` 已是账户币种,与这两个字段同单位后再比。不要用 `targetCpa_BidingAmount`(分)。
133
133
  - **IF** `actual_cpa > target_cpa * spike_threshold`(如 `1.3`),则进入写操作。
134
134
 
135
135
  ### 最终操作
@@ -160,10 +160,10 @@ siluzan-tso ad adgroup-edit -a <mediaCustomerId> --id <adGroupId> --max-cpc <主
160
160
 
161
161
  ### 写后复核
162
162
 
163
- 再次 **`ad campaigns --json-out ./snap`** 或 **`ad groups --json-out ./snap`**:
163
+ 再次 **`ad campaigns --json-out ./snap`** 或 **`ad groups --json-out ./snap`**(刚改完系列/组加 `--fresh`):
164
164
 
165
- - 优先比对 **`*Display` 字段**(主币种)与预期主币种金额一致;
166
- - 整数字段 `targetCpa_BidingAmount` / `targetCpaAmount` / `maxCPCAmount` / `budget` 应等于 `主币种 × 100` 后的整数。
165
+ - 系列比对 `budget`、`targetSpendBidCeilingYuan`、`targetCpaAmountYuan`(已是元)。不要用 `targetSpend_BidCeilingAmount` / `targetCpa_BidingAmount`(分),也不要再 ×100。
166
+ - 组只比对 `maxCPCAmountYuan`、`targetCpaAmountYuan`。出口里没有 `maxCPCAmount` / `targetCpaAmount`。
167
167
 
168
168
  ### CLI 能力摘要
169
169
 
@@ -23,7 +23,7 @@ siluzan-tso website-diagnosis collect --url <网站URL> --json-out ./out
23
23
  siluzan-tso website-diagnosis prepare --collect ./out/<collect>.json --out ./out/diagnosis.scaffold.json
24
24
  ```
25
25
 
26
- 3. **补全诊断 JSON**:Agent 依据 `signals` + `assets/website-diagnosis-rules.md`,只改脚手架中 `needsAgent=true` 的项,落盘 `./out/diagnosis.json`。**此步只能输出 JSON,不能输出 HTML。**
26
+ 3. **补全诊断 JSON**:Agent 依据 `signals` + `assets/website-diagnosis-rules.md`,只改脚手架中 `needsAgent=true` 的项,并按本站未满分项写 `priorityPlan`,落盘 `./out/diagnosis.json`。**此步只能输出 JSON,不能输出 HTML。** `priorityPlan` 为空时下一步 render 会失败。
27
27
 
28
28
  4. **用 JSON 调用 render 生成 HTML**(唯一允许产出 HTML 的方式):
29
29
 
@@ -48,7 +48,7 @@ siluzan-tso website-diagnosis render --data ./out/diagnosis.json [--collect ./ou
48
48
  | `HealthOverview` | 总分、行业对比文案、`ratingId` 说明、**ECharts 雷达图** + 得分环 |
49
49
  | `RiskMap` | `coreIssuesIds` 列表 + **模块得分横向条形图**(对齐前端 RiskMap) |
50
50
  | `ModuleDetail` | 6 模块 × 子项:得分、status、issue、suggestion(表格或卡片) |
51
- | `PriorityPlan` | 高/中/低优先级改进计划(来自核心问题 + 低分子项) |
51
+ | `PriorityPlan` | 高/中/低行动计划,**只渲染** `priorityPlan`(Agent 按本站未满分项写) |
52
52
  | `LoadingSpeed` | **Lighthouse 柱状对比图** + desktop/mobile 指标表;缺失时 callout 说明 |
53
53
  | `LongtermValue` | 投放建议、长期优化价值(与 s1–s5 评级一致) |
54
54
 
@@ -75,7 +75,8 @@ siluzan-tso website-diagnosis render --data ./out/diagnosis.json [--collect ./ou
75
75
  | `analyzedAt` | 诊断时间 |
76
76
  | `ratingId` | 等级徽章(s1–s5 → 优秀…不建议投放) |
77
77
  | `score` | 总分(可由 modules 汇总) |
78
- | `coreIssuesIds` | RiskMap / 优先改进 |
78
+ | `coreIssuesIds` | 已勾选的核心问题 id |
79
+ | `priorityPlan` | 第四节三列卡片:`priority` / `period` / `impact` / `items[]` |
79
80
  | `modules[].id` | m1–m6 分节标题 |
80
81
  | `modules[].items[]` | 子项行:诊断项名、得分/满分、issue、suggestion |
81
82
  | `lighthouse` / `lighthouseResult` | LoadingSpeed 节(collect payload 为 `lighthouse`) |
@@ -696,6 +696,12 @@
696
696
  gap: 24px;
697
697
  }
698
698
 
699
+ .priority-empty {
700
+ grid-column: 1 / -1;
701
+ margin: 0;
702
+ color: var(--text-secondary, #4e5969);
703
+ }
704
+
699
705
  .priority-column {
700
706
  display: flex;
701
707
  flex-direction: column;
@@ -1334,80 +1340,6 @@
1334
1340
  { id: "ci10", issue: "品牌风格不统一", impact: "品牌可信度低", priority: "低" },
1335
1341
  ];
1336
1342
 
1337
- const PRIORITY_GROUP_DATA = [
1338
- {
1339
- 优先级: "高",
1340
- 执行周期: "1-2周",
1341
- 影响内容: "影响转化和广告投放",
1342
- 优化项: [
1343
- {
1344
- 优化方向: "核心Banner与首屏CTA",
1345
- 具体任务: "优化Banner图片清晰度、风格统一;突出首屏核心CTA和价值主张",
1346
- 广告影响说明: "提升Landing Page Experience评分,降低跳出率,直接影响Quality Score",
1347
- },
1348
- {
1349
- 优化方向: "转化跟踪部署",
1350
- 具体任务: "部署GA4、GTM、Facebook Pixe等;设置表单提交、电话点击事件",
1351
- 广告影响说明: "完整数据采集,支持自动出价和再营销,提高广告ROI",
1352
- },
1353
- {
1354
- 优化方向: "移动端适配优化",
1355
- 具体任务: "响应式布局优化;按钮触控目标≥44px;检查移动端首屏加载",
1356
- 广告影响说明: "移动端体验直接影响广告质量分和移动端转化率",
1357
- },
1358
- {
1359
- 优化方向: "页面加载速度优化",
1360
- 具体任务: "压缩图片、JS/CSS;使用缓存/CDN;优化首屏加载",
1361
- 广告影响说明: "提升Google Ads速度评分,降低CPC成本,提高广告排名",
1362
- },
1363
- ],
1364
- },
1365
- {
1366
- 优先级: "中",
1367
- 执行周期: "2-4周",
1368
- 影响内容: "影响用户体验",
1369
- 优化项: [
1370
- {
1371
- 优化方向: "导航栏与页面布局调整",
1372
- 具体任务: "栏目精简,导航层级≤3;首屏信息层级清晰",
1373
- 广告影响说明: "提升用户体验,降低跳出率,增加表单提交概率",
1374
- },
1375
- {
1376
- 优化方向: "表单优化",
1377
- 具体任务: "字段≤6个,必填≤4个;自动校验和清晰错误提示",
1378
- 广告影响说明: "提升表单提交率,增加转化量,优化广告ROI",
1379
- },
1380
- {
1381
- 优化方向: "产品类目页与详情页内容优化",
1382
- 具体任务: "每页英文说明≥200词;每产品≥3张高清图;规格参数齐全",
1383
- 广告影响说明: "提高Ad Relevance和转化率,降低广告质量分惩罚",
1384
- },
1385
- ],
1386
- },
1387
- {
1388
- 优先级: "低",
1389
- 执行周期: "1-3个月",
1390
- 影响内容: "优化细节提高美观或品牌感",
1391
- 优化项: [
1392
- {
1393
- 优化方向: "全站语言统一",
1394
- 具体任务: "全站英文(除语言切换外)",
1395
- 广告影响说明: "保证广告与落地页语言一致,提升Ad Relevance",
1396
- },
1397
- {
1398
- 优化方向: "品牌视觉统一",
1399
- 具体任务: "图片、Banner风格统一;字体、Logo、配色一致",
1400
- 广告影响说明: "提升品牌认知度和广告可信度,间接增加转化",
1401
- },
1402
- {
1403
- 优化方向: "社媒分享功能完善",
1404
- 具体任务: "添加OG标签、社交媒体Card;配置社媒分享按钮",
1405
- 广告影响说明: "提升社交可分享性,辅助再营销和广告推广",
1406
- },
1407
- ],
1408
- },
1409
- ];
1410
-
1411
1343
  /** 对齐 useWebsiteGuide/config allModuleConfigs(仅渲染所需字段) */
1412
1344
  const ALL_MODULE_CONFIGS = [
1413
1345
  {
@@ -1782,46 +1714,79 @@
1782
1714
  </div>`;
1783
1715
  }
1784
1716
 
1785
- function renderPriorityPlan() {
1786
- return PRIORITY_GROUP_DATA.map((group) => {
1787
- const cls =
1788
- group.优先级 === "高"
1789
- ? "priority-high"
1790
- : group.优先级 === "中"
1791
- ? "priority-medium"
1792
- : "priority-low";
1793
- const title =
1794
- group.优先级 === "高"
1795
- ? "急需优化!"
1796
- : group.优先级 === "中"
1797
- ? "建议优化"
1798
- : "推荐优化";
1799
- const icon = group.优先级 === "高" ? "🔥" : group.优先级 === "中" ? "💡" : "🚩";
1800
- const items = group.优化项
1801
- .map(
1802
- (item) => `
1717
+ function priorityRank(priority) {
1718
+ const p = String(priority || "").trim();
1719
+ if (p === "高" || p.toLowerCase() === "high") return "高";
1720
+ if (p === "低" || p.toLowerCase() === "low") return "低";
1721
+ if (p === "中" || p.toLowerCase() === "medium") return "中";
1722
+ return "";
1723
+ }
1724
+
1725
+ /** 第四节只渲染 Agent 写入的 priorityPlan,不使用固定清单。 */
1726
+ function readPriorityPlan(data) {
1727
+ const raw = data && (data.priorityPlan || data["优化优先级"]);
1728
+ if (!Array.isArray(raw)) return [];
1729
+ return raw
1730
+ .map((group) => {
1731
+ const rank = priorityRank(group.priority || group["优先级"]);
1732
+ const items = (group.items || group["优化项"] || [])
1733
+ .map((item) => ({
1734
+ direction: String(item.direction || item["优化方向"] || "").trim(),
1735
+ task: String(item.task || item["具体任务"] || "").trim(),
1736
+ adImpact: String(item.adImpact || item["广告影响说明"] || "").trim(),
1737
+ }))
1738
+ .filter((item) => item.direction || item.task);
1739
+ return {
1740
+ rank,
1741
+ period: String(group.period || group["执行周期"] || "").trim(),
1742
+ impact: String(group.impact || group["影响内容"] || "").trim(),
1743
+ items,
1744
+ };
1745
+ })
1746
+ .filter((group) => group.rank && group.items.length > 0);
1747
+ }
1748
+
1749
+ function renderPriorityPlan(groups) {
1750
+ if (!groups.length) {
1751
+ return `<p class="priority-empty">本次诊断未写入行动计划。请在 diagnosis.json 的 priorityPlan 中按本站未满分项编写后再生成报告。</p>`;
1752
+ }
1753
+ return groups
1754
+ .map((group) => {
1755
+ const cls =
1756
+ group.rank === "高"
1757
+ ? "priority-high"
1758
+ : group.rank === "中"
1759
+ ? "priority-medium"
1760
+ : "priority-low";
1761
+ const title =
1762
+ group.rank === "高" ? "急需优化!" : group.rank === "中" ? "建议优化" : "推荐优化";
1763
+ const icon = group.rank === "高" ? "🔥" : group.rank === "中" ? "💡" : "🚩";
1764
+ const items = group.items
1765
+ .map(
1766
+ (item) => `
1803
1767
  <div class="priority-item-card">
1804
- <div class="item-header"><span class="item-bullet">•</span><h4 class="item-title-text">${escapeHtml(item.优化方向)}</h4></div>
1805
- <p class="item-description">${escapeHtml(item.具体任务)}</p>
1806
- <div class="item-impact"><span class="impact-icon">↑</span><span class="impact-text">${escapeHtml(item.广告影响说明)}</span></div>
1768
+ <div class="item-header"><span class="item-bullet">•</span><h4 class="item-title-text">${escapeHtml(item.direction)}</h4></div>
1769
+ <p class="item-description">${escapeHtml(item.task)}</p>
1770
+ <div class="item-impact"><span class="impact-icon">↑</span><span class="impact-text">${escapeHtml(item.adImpact)}</span></div>
1807
1771
  </div>`,
1808
- )
1809
- .join("");
1810
- return `
1772
+ )
1773
+ .join("");
1774
+ return `
1811
1775
  <div class="priority-column ${cls}">
1812
1776
  <div class="priority-header">
1813
1777
  <div class="priority-header-top">
1814
1778
  <div class="priority-icon-wrapper"><span class="priority-icon">${icon}</span></div>
1815
1779
  <div class="priority-info">
1816
1780
  <h3 class="priority-title">${title}</h3>
1817
- <div class="priority-badge">${escapeHtml(group.执行周期)}</div>
1781
+ <div class="priority-badge">${escapeHtml(group.period)}</div>
1818
1782
  </div>
1819
1783
  </div>
1820
- <p class="priority-subtitle">${escapeHtml(group.影响内容)}</p>
1784
+ <p class="priority-subtitle">${escapeHtml(group.impact)}</p>
1821
1785
  </div>
1822
1786
  <div class="priority-items">${items}</div>
1823
1787
  </div>`;
1824
- }).join("");
1788
+ })
1789
+ .join("");
1825
1790
  }
1826
1791
 
1827
1792
  function renderLongtermValue() {
@@ -2050,7 +2015,7 @@
2050
2015
  <p>行动计划以任务化方式呈现,您可以直接分配给团队执行。</p>
2051
2016
  <p>高优先级任务可优先完成,快速释放广告投放价值!</p>
2052
2017
  </div>
2053
- <div class="priority-columns">${renderPriorityPlan()}</div>
2018
+ <div class="priority-columns">${renderPriorityPlan(readPriorityPlan(data))}</div>
2054
2019
  </div>
2055
2020
 
2056
2021
  <div class="report-section">
@@ -23,7 +23,7 @@ siluzan-tso website-diagnosis collect --url <网站URL> --json-out ./out
23
23
  siluzan-tso website-diagnosis prepare --collect ./out/<collect>.json --out ./out/diagnosis.scaffold.json
24
24
  ```
25
25
 
26
- 3. **补全诊断 JSON**:Agent 依据 `signals` + `assets/website-diagnosis-rules.md`,只改脚手架中 `needsAgent=true` 的项,落盘 `./out/diagnosis.json`。**此步只能输出 JSON,不能输出 HTML。**
26
+ 3. **补全诊断 JSON**:Agent 依据 `signals` + `assets/website-diagnosis-rules.md`,只改脚手架中 `needsAgent=true` 的项,并按本站未满分项写 `priorityPlan`,落盘 `./out/diagnosis.json`。**此步只能输出 JSON,不能输出 HTML。** `priorityPlan` 为空时下一步 render 会失败。
27
27
 
28
28
  4. **用 JSON 调用 render 生成 HTML**(唯一允许产出 HTML 的方式):
29
29
 
@@ -48,7 +48,7 @@ siluzan-tso website-diagnosis render --data ./out/diagnosis.json [--collect ./ou
48
48
  | `HealthOverview` | 总分、行业对比文案、`ratingId` 说明、**ECharts 雷达图** + 得分环 |
49
49
  | `RiskMap` | `coreIssuesIds` 列表 + **模块得分横向条形图**(对齐前端 RiskMap) |
50
50
  | `ModuleDetail` | 6 模块 × 子项:得分、status、issue、suggestion(表格或卡片) |
51
- | `PriorityPlan` | 高/中/低优先级改进计划(来自核心问题 + 低分子项) |
51
+ | `PriorityPlan` | 高/中/低行动计划,**只渲染** `priorityPlan`(Agent 按本站未满分项写) |
52
52
  | `LoadingSpeed` | **Lighthouse 柱状对比图** + desktop/mobile 指标表;缺失时 callout 说明 |
53
53
  | `LongtermValue` | 投放建议、长期优化价值(与 s1–s5 评级一致) |
54
54
 
@@ -75,7 +75,8 @@ siluzan-tso website-diagnosis render --data ./out/diagnosis.json [--collect ./ou
75
75
  | `analyzedAt` | 诊断时间 |
76
76
  | `ratingId` | 等级徽章(s1–s5 → 优秀…不建议投放) |
77
77
  | `score` | 总分(可由 modules 汇总) |
78
- | `coreIssuesIds` | RiskMap / 优先改进 |
78
+ | `coreIssuesIds` | 已勾选的核心问题 id |
79
+ | `priorityPlan` | 第四节三列卡片:`priority` / `period` / `impact` / `items[]` |
79
80
  | `modules[].id` | m1–m6 分节标题 |
80
81
  | `modules[].items[]` | 子项行:诊断项名、得分/满分、issue、suggestion |
81
82
  | `lighthouse` / `lighthouseResult` | LoadingSpeed 节(collect payload 为 `lighthouse`) |
@@ -9,7 +9,7 @@ $ErrorActionPreference = 'Stop'
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  $PKG_NAME = 'siluzan-tso-cli'
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- $PKG_VERSION = '1.1.62-beta.2'
12
+ $PKG_VERSION = '1.1.62-beta.4'
13
13
  $CLI_BIN = 'siluzan-tso'
14
14
  $SKILL_LABEL = 'Siluzan TSO'
15
15
  $INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
@@ -9,7 +9,7 @@ set -euo pipefail
9
9
  # -- Package info (injected at build time) ------------------------------------
10
10
  readonly PKG_NAME="siluzan-tso-cli"
11
11
  # PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
12
- readonly PKG_VERSION="1.1.62-beta.2"
12
+ readonly PKG_VERSION="1.1.62-beta.4"
13
13
  readonly CLI_BIN="siluzan-tso"
14
14
  readonly SKILL_LABEL="Siluzan TSO"
15
15
  readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.62-beta.2",
3
+ "version": "1.1.62-beta.4",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",