siluzan-tso-cli 1.1.44-beta.2 → 1.1.44-beta.3

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.44-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.44-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -128632,7 +128632,6 @@ function createDefaultDiagnosisReportData() {
128632
128632
  keywordCount: 0,
128633
128633
  adCount: 0,
128634
128634
  extensionCount: 0,
128635
- negativeKeywordCount: 0,
128636
128635
  countriesWithConversionsCount: 0
128637
128636
  },
128638
128637
  conversionGoals: { items: [] },
@@ -128965,7 +128964,7 @@ function buildStructureCounts(resourceRecord, campaignRows2, keywordRows, search
128965
128964
  keywordCount,
128966
128965
  adCount: num(resourceRecord?.adCount),
128967
128966
  extensionCount: num(resourceRecord?.extensionCount),
128968
- negativeKeywordCount: num(resourceRecord?.negativeKeywordCount),
128967
+ // collect 未拉取否词明细,不输出 negativeKeywordCount(避免报告展示 0 误导)
128969
128968
  countriesWithConversionsCount: num(resourceRecord?.countriesWithConversionsCount),
128970
128969
  searchTermCount: searchRows.length
128971
128970
  };
@@ -129126,6 +129125,7 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
129126
129125
  loadSectionSnapshot(snapDir, "resource-counts", accountId, start, end)
129127
129126
  );
129128
129127
  const goldRecord = sectionRecord(loadSectionSnapshot(snapDir, "gold-account", accountId, start, end)) ?? {};
129128
+ delete goldRecord.adsNegativeQualified;
129129
129129
  const campaignCurrent = sectionItems(
129130
129130
  loadSectionSnapshot(snapDir, "campaigns", accountId, start, end)
129131
129131
  );
@@ -129195,7 +129195,6 @@ function buildGoogleAdsDiagnosisCollectPayload(opts) {
129195
129195
  keywordCount: structureCounts.keywordCount,
129196
129196
  adCount: structureCounts.adCount,
129197
129197
  extensionCount: structureCounts.extensionCount,
129198
- negativeKeywordCount: structureCounts.negativeKeywordCount,
129199
129198
  countriesWithConversionsCount: structureCounts.countriesWithConversionsCount
129200
129199
  };
129201
129200
  data.conversionGoals = {
@@ -129930,7 +129929,7 @@ var NARRATIVE_CONSISTENCY_RULES = [
129930
129929
  id: "negative-keyword-zero-as-core-issue",
129931
129930
  textPattern: /否定(关键)?词.{0,40}(为\s*0|数量为\s*0|为0|缺少|没有)/,
129932
129931
  isContradicted: () => true,
129933
- hint: "\u7981\u6B62\u5C06\u300C\u5426\u5B9A\u5173\u952E\u8BCD\u6570\u91CF\u4E3A 0 / \u7F3A\u5C11\u5426\u8BCD\u300D\u5199\u5165 summary.keyIssues \u6216 diagnosisOverview.disadvantages\u3002structure.negativeKeywordCount \u53EF\u5728\u7ED3\u6784\u8868\u5C55\u793A\uFF0C\u4F46\u4E0D\u4F5C\u4E3A\u6838\u5FC3\u95EE\u9898\uFF1B\u5426\u8BCD\u5206\u6790\u653E\u5230\u5173\u952E\u8BCD/\u641C\u7D22\u8BCD\u6A21\u5757\u6309\u641C\u7D22\u8BCD\u6D6A\u8D39\u8BC1\u636E\u5199\u3002"
129932
+ hint: "\u672C\u62A5\u544A collect \u672A\u62C9\u53D6\u5426\u8BCD\u6570\u636E\uFF0C\u7981\u6B62\u5C06\u300C\u5426\u5B9A\u5173\u952E\u8BCD\u6570\u91CF\u4E3A 0 / \u7F3A\u5C11\u5426\u8BCD\u300D\u5199\u5165 summary.keyIssues \u6216 diagnosisOverview.disadvantages\u3002\u6709\u641C\u7D22\u8BCD\u6D6A\u8D39\u65F6\u4EC5\u5728 \xA708 \u6309\u5177\u4F53\u641C\u7D22\u8BCD\u5199\u4F18\u5316\u5EFA\u8BAE\uFF0C\u52FF\u4EE5\u5426\u8BCD\u8BA1\u6570\u4E0B\u7ED3\u8BBA\u3002"
129934
129933
  },
129935
129934
  {
129936
129935
  id: "landing-page-missing-as-core-issue",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.44-beta.2",
4
- "publishedAt": 1786442545653
3
+ "version": "1.1.44-beta.3",
4
+ "publishedAt": 1786500005435
5
5
  }
@@ -68,7 +68,7 @@ siluzan-tso google-ads-diagnosis render \
68
68
  - 若环比列全为 `null`,检查落盘目录是否存在上一周期文件(如 `campaigns-<id>_20260503-20260602.json`);不存在则**重新执行 collect**(勿用手动拼装或 Python 脚本绕过 CLI)。
69
69
 
70
70
  - **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串)或整体覆盖丢弃了 `landingPageAnalysis.desktop`/`mobile`,CLI **自动从 collect.reportData 恢复**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
71
- - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`campaigns/geographic/keywords.items` 默认同目录 collect **整表覆盖** Agent 改动(Agent 只填 analysis/suggestions);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与事实字段矛盾,或出现「否词=0 / 着陆页测速缺失」禁写项、预算/创意建议千篇一律、§07 写成关键词洞察时同样 **exit 1**。
71
+ - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`campaigns/geographic/keywords.items` 默认同目录 collect **整表覆盖** Agent 改动(Agent 只填 analysis/suggestions);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与事实字段矛盾,或出现「否词相关 / 着陆页测速缺失」禁写项、预算/创意建议千篇一律、§07 写成关键词洞察时同样 **exit 1**。
72
72
 
73
73
  - **禁止**在 `collect` 阶段生成或写入 `analysis` / `suggestions` / `diagnosisOverview` / `summary` 等建议性文案。
74
74
  - **禁止**跳过 Agent 直接 `render` collect 产物(`render` 会校验全部叙事 / 建议字段;调试可加 `--lenient`)。
@@ -144,7 +144,7 @@ siluzan-tso google-ads-diagnosis render \
144
144
  - 结论涉及「转化跟踪/GA 关联」→ 核对 `goldAccount.gaConfigured` / `goldAccount.conversionTrackingQualified`(按第 4 条 `true`=已达标)。
145
145
  - 结论涉及「着陆页测速/性能」→ 先看 `landingPageAnalysis.source`:`api` 才可用 desktop/mobile 的 score/FCP;`simple` 只能写简易诊断信号(`simpleDiagnosis`),**禁止**把 fetchMs 当 FCP 或编造性能指数;`none` 写「数据未获取」。
146
146
  - **禁止写入 keyIssues / disadvantages(render 硬拦截)**:
147
- - 「否定关键词数量为 0 / 缺少否词过滤」——`structure.negativeKeywordCount` 可在结构表展示,**不要**抬升为核心问题;有搜索词浪费时在 §08 按具体搜索词写。
147
+ - 「否定关键词数量为 0 / 缺少否词过滤」——本报告 **collect 未拉取否词数据**,结构表与黄金账户**不展示否词项**;禁止据此下结论。有搜索词浪费时仅在 §08 按具体搜索词写。
148
148
  - 「着陆页性能数据缺失 / 无法评估加载速度」——测速未获取是采集降级,**不是**投放核心问题;只在 §07 `analysis` 说明即可。
149
149
  - `render` 会对已知矛盾与上述禁写模式做**硬校验**,命中直接 `exit 1`——须按提示改写,**禁止**删字段绕过。
150
150
 
@@ -243,10 +243,9 @@ siluzan-tso google-ads-diagnosis render \
243
243
  | 有效关键字 (Effective Keywords) | |
244
244
  | 有效广告 (Effective Ads) | |
245
245
  | 附加链接 (Sitelinks) | |
246
- | 否定关键字 (Negative Keywords) | |
247
246
  | 有效国家 (Effective Countries) | |
248
247
 
249
- **对应字段**:`structure.campaignCount`、`adGroupCount`、`keywordCount`、`adCount`、`extensionCount`、`negativeKeywordCount`、`countriesWithConversionsCount`
248
+ **对应字段**:`structure.campaignCount`、`adGroupCount`、`keywordCount`、`adCount`、`extensionCount`、`countriesWithConversionsCount`(**不含**否词计数;collect 未拉否词)
250
249
 
251
250
  ### 3.3 指标检测
252
251
 
@@ -329,10 +328,11 @@ siluzan-tso google-ads-diagnosis render \
329
328
  | 附加链接 | | |
330
329
  | 附加宣传信息 | | |
331
330
  | 附加电话信息 | | |
332
- | 否定词添加 | | |
333
331
  | 受众群体设置 | | |
334
332
  | 出价策略 | | |
335
333
 
334
+ > 黄金账户表**不含**「否定词添加」(`adsNegativeQualified`):collect 未拉取否词数据,render 会剥离该字段。
335
+
336
336
  **得分**:`goldAccountScore`;**未达标项数**:若有 `goldAccountUnqualifiedCount` 可注明。
337
337
 
338
338
  ---
@@ -498,7 +498,7 @@ siluzan-tso google-ads-diagnosis render \
498
498
  ### 12.1 核心问题总结
499
499
 
500
500
  - `summary.keyIssues`:逐条列出(或写「暂无核心问题」)
501
- - **不要写**:否词数量=0;着陆页测速数据缺失(见撰写硬约束第 5 条)
501
+ - **不要写**:否词数量/缺少否词(本报告无否词数据);着陆页测速数据缺失(见撰写硬约束第 5 条)
502
502
 
503
503
  ### 12.2 优先级优化路线图
504
504
 
@@ -2110,7 +2110,7 @@
2110
2110
  { key: "adsSitelinkExtensionQualified", name: "附加链接" },
2111
2111
  { key: "adsCalloutExtensionQualified", name: "附加宣传信息" },
2112
2112
  { key: "adsCallExtensionQualified", name: "附加电话信息" },
2113
- { key: "adsNegativeQualified", name: "否定词添加" },
2113
+ // collect 未拉取否词数据,不展示「否定词添加」
2114
2114
  { key: "audienceQualified", name: "受众群体设置" },
2115
2115
  { key: "biddingPolicyQualified", name: "出价策略" },
2116
2116
  ];
@@ -2130,7 +2130,6 @@
2130
2130
  附加链接: "请添加附加链接,为用户提供更多访问路径",
2131
2131
  附加宣传信息: "请添加宣传信息,突出产品优势",
2132
2132
  附加电话信息: "请添加电话信息,方便用户直接联系",
2133
- 否定词添加: "请添加否定关键词,排除不相关流量",
2134
2133
  受众群体设置: "请设置受众群体,提高广告精准度",
2135
2134
  出价策略: "请使用智能出价策略,让Google AI优化出价",
2136
2135
  };
@@ -2933,31 +2932,83 @@
2933
2932
  return suggestions;
2934
2933
  };
2935
2934
 
2935
+ /**
2936
+ * 统一图例布局:卡片已有 HTML chart-title 时不再画 ECharts title,避免与 legend 重叠。
2937
+ * 无 HTML 标题时把 ECharts title 置顶、legend 放底部。
2938
+ */
2939
+ const applyChartChrome = (option, opts = {}) => {
2940
+ const { chartTitle, hasHtmlTitle = false, legendPlacement = "top" } = opts;
2941
+ const next = { ...option };
2942
+ const baseLegend =
2943
+ typeof next.legend === "object" && next.legend ? { ...next.legend } : {};
2944
+
2945
+ if (hasHtmlTitle || !chartTitle) {
2946
+ delete next.title;
2947
+ if (baseLegend.orient === "vertical") {
2948
+ next.legend = {
2949
+ ...baseLegend,
2950
+ orient: "vertical",
2951
+ right: 10,
2952
+ top: "middle",
2953
+ left: undefined,
2954
+ bottom: undefined,
2955
+ };
2956
+ } else if (legendPlacement === "bottom") {
2957
+ next.legend = { ...baseLegend, bottom: 0, top: undefined, left: "center" };
2958
+ if (next.grid) {
2959
+ next.grid = {
2960
+ ...next.grid,
2961
+ bottom: Math.max(Number(next.grid.bottom) || 0, 48),
2962
+ };
2963
+ }
2964
+ } else {
2965
+ next.legend = { ...baseLegend, top: 4, left: "center", bottom: undefined };
2966
+ if (next.grid) {
2967
+ next.grid = { ...next.grid, top: 48 };
2968
+ }
2969
+ }
2970
+ } else {
2971
+ next.title = {
2972
+ text: chartTitle,
2973
+ left: "center",
2974
+ top: 0,
2975
+ textStyle: { fontSize: 14, fontWeight: "bold" },
2976
+ };
2977
+ next.legend = { ...baseLegend, bottom: 0, top: undefined, left: "center" };
2978
+ if (next.grid) {
2979
+ next.grid = {
2980
+ ...next.grid,
2981
+ top: 40,
2982
+ bottom: Math.max(Number(next.grid.bottom) || 0, 48),
2983
+ };
2984
+ }
2985
+ }
2986
+ return next;
2987
+ };
2988
+
2936
2989
  const buildGeographicChartConfig = (items) => {
2937
2990
  if (!Array.isArray(items) || !items.length) return null;
2938
2991
  const labels = items.map((item) => getItemLabel(item, "geographic"));
2939
2992
  const dataPoints = items.map((item) => Number(item?.cost) || 0);
2940
2993
  if (!dataPoints.some((value) => value > 0)) return null;
2941
- return {
2942
- color: ["#3b68f9", "#5b8fff", "#7ba7ff", "#9db9ff", "#bfcbff", "#dce5ff"],
2943
- title: {
2944
- text: "地理位置消费分布",
2945
- left: "center",
2946
- textStyle: { fontSize: 16, fontWeight: "bold" },
2994
+ return applyChartChrome(
2995
+ {
2996
+ color: ["#3b68f9", "#5b8fff", "#7ba7ff", "#9db9ff", "#bfcbff", "#dce5ff"],
2997
+ tooltip: { trigger: "item" },
2998
+ legend: { type: "scroll" },
2999
+ series: [
3000
+ {
3001
+ type: "pie",
3002
+ radius: ["40%", "65%"],
3003
+ center: ["50%", "46%"],
3004
+ itemStyle: { borderColor: "#ffffff", borderWidth: 2 },
3005
+ label: { show: false },
3006
+ data: labels.map((name, index) => ({ name, value: dataPoints[index] })),
3007
+ },
3008
+ ],
2947
3009
  },
2948
- tooltip: { trigger: "item" },
2949
- legend: { bottom: 0, type: "scroll" },
2950
- series: [
2951
- {
2952
- type: "pie",
2953
- radius: ["40%", "70%"],
2954
- center: ["50%", "48%"],
2955
- itemStyle: { borderColor: "#ffffff", borderWidth: 2 },
2956
- label: { show: false },
2957
- data: labels.map((name, index) => ({ name, value: dataPoints[index] })),
2958
- },
2959
- ],
2960
- };
3010
+ { hasHtmlTitle: true, legendPlacement: "bottom" },
3011
+ );
2961
3012
  };
2962
3013
 
2963
3014
  const buildDeviceChartConfig = (items) => {
@@ -2966,22 +3017,20 @@
2966
3017
  const costData = items.map((item) => Number(item?.cost) || 0);
2967
3018
  const conversionData = items.map((item) => Number(item?.conversions) || 0);
2968
3019
  if (!costData.some((v) => v > 0) && !conversionData.some((v) => v > 0)) return null;
2969
- return {
2970
- title: {
2971
- text: "设备类型表现对比",
2972
- left: "center",
2973
- textStyle: { fontSize: 16, fontWeight: "bold" },
3020
+ return applyChartChrome(
3021
+ {
3022
+ tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
3023
+ legend: {},
3024
+ grid: { left: "8%", right: "5%", bottom: 30, containLabel: true },
3025
+ xAxis: { type: "category", data: labels, axisLine: { show: false }, axisTick: { show: false } },
3026
+ yAxis: { type: "value", splitLine: { lineStyle: { color: "#f0f0f0" } } },
3027
+ series: [
3028
+ { name: "消费", type: "bar", data: costData, itemStyle: { color: "#3b68f9" } },
3029
+ { name: "转化次数", type: "bar", data: conversionData, itemStyle: { color: "#5b8fff" } },
3030
+ ],
2974
3031
  },
2975
- tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
2976
- legend: { top: 28 },
2977
- grid: { left: "8%", right: "5%", top: 70, bottom: 30, containLabel: true },
2978
- xAxis: { type: "category", data: labels, axisLine: { show: false }, axisTick: { show: false } },
2979
- yAxis: { type: "value", splitLine: { lineStyle: { color: "#f0f0f0" } } },
2980
- series: [
2981
- { name: "消费", type: "bar", data: costData, itemStyle: { color: "#3b68f9" } },
2982
- { name: "转化次数", type: "bar", data: conversionData, itemStyle: { color: "#5b8fff" } },
2983
- ],
2984
- };
3032
+ { hasHtmlTitle: true },
3033
+ );
2985
3034
  };
2986
3035
 
2987
3036
  const buildAudienceChartConfig = (items) => {
@@ -2989,25 +3038,23 @@
2989
3038
  const labels = items.map((item) => getItemLabel(item, "audience"));
2990
3039
  const dataPoints = items.map((item) => Number(item?.cost) || 0);
2991
3040
  if (!dataPoints.some((value) => value > 0)) return null;
2992
- return {
2993
- color: ["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57", "#ff9ff3"],
2994
- title: {
2995
- text: "受众特征消费分布",
2996
- left: "center",
2997
- textStyle: { fontSize: 16, fontWeight: "bold" },
3041
+ return applyChartChrome(
3042
+ {
3043
+ color: ["#ff6b6b", "#4ecdc4", "#45b7d1", "#96ceb4", "#feca57", "#ff9ff3"],
3044
+ tooltip: { trigger: "item" },
3045
+ legend: { orient: "vertical", type: "scroll" },
3046
+ series: [
3047
+ {
3048
+ type: "pie",
3049
+ radius: "65%",
3050
+ center: ["40%", "55%"],
3051
+ itemStyle: { borderColor: "#ffffff", borderWidth: 2 },
3052
+ data: labels.map((name, index) => ({ name, value: dataPoints[index] })),
3053
+ },
3054
+ ],
2998
3055
  },
2999
- tooltip: { trigger: "item" },
3000
- legend: { orient: "vertical", right: 10, top: "middle", type: "scroll" },
3001
- series: [
3002
- {
3003
- type: "pie",
3004
- radius: "65%",
3005
- center: ["40%", "55%"],
3006
- itemStyle: { borderColor: "#ffffff", borderWidth: 2 },
3007
- data: labels.map((name, index) => ({ name, value: dataPoints[index] })),
3008
- },
3009
- ],
3010
- };
3056
+ { hasHtmlTitle: true },
3057
+ );
3011
3058
  };
3012
3059
 
3013
3060
  const buildCustomAudienceChartConfig = (items) => {
@@ -3029,31 +3076,29 @@
3029
3076
  if (totalConversions === 0) return 0;
3030
3077
  return Number(((conversions / totalConversions) * 100).toFixed(2));
3031
3078
  });
3032
- return {
3033
- title: {
3034
- text: "自定义受众花费与转化占比",
3035
- left: "center",
3036
- textStyle: { fontSize: 16, fontWeight: "bold" },
3037
- },
3038
- tooltip: {
3039
- trigger: "axis",
3040
- axisPointer: { type: "shadow" },
3041
- formatter: (params) =>
3042
- params.map((p) => `${p.marker}${p.seriesName}: ${p.value}%`).join("<br/>"),
3043
- },
3044
- legend: { top: 28 },
3045
- grid: { left: "8%", right: "5%", top: 70, bottom: 30, containLabel: true },
3046
- xAxis: { type: "category", data: labels, axisLine: { show: false }, axisTick: { show: false } },
3047
- yAxis: {
3048
- type: "value",
3049
- axisLabel: { formatter: "{value}%" },
3050
- splitLine: { lineStyle: { color: "#f0f0f0" } },
3079
+ return applyChartChrome(
3080
+ {
3081
+ tooltip: {
3082
+ trigger: "axis",
3083
+ axisPointer: { type: "shadow" },
3084
+ formatter: (params) =>
3085
+ params.map((p) => `${p.marker}${p.seriesName}: ${p.value}%`).join("<br/>"),
3086
+ },
3087
+ legend: {},
3088
+ grid: { left: "8%", right: "5%", bottom: 30, containLabel: true },
3089
+ xAxis: { type: "category", data: labels, axisLine: { show: false }, axisTick: { show: false } },
3090
+ yAxis: {
3091
+ type: "value",
3092
+ axisLabel: { formatter: "{value}%" },
3093
+ splitLine: { lineStyle: { color: "#f0f0f0" } },
3094
+ },
3095
+ series: [
3096
+ { name: "花费占比 (%)", type: "bar", data: costPercent, itemStyle: { color: "#3b68f9" } },
3097
+ { name: "转化占比 (%)", type: "bar", data: conversionPercent, itemStyle: { color: "#5b8fff" } },
3098
+ ],
3051
3099
  },
3052
- series: [
3053
- { name: "花费占比 (%)", type: "bar", data: costPercent, itemStyle: { color: "#3b68f9" } },
3054
- { name: "转化占比 (%)", type: "bar", data: conversionPercent, itemStyle: { color: "#5b8fff" } },
3055
- ],
3056
- };
3100
+ { hasHtmlTitle: true },
3101
+ );
3057
3102
  };
3058
3103
 
3059
3104
  const buildTargetingTableRow = (item, labelKey) => {
@@ -3387,22 +3432,28 @@
3387
3432
 
3388
3433
  const buildKeywordChartConfig = (items) => {
3389
3434
  if (!Array.isArray(items) || !items.length) {
3390
- return {
3391
- title: { text: "关键词表现趋势", left: "center" },
3392
- tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, valueFormatter: (v) => `${v}%` },
3393
- legend: { top: 28 },
3394
- grid: { left: "8%", right: "5%", top: 60, bottom: 30, containLabel: true },
3395
- xAxis: { type: "category", data: ["暂无数据"] },
3396
- yAxis: { type: "value", axisLabel: { formatter: "{value}%" } },
3397
- series: [
3398
- {
3399
- name: "花费占比 (%)",
3400
- type: "bar",
3401
- data: [0],
3402
- itemStyle: { color: "rgba(59, 104, 249, 0.6)", borderRadius: 6 },
3435
+ return applyChartChrome(
3436
+ {
3437
+ tooltip: {
3438
+ trigger: "axis",
3439
+ axisPointer: { type: "shadow" },
3440
+ valueFormatter: (v) => `${v}%`,
3403
3441
  },
3404
- ],
3405
- };
3442
+ legend: {},
3443
+ grid: { left: "8%", right: "5%", containLabel: true },
3444
+ xAxis: { type: "category", data: ["暂无数据"] },
3445
+ yAxis: { type: "value", axisLabel: { formatter: "{value}%" } },
3446
+ series: [
3447
+ {
3448
+ name: "花费占比 (%)",
3449
+ type: "bar",
3450
+ data: [0],
3451
+ itemStyle: { color: "rgba(59, 104, 249, 0.6)", borderRadius: 6 },
3452
+ },
3453
+ ],
3454
+ },
3455
+ { chartTitle: "关键词表现趋势" },
3456
+ );
3406
3457
  }
3407
3458
 
3408
3459
  const topItems = items.slice(0, 20);
@@ -3416,52 +3467,60 @@
3416
3467
  return Number.isFinite(value) ? Number(value.toFixed(2)) : 0;
3417
3468
  });
3418
3469
 
3419
- return {
3420
- title: { text: "关键词表现趋势", left: "center" },
3421
- tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, valueFormatter: (v) => `${v}%` },
3422
- legend: { top: 28 },
3423
- grid: { left: "8%", right: "5%", top: 60, bottom: 60, containLabel: true },
3424
- xAxis: {
3425
- type: "category",
3426
- data: labels,
3427
- axisLabel: { rotate: 45 },
3428
- },
3429
- yAxis: { type: "value", axisLabel: { formatter: "{value}%" } },
3430
- series: [
3431
- {
3432
- name: "花费占比 (%)",
3433
- type: "bar",
3434
- data: costShareValues,
3435
- itemStyle: { color: "rgba(59, 104, 249, 0.7)", borderRadius: 6 },
3470
+ return applyChartChrome(
3471
+ {
3472
+ tooltip: {
3473
+ trigger: "axis",
3474
+ axisPointer: { type: "shadow" },
3475
+ valueFormatter: (v) => `${v}%`,
3436
3476
  },
3437
- {
3438
- name: "转化占比 (%)",
3439
- type: "bar",
3440
- data: conversionShareValues,
3441
- itemStyle: { color: "rgba(16, 185, 129, 0.7)", borderRadius: 6 },
3477
+ legend: {},
3478
+ grid: { left: "8%", right: "5%", bottom: 60, containLabel: true },
3479
+ xAxis: {
3480
+ type: "category",
3481
+ data: labels,
3482
+ axisLabel: { rotate: 45 },
3442
3483
  },
3443
- ],
3444
- };
3445
- };
3446
-
3447
- const buildSearchTermsChartConfig = (items) => {
3448
- if (!Array.isArray(items) || !items.length) {
3449
- return {
3450
- title: { text: "搜索词表现趋势", left: "center" },
3451
- tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
3452
- legend: { top: 28 },
3453
- grid: { left: "8%", right: "5%", top: 60, bottom: 30, containLabel: true },
3454
- xAxis: { type: "category", data: ["暂无数据"] },
3455
- yAxis: { type: "value" },
3484
+ yAxis: { type: "value", axisLabel: { formatter: "{value}%" } },
3456
3485
  series: [
3457
3486
  {
3458
- name: "展示次数",
3487
+ name: "花费占比 (%)",
3488
+ type: "bar",
3489
+ data: costShareValues,
3490
+ itemStyle: { color: "rgba(59, 104, 249, 0.7)", borderRadius: 6 },
3491
+ },
3492
+ {
3493
+ name: "转化占比 (%)",
3459
3494
  type: "bar",
3460
- data: [0],
3461
- itemStyle: { color: "rgba(245, 158, 11, 0.7)", borderRadius: 6 },
3495
+ data: conversionShareValues,
3496
+ itemStyle: { color: "rgba(16, 185, 129, 0.7)", borderRadius: 6 },
3462
3497
  },
3463
3498
  ],
3464
- };
3499
+ },
3500
+ { chartTitle: "关键词表现趋势" },
3501
+ );
3502
+ };
3503
+
3504
+ const buildSearchTermsChartConfig = (items) => {
3505
+ if (!Array.isArray(items) || !items.length) {
3506
+ return applyChartChrome(
3507
+ {
3508
+ tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
3509
+ legend: {},
3510
+ grid: { left: "8%", right: "5%", containLabel: true },
3511
+ xAxis: { type: "category", data: ["暂无数据"] },
3512
+ yAxis: { type: "value" },
3513
+ series: [
3514
+ {
3515
+ name: "展示次数",
3516
+ type: "bar",
3517
+ data: [0],
3518
+ itemStyle: { color: "rgba(245, 158, 11, 0.7)", borderRadius: 6 },
3519
+ },
3520
+ ],
3521
+ },
3522
+ { chartTitle: "搜索词表现趋势" },
3523
+ );
3465
3524
  }
3466
3525
 
3467
3526
  const topItems = items.slice(0, 10);
@@ -3469,32 +3528,34 @@
3469
3528
  const impressionsData = topItems.map((item) => Number(item?.impressions) || 0);
3470
3529
  const clicksData = topItems.map((item) => Number(item?.clicks) || 0);
3471
3530
 
3472
- return {
3473
- title: { text: "搜索词表现趋势", left: "center" },
3474
- tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
3475
- legend: { top: 28 },
3476
- grid: { left: "8%", right: "5%", top: 60, bottom: 60, containLabel: true },
3477
- xAxis: {
3478
- type: "category",
3479
- data: labels,
3480
- axisLabel: { rotate: 45 },
3481
- },
3482
- yAxis: { type: "value" },
3483
- series: [
3484
- {
3485
- name: "展示次数",
3486
- type: "bar",
3487
- data: impressionsData,
3488
- itemStyle: { color: "rgba(245, 158, 11, 0.75)", borderRadius: 6 },
3489
- },
3490
- {
3491
- name: "点击次数",
3492
- type: "bar",
3493
- data: clicksData,
3494
- itemStyle: { color: "rgba(16, 185, 129, 0.75)", borderRadius: 6 },
3531
+ return applyChartChrome(
3532
+ {
3533
+ tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
3534
+ legend: {},
3535
+ grid: { left: "8%", right: "5%", bottom: 60, containLabel: true },
3536
+ xAxis: {
3537
+ type: "category",
3538
+ data: labels,
3539
+ axisLabel: { rotate: 45 },
3495
3540
  },
3496
- ],
3497
- };
3541
+ yAxis: { type: "value" },
3542
+ series: [
3543
+ {
3544
+ name: "展示次数",
3545
+ type: "bar",
3546
+ data: impressionsData,
3547
+ itemStyle: { color: "rgba(245, 158, 11, 0.75)", borderRadius: 6 },
3548
+ },
3549
+ {
3550
+ name: "点击次数",
3551
+ type: "bar",
3552
+ data: clicksData,
3553
+ itemStyle: { color: "rgba(16, 185, 129, 0.75)", borderRadius: 6 },
3554
+ },
3555
+ ],
3556
+ },
3557
+ { chartTitle: "搜索词表现趋势" },
3558
+ );
3498
3559
  };
3499
3560
 
3500
3561
  const STRATEGY_NAME_MAP = {
@@ -4101,22 +4162,78 @@
4101
4162
  if (useCostClickTrend) {
4102
4163
  const costData = items.map((item) => Number(item?.cost) || 0);
4103
4164
  const clicksData = items.map((item) => Number(item?.clicks) || 0);
4104
- return {
4165
+ return applyChartChrome(
4166
+ {
4167
+ tooltip: { trigger: "axis" },
4168
+ legend: { textStyle: { fontSize: 12 } },
4169
+ grid: { left: "8%", right: "8%", bottom: 30, containLabel: true },
4170
+ xAxis: { type: "category", data: labels, axisLabel: { fontSize: 11 } },
4171
+ yAxis: [
4172
+ {
4173
+ type: "value",
4174
+ name: "消耗",
4175
+ position: "left",
4176
+ axisLabel: { fontSize: 11 },
4177
+ nameTextStyle: { fontSize: 12 },
4178
+ },
4179
+ {
4180
+ type: "value",
4181
+ name: "点击次数",
4182
+ position: "right",
4183
+ splitLine: { show: false },
4184
+ axisLabel: { fontSize: 11 },
4185
+ nameTextStyle: { fontSize: 12 },
4186
+ },
4187
+ ],
4188
+ series: [
4189
+ {
4190
+ name: "消耗",
4191
+ type: "line",
4192
+ data: costData,
4193
+ smooth: 0.3,
4194
+ symbolSize: 6,
4195
+ yAxisIndex: 0,
4196
+ lineStyle: { color: "#3b82f6" },
4197
+ itemStyle: { color: "#3b82f6" },
4198
+ areaStyle: { color: "rgba(59, 130, 246, 0.12)" },
4199
+ },
4200
+ {
4201
+ name: "点击次数",
4202
+ type: "line",
4203
+ data: clicksData,
4204
+ smooth: 0.3,
4205
+ symbolSize: 6,
4206
+ yAxisIndex: 1,
4207
+ lineStyle: { color: "#10b981" },
4208
+ itemStyle: { color: "#10b981" },
4209
+ areaStyle: { color: "rgba(16, 185, 129, 0.12)" },
4210
+ },
4211
+ ],
4212
+ },
4213
+ { hasHtmlTitle: true },
4214
+ );
4215
+ }
4216
+
4217
+ const cpaData = items.map((item) => Number(item?.cpa) || 0);
4218
+ const conversionsData = items.map((item) => Number(item?.conversions ?? 0));
4219
+
4220
+ return applyChartChrome(
4221
+ {
4105
4222
  tooltip: { trigger: "axis" },
4106
- legend: { top: 0, textStyle: { fontSize: 12 } },
4107
- grid: { left: "8%", right: "8%", top: 40, bottom: 30, containLabel: true },
4223
+ legend: { textStyle: { fontSize: 12 } },
4224
+ grid: { left: "8%", right: "8%", bottom: 30, containLabel: true },
4108
4225
  xAxis: { type: "category", data: labels, axisLabel: { fontSize: 11 } },
4109
4226
  yAxis: [
4110
4227
  {
4111
4228
  type: "value",
4112
- name: "消耗",
4229
+ name: "CPA",
4113
4230
  position: "left",
4114
4231
  axisLabel: { fontSize: 11 },
4115
4232
  nameTextStyle: { fontSize: 12 },
4116
4233
  },
4117
4234
  {
4118
4235
  type: "value",
4119
- name: "点击次数",
4236
+ name: "转化次数",
4120
4237
  position: "right",
4121
4238
  splitLine: { show: false },
4122
4239
  axisLabel: { fontSize: 11 },
@@ -4125,9 +4242,9 @@
4125
4242
  ],
4126
4243
  series: [
4127
4244
  {
4128
- name: "消耗",
4245
+ name: "CPA",
4129
4246
  type: "line",
4130
- data: costData,
4247
+ data: cpaData,
4131
4248
  smooth: 0.3,
4132
4249
  symbolSize: 6,
4133
4250
  yAxisIndex: 0,
@@ -4136,70 +4253,20 @@
4136
4253
  areaStyle: { color: "rgba(59, 130, 246, 0.12)" },
4137
4254
  },
4138
4255
  {
4139
- name: "点击次数",
4256
+ name: "转化次数",
4140
4257
  type: "line",
4141
- data: clicksData,
4258
+ data: conversionsData,
4142
4259
  smooth: 0.3,
4143
4260
  symbolSize: 6,
4144
4261
  yAxisIndex: 1,
4145
- lineStyle: { color: "#10b981" },
4146
- itemStyle: { color: "#10b981" },
4147
- areaStyle: { color: "rgba(16, 185, 129, 0.12)" },
4262
+ lineStyle: { color: "#f97316" },
4263
+ itemStyle: { color: "#f97316" },
4264
+ areaStyle: { color: "rgba(249, 115, 22, 0.12)" },
4148
4265
  },
4149
4266
  ],
4150
- };
4151
- }
4152
-
4153
- const cpaData = items.map((item) => Number(item?.cpa) || 0);
4154
- const conversionsData = items.map((item) => Number(item?.conversions ?? 0));
4155
-
4156
- return {
4157
- tooltip: { trigger: "axis" },
4158
- legend: { top: 0, textStyle: { fontSize: 12 } },
4159
- grid: { left: "8%", right: "8%", top: 40, bottom: 30, containLabel: true },
4160
- xAxis: { type: "category", data: labels, axisLabel: { fontSize: 11 } },
4161
- yAxis: [
4162
- {
4163
- type: "value",
4164
- name: "CPA",
4165
- position: "left",
4166
- axisLabel: { fontSize: 11 },
4167
- nameTextStyle: { fontSize: 12 },
4168
- },
4169
- {
4170
- type: "value",
4171
- name: "转化次数",
4172
- position: "right",
4173
- splitLine: { show: false },
4174
- axisLabel: { fontSize: 11 },
4175
- nameTextStyle: { fontSize: 12 },
4176
- },
4177
- ],
4178
- series: [
4179
- {
4180
- name: "CPA",
4181
- type: "line",
4182
- data: cpaData,
4183
- smooth: 0.3,
4184
- symbolSize: 6,
4185
- yAxisIndex: 0,
4186
- lineStyle: { color: "#3b82f6" },
4187
- itemStyle: { color: "#3b82f6" },
4188
- areaStyle: { color: "rgba(59, 130, 246, 0.12)" },
4189
- },
4190
- {
4191
- name: "转化次数",
4192
- type: "line",
4193
- data: conversionsData,
4194
- smooth: 0.3,
4195
- symbolSize: 6,
4196
- yAxisIndex: 1,
4197
- lineStyle: { color: "#f97316" },
4198
- itemStyle: { color: "#f97316" },
4199
- areaStyle: { color: "rgba(249, 115, 22, 0.12)" },
4200
- },
4201
- ],
4202
- };
4267
+ },
4268
+ { hasHtmlTitle: true },
4269
+ );
4203
4270
  }
4204
4271
 
4205
4272
  const labels = ["消耗", "点击", "转化"];
@@ -4485,10 +4552,6 @@
4485
4552
  label: "附加链接 (Sitelinks)",
4486
4553
  value: structure.extensionCount ?? 0,
4487
4554
  },
4488
- {
4489
- label: "否定关键字 (Negative Keywords)",
4490
- value: structure.negativeKeywordCount ?? 0,
4491
- },
4492
4555
  {
4493
4556
  label: "有效国家 (Effective Countries)",
4494
4557
  value: structure.countriesWithConversionsCount ?? 0,
@@ -4921,10 +4984,8 @@
4921
4984
  (count, item) => count + (goldAccount[item.key] ? 1 : 0),
4922
4985
  0,
4923
4986
  );
4924
- const unqualifiedCount =
4925
- typeof goldAccount.goldAccountUnqualifiedCount === "number"
4926
- ? goldAccount.goldAccountUnqualifiedCount
4927
- : totalItems - qualifiedCount;
4987
+ // 按当前展示项重算(已排除否词项),勿用可能含 adsNegativeQualified 的接口计数
4988
+ const unqualifiedCount = totalItems - qualifiedCount;
4928
4989
  const scoreValue =
4929
4990
  goldAccount.goldAccountScore !== undefined && goldAccount.goldAccountScore !== null
4930
4991
  ? String(goldAccount.goldAccountScore)
@@ -68,7 +68,7 @@ siluzan-tso google-ads-diagnosis render \
68
68
  - 若环比列全为 `null`,检查落盘目录是否存在上一周期文件(如 `campaigns-<id>_20260503-20260602.json`);不存在则**重新执行 collect**(勿用手动拼装或 Python 脚本绕过 CLI)。
69
69
 
70
70
  - **render 自动合并(默认开启)**:若同目录存在 `google-ads-diagnosis-collect.json`,且 Agent 改坏了 `campaigns` / `geographic` / `keywords` 的 `items`(缺 `title`/`currentCost` 或环比写成字符串)或整体覆盖丢弃了 `landingPageAnalysis.desktop`/`mobile`,CLI **自动从 collect.reportData 恢复**,保留 Agent 的 `analysis`/`suggestions`。可用 `--no-merge-collect` 关闭;`--collect <file>` 指定 collect 路径。
71
- - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`campaigns/geographic/keywords.items` 默认同目录 collect **整表覆盖** Agent 改动(Agent 只填 analysis/suggestions);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与事实字段矛盾,或出现「否词=0 / 着陆页测速缺失」禁写项、预算/创意建议千篇一律、§07 写成关键词洞察时同样 **exit 1**。
71
+ - **render 硬校验**:合并后对比表仍不完整则 **exit 1**(避免 HTML 表体全空);`campaigns/geographic/keywords.items` 默认同目录 collect **整表覆盖** Agent 改动(Agent 只填 analysis/suggestions);`summary.keyIssues` / `diagnosisOverview.disadvantages` 与事实字段矛盾,或出现「否词相关 / 着陆页测速缺失」禁写项、预算/创意建议千篇一律、§07 写成关键词洞察时同样 **exit 1**。
72
72
 
73
73
  - **禁止**在 `collect` 阶段生成或写入 `analysis` / `suggestions` / `diagnosisOverview` / `summary` 等建议性文案。
74
74
  - **禁止**跳过 Agent 直接 `render` collect 产物(`render` 会校验全部叙事 / 建议字段;调试可加 `--lenient`)。
@@ -144,7 +144,7 @@ siluzan-tso google-ads-diagnosis render \
144
144
  - 结论涉及「转化跟踪/GA 关联」→ 核对 `goldAccount.gaConfigured` / `goldAccount.conversionTrackingQualified`(按第 4 条 `true`=已达标)。
145
145
  - 结论涉及「着陆页测速/性能」→ 先看 `landingPageAnalysis.source`:`api` 才可用 desktop/mobile 的 score/FCP;`simple` 只能写简易诊断信号(`simpleDiagnosis`),**禁止**把 fetchMs 当 FCP 或编造性能指数;`none` 写「数据未获取」。
146
146
  - **禁止写入 keyIssues / disadvantages(render 硬拦截)**:
147
- - 「否定关键词数量为 0 / 缺少否词过滤」——`structure.negativeKeywordCount` 可在结构表展示,**不要**抬升为核心问题;有搜索词浪费时在 §08 按具体搜索词写。
147
+ - 「否定关键词数量为 0 / 缺少否词过滤」——本报告 **collect 未拉取否词数据**,结构表与黄金账户**不展示否词项**;禁止据此下结论。有搜索词浪费时仅在 §08 按具体搜索词写。
148
148
  - 「着陆页性能数据缺失 / 无法评估加载速度」——测速未获取是采集降级,**不是**投放核心问题;只在 §07 `analysis` 说明即可。
149
149
  - `render` 会对已知矛盾与上述禁写模式做**硬校验**,命中直接 `exit 1`——须按提示改写,**禁止**删字段绕过。
150
150
 
@@ -243,10 +243,9 @@ siluzan-tso google-ads-diagnosis render \
243
243
  | 有效关键字 (Effective Keywords) | |
244
244
  | 有效广告 (Effective Ads) | |
245
245
  | 附加链接 (Sitelinks) | |
246
- | 否定关键字 (Negative Keywords) | |
247
246
  | 有效国家 (Effective Countries) | |
248
247
 
249
- **对应字段**:`structure.campaignCount`、`adGroupCount`、`keywordCount`、`adCount`、`extensionCount`、`negativeKeywordCount`、`countriesWithConversionsCount`
248
+ **对应字段**:`structure.campaignCount`、`adGroupCount`、`keywordCount`、`adCount`、`extensionCount`、`countriesWithConversionsCount`(**不含**否词计数;collect 未拉否词)
250
249
 
251
250
  ### 3.3 指标检测
252
251
 
@@ -329,10 +328,11 @@ siluzan-tso google-ads-diagnosis render \
329
328
  | 附加链接 | | |
330
329
  | 附加宣传信息 | | |
331
330
  | 附加电话信息 | | |
332
- | 否定词添加 | | |
333
331
  | 受众群体设置 | | |
334
332
  | 出价策略 | | |
335
333
 
334
+ > 黄金账户表**不含**「否定词添加」(`adsNegativeQualified`):collect 未拉取否词数据,render 会剥离该字段。
335
+
336
336
  **得分**:`goldAccountScore`;**未达标项数**:若有 `goldAccountUnqualifiedCount` 可注明。
337
337
 
338
338
  ---
@@ -498,7 +498,7 @@ siluzan-tso google-ads-diagnosis render \
498
498
  ### 12.1 核心问题总结
499
499
 
500
500
  - `summary.keyIssues`:逐条列出(或写「暂无核心问题」)
501
- - **不要写**:否词数量=0;着陆页测速数据缺失(见撰写硬约束第 5 条)
501
+ - **不要写**:否词数量/缺少否词(本报告无否词数据);着陆页测速数据缺失(见撰写硬约束第 5 条)
502
502
 
503
503
  ### 12.2 优先级优化路线图
504
504
 
@@ -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.44-beta.2'
12
+ $PKG_VERSION = '1.1.44-beta.3'
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.44-beta.2"
12
+ readonly PKG_VERSION="1.1.44-beta.3"
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.44-beta.2",
3
+ "version": "1.1.44-beta.3",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",