siluzan-tso-cli 1.1.28-beta.1 → 1.1.28-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.28-beta.1),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.28-beta.3),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -102729,7 +102729,7 @@ init_dist();
102729
102729
  import * as fs18 from "fs";
102730
102730
  import * as path26 from "path";
102731
102731
  import { fileURLToPath as fileURLToPath6 } from "url";
102732
- import { Command } from "commander";
102732
+ import { Command as Command2 } from "commander";
102733
102733
 
102734
102734
  // src/commands/login/urls.ts
102735
102735
  init_defaults();
@@ -106072,6 +106072,55 @@ async function writeReportAnalysisSnapshot(params) {
106072
106072
 
106073
106073
  // src/commands/report/main.ts
106074
106074
  init_version();
106075
+
106076
+ // src/commands/facebook-analysis/fetch.ts
106077
+ init_auth();
106078
+ function normalizeFacebookAccountId(raw) {
106079
+ const t = raw.trim();
106080
+ if (!t) {
106081
+ console.error("\n\u274C --account \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
106082
+ process.exit(1);
106083
+ }
106084
+ const digits = t.startsWith("act_") ? t.slice(4) : t;
106085
+ if (!/^\d+$/.test(digits)) {
106086
+ console.error(
106087
+ "\n\u274C --account \u987B\u4E3A Facebook \u5E7F\u544A\u8D26\u6237 ID\uFF08\u6570\u5B57 mediaCustomerId\uFF0C\u6216 act_<\u6570\u5B57>\uFF0C\u4E0E list-accounts -m MetaAd \u4E00\u81F4\uFF09\u3002\n"
106088
+ );
106089
+ process.exit(1);
106090
+ }
106091
+ return { apiId: `act_${digits}`, manifestId: digits };
106092
+ }
106093
+ function resolveFacebookDateRange(start, end) {
106094
+ if (start && end) return { startDate: start, endDate: end };
106095
+ if (!start && !end) {
106096
+ const endD = /* @__PURE__ */ new Date();
106097
+ endD.setDate(endD.getDate() - 1);
106098
+ const startD = new Date(endD);
106099
+ startD.setDate(startD.getDate() - 6);
106100
+ const fmt2 = (d) => d.toISOString().slice(0, 10);
106101
+ return { startDate: fmt2(startD), endDate: fmt2(endD) };
106102
+ }
106103
+ console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
106104
+ process.exit(1);
106105
+ }
106106
+ function facebookReportingUrl(config, apiId, segment, query) {
106107
+ const q = query ? query.startsWith("?") ? query : `?${query}` : "";
106108
+ return `${config.apiBaseUrl}/reporting/media-account/FacebookAds/${encodeURIComponent(apiId)}/${segment}${q}`;
106109
+ }
106110
+ async function fetchFacebookSectionPayload(def, opts, config, apiId) {
106111
+ const { startDate, endDate } = resolveFacebookDateRange(opts.start, opts.end);
106112
+ const params = new URLSearchParams({ startDate, endDate });
106113
+ if (def.countryLimitOption && opts.limit !== void 0 && Number.isFinite(opts.limit)) {
106114
+ params.set("limit", String(Math.max(1, Math.floor(opts.limit))));
106115
+ }
106116
+ const url = facebookReportingUrl(config, apiId, def.segment, params.toString());
106117
+ return apiFetch2(url, config, {}, opts.verbose ?? false);
106118
+ }
106119
+ function endpointHintForFacebookSection(def, apiId) {
106120
+ return `GET \u2026/FacebookAds/${apiId}/${def.segment}`;
106121
+ }
106122
+
106123
+ // src/commands/report/main.ts
106075
106124
  async function runReportList(opts) {
106076
106125
  if (!VALID_MEDIA_TYPES5.includes(opts.media)) {
106077
106126
  console.error(`
@@ -106173,30 +106222,61 @@ function fmtNum2(n, digits = 2) {
106173
106222
  parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
106174
106223
  return parts.join(".");
106175
106224
  }
106225
+ function metaOverviewManifestId(raw) {
106226
+ const t = raw.trim();
106227
+ if (t.startsWith("act_")) return t.slice(4);
106228
+ return t;
106229
+ }
106230
+ function buildMetaOverviewUrl(config, accountId, mediaAccountType, startDate, endDate) {
106231
+ const { apiId, manifestId } = normalizeFacebookAccountId(accountId);
106232
+ const params = new URLSearchParams({ startDate, endDate }).toString();
106233
+ if (mediaAccountType === "FacebookAds") {
106234
+ return facebookReportingUrl(config, apiId, "OverviewSectionData", params);
106235
+ }
106236
+ return `${config.apiBaseUrl}/reporting/media-account/MetaAd/${manifestId}/OverviewSectionData?${params}`;
106237
+ }
106176
106238
  async function runReportMetaOverview(opts) {
106177
- const id = opts.account.trim();
106178
- if (!/^\d+$/.test(id)) {
106179
- console.error("\n\u274C --account \u987B\u4E3A\u6570\u5B57 mediaCustomerId\uFF08\u4E0E list-accounts \u4E00\u81F4\uFF09\u3002\n");
106239
+ const rawAccount = opts.account.trim();
106240
+ let manifestId;
106241
+ try {
106242
+ manifestId = normalizeFacebookAccountId(rawAccount).manifestId;
106243
+ } catch {
106244
+ console.error(
106245
+ "\n\u274C --account \u987B\u4E3A\u6570\u5B57 mediaCustomerId \u6216 act_<\u6570\u5B57>\uFF08\u4E0E list-accounts -m MetaAd \u4E00\u81F4\uFF09\u3002\n"
106246
+ );
106180
106247
  process.exit(1);
106181
106248
  }
106182
106249
  const { startDate, endDate } = resolveMetaOverviewDateRange(opts.startDate, opts.endDate);
106183
106250
  const config = loadConfig(opts.token);
106184
- const params = new URLSearchParams({ startDate, endDate });
106185
- const url = `${config.apiBaseUrl}/reporting/media-account/MetaAd/${id}/OverviewSectionData?${params}`;
106251
+ const accountDetail = await lookupMetaAccountForOverview(config, manifestId, opts.verbose);
106252
+ const mediaAccountType = accountDetail?.mediaAccountType ?? "FacebookAds";
106253
+ const url = buildMetaOverviewUrl(config, rawAccount, mediaAccountType, startDate, endDate);
106186
106254
  let data;
106187
106255
  try {
106188
106256
  data = await apiFetch2(url, config, {}, opts.verbose);
106189
106257
  } catch (err) {
106190
- console.error(`
106191
- \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
106258
+ const msg = err instanceof Error ? err.message : String(err);
106259
+ if (msg.includes("403")) {
106260
+ console.error(
106261
+ `
106262
+ \u274C \u8BF7\u6C42\u5931\u8D25\uFF1AHTTP 403
106263
+ \u8D26\u6237 ${manifestId} \u7684 mediaAccountType=${mediaAccountType ?? "\u672A\u77E5"}\u3002
106264
+ Facebook \u6388\u6743\u8D26\u6237\uFF08FacebookAds\uFF09\u987B\u8D70 FacebookAds/act_ \u8DEF\u5F84\uFF1B\u4E1D\u8DEF\u8D5E Meta \u5F00\u6237\u8D70 MetaAd \u8DEF\u5F84\u3002
106265
+ \u5468\u671F/\u591A\u7EF4\u5EA6\u62A5\u544A\u8BF7\u6539\u7528\uFF1Asiluzan-tso facebook-analysis -a ${manifestId} --sections overview --json-out <dir>
106266
+ `
106267
+ );
106268
+ } else {
106269
+ console.error(`
106270
+ \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${msg}
106192
106271
  `);
106272
+ }
106193
106273
  process.exit(1);
106194
106274
  }
106195
106275
  if (opts.jsonOut) {
106196
106276
  const summary = await writeReportAnalysisSnapshot({
106197
106277
  snapshotDir: opts.jsonOut,
106198
106278
  section: "meta-overview",
106199
- accountId: id,
106279
+ accountId: manifestId,
106200
106280
  dateRange: { start: startDate, end: endDate },
106201
106281
  payload: data,
106202
106282
  cliVersion: getCurrentVersion2()
@@ -106211,7 +106291,7 @@ async function runReportMetaOverview(opts) {
106211
106291
  const optRaw = row.optimizationScore;
106212
106292
  const optPct = typeof optRaw === "number" && optRaw <= 1 && optRaw >= 0 ? (optRaw * 100).toFixed(0) : typeof optRaw === "number" ? String(optRaw) : "\u2014";
106213
106293
  console.log(`
106214
- \u2705 Meta \u8D26\u6237\u603B\u89C8 ${id} \u533A\u95F4 ${startDate} ~ ${endDate}
106294
+ \u2705 Meta \u8D26\u6237\u603B\u89C8 ${manifestId} \u533A\u95F4 ${startDate} ~ ${endDate}
106215
106295
  `);
106216
106296
  console.log(
106217
106297
  ` \u8D27\u5E01 ${currency} \u4F18\u5316\u5F97\u5206 ${optPct}${typeof optRaw === "number" && optRaw <= 1 ? "%\uFF080\u20131 \u5DF2\u6362\u7B97\uFF09" : ""} \u603B\u6D88\u8017 ${fmtNum2(typeof row.totalCost === "number" ? row.totalCost : cur.spend)} \u4F59\u989D ${fmtNum2(typeof row.remainingAccountBudget === "number" ? row.remainingAccountBudget : void 0)} \u6D3B\u8DC3\u5929\u6570 ${row.activeDays ?? "\u2014"} \u65E5\u5747\u6D88\u8017 ${fmtNum2(typeof row.averageDailyCost === "number" ? row.averageDailyCost : void 0)}
@@ -106227,6 +106307,42 @@ async function runReportMetaOverview(opts) {
106227
106307
  function toSlashDate(date) {
106228
106308
  return date.replace(/-/g, "/");
106229
106309
  }
106310
+ async function lookupMetaAccountForOverview(config, manifestId, verbose = false) {
106311
+ for (const idCandidate of [manifestId, `act_${manifestId}`]) {
106312
+ const params = new URLSearchParams({
106313
+ MediaTypes: "MetaAd|FacebookAds",
106314
+ advStatus: "",
106315
+ mediaAccountState: "Approved,Linked",
106316
+ isForce: "false",
106317
+ pageNum: "1",
106318
+ pageSize: "20",
106319
+ mediaCustomerIds: idCandidate
106320
+ });
106321
+ try {
106322
+ const data = await apiFetch2(
106323
+ `${config.apiBaseUrl}/query/media-account/SearchMediaAcountByCriteria?${params}`,
106324
+ config,
106325
+ {},
106326
+ verbose
106327
+ );
106328
+ for (const item of data?.mas ?? []) {
106329
+ const ma = item.ma ?? {};
106330
+ const cidDigits = metaOverviewManifestId(String(ma.mediaCustomerId ?? ""));
106331
+ if (cidDigits === manifestId) {
106332
+ return {
106333
+ entityId: String(ma.entityId ?? ""),
106334
+ mediaCustomerId: String(ma.mediaCustomerId ?? ""),
106335
+ mediaCustomerName: String(ma.mediaCustomerName ?? ""),
106336
+ mediaAccountType: String(ma.mediaAccountType ?? ""),
106337
+ mediaAccountGroupId: String(ma.mediaAccountGroupId ?? "")
106338
+ };
106339
+ }
106340
+ }
106341
+ } catch {
106342
+ }
106343
+ }
106344
+ return void 0;
106345
+ }
106230
106346
  async function lookupAccountsByCustomerIds(apiBaseUrl, config, mediaType, customerIds, verbose = false) {
106231
106347
  const params = new URLSearchParams({
106232
106348
  MediaType: mediaType,
@@ -106239,14 +106355,17 @@ async function lookupAccountsByCustomerIds(apiBaseUrl, config, mediaType, custom
106239
106355
  {},
106240
106356
  verbose
106241
106357
  );
106242
- const idSet = new Set(customerIds.map((id) => id.trim()));
106358
+ const idSet = new Set(
106359
+ customerIds.map((id) => metaOverviewManifestId(id.trim())).filter(Boolean)
106360
+ );
106243
106361
  const result = /* @__PURE__ */ new Map();
106244
106362
  const items = Array.isArray(data) ? data : [];
106245
106363
  for (const item of items) {
106246
106364
  const ma = item.ma ?? {};
106247
106365
  const cid = String(ma.mediaCustomerId ?? "");
106248
- if (idSet.has(cid)) {
106249
- result.set(cid, {
106366
+ const cidDigits = metaOverviewManifestId(cid);
106367
+ if (idSet.has(cidDigits)) {
106368
+ result.set(cidDigits, {
106250
106369
  entityId: String(ma.entityId ?? ""),
106251
106370
  mediaCustomerId: cid,
106252
106371
  mediaCustomerName: String(ma.mediaCustomerName ?? ""),
@@ -107480,7 +107599,9 @@ function register13(program2) {
107480
107599
  verbose: opts.verbose
107481
107600
  });
107482
107601
  });
107483
- reportCmd.command("meta-overview").description("Meta \u8D26\u6237\u5206\u6790\u603B\u89C8\uFF08OverviewSectionData\uFF0C\u9700 tsoApiBaseUrl \u9274\u6743\uFF09").requiredOption("-a, --account <id>", "Meta \u5E7F\u544A\u8D26\u6237 mediaCustomerId").option("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--token <token>", "Auth Token").option(
107602
+ reportCmd.command("meta-overview").description(
107603
+ "Meta \u8D26\u6237\u5206\u6790\u603B\u89C8\uFF08OverviewSectionData\uFF1BFacebookAds \u6388\u6743\u6237\u8D70 FacebookAds/act_\uFF0CMetaAd \u5F00\u6237\u8D70 MetaAd\uFF09"
107604
+ ).requiredOption("-a, --account <id>", "Meta \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09").option("--start <date>", "\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08\u4E0E --end \u540C\u4F20\u6216\u540C\u7701\u7565\uFF09").option("--end <date>", "\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--token <token>", "Auth Token").option(
107484
107605
  "--json-out <path>",
107485
107606
  "\u843D\u76D8\u76EE\u5F55\u5E76\u66F4\u65B0 report-manifest[-<accountId>].json\uFF1B\u6587\u4EF6\u540D\u4E3A `<section>[-<accountId>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09"
107486
107607
  ).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
@@ -121435,6 +121556,8 @@ function register25(program2) {
121435
121556
  console.log(" CLI \u62C9\u6570\uFF1A");
121436
121557
  console.log(" siluzan-tso website-diagnosis collect --url <url> --json-out ./snap-web");
121437
121558
  console.log(" siluzan-tso website-diagnosis performance --url <url> --json-out ./snap-web");
121559
+ console.log(" \u9ED8\u8BA4\u4EA4\u4ED8 HTML\uFF1A");
121560
+ console.log(" siluzan-tso website-diagnosis render --data ./diagnosis.json --collect ./snap-web/<collect>.json");
121438
121561
  console.log(" Skill\uFF1Areferences/analytics/website-diagnosis-guide.md\n");
121439
121562
  console.log(" \u3010\u8D26\u6237\u5217\u8868 ARIT \u5206\u3011");
121440
121563
  console.log(" list-accounts \u4F1A\u8865\u5145 ma.diagnoseReports \u4E2D reportSource=ARIT \u7684\u5F97\u5206\uFF1B");
@@ -121633,7 +121756,7 @@ async function runWebsiteDiagnosisCollect(opts) {
121633
121756
  htmlPreview,
121634
121757
  ...htmlFull ? { htmlContent: htmlFull } : {},
121635
121758
  ...htmlError ? { htmlError } : {},
121636
- agentHint: "1) \u6309 website-diagnosis-rules.md \u751F\u6210\u8BCA\u65AD JSON\uFF1B2) siluzan-tso website-diagnosis render --data <diagnosis.json> --collect <\u672C\u6587\u4EF6> \u8F93\u51FA\u5E26\u56FE\u8868\u7684 website-diagnosis-report.html\u3002"
121759
+ agentHint: "1) \u6309 website-diagnosis-rules.md \u751F\u6210\u8BCA\u65AD JSON\uFF1B2) siluzan-tso website-diagnosis render --data <diagnosis.json> --collect <\u672C\u6587\u4EF6> \u9ED8\u8BA4\u4EA7\u51FA HTML \u7EC8\u7A3F website-diagnosis-report.html\uFF08\u7981\u6B62\u4EC5 Markdown/JSON \u4EA4\u4ED8\uFF09\u3002"
121637
121760
  };
121638
121761
  if (await emitCliJsonOrSnapshot(
121639
121762
  { jsonOut: opts.jsonOut },
@@ -121756,7 +121879,7 @@ async function runWebsiteDiagnosisRender(opts) {
121756
121879
  // src/commands/website-diagnosis/register.ts
121757
121880
  function registerWebsiteDiagnosisCommands(program2) {
121758
121881
  const root = program2.command("website-diagnosis").description(
121759
- "\u7F51\u7AD9\u8BCA\u65AD\uFF1ALighthouse \u6027\u80FD\u3001HTML \u91C7\u96C6\uFF08Agent download-assets\uFF09\u3001ARIT \u5386\u53F2\u5F97\u5206\u67E5\u8BE2"
121882
+ "\u7F51\u7AD9\u8BCA\u65AD\uFF1ALighthouse \u6027\u80FD\u3001HTML \u91C7\u96C6\u3001ARIT \u5386\u53F2\u5F97\u5206\uFF1B\u9ED8\u8BA4\u4EA4\u4ED8 HTML \u62A5\u544A\uFF08render \u5B50\u547D\u4EE4\uFF09"
121760
121883
  );
121761
121884
  root.command("performance").description("\u62C9\u53D6 Lighthouse \u6027\u80FD\u6570\u636E\uFF08GET WebsiteDiagnosisReports/performance\uFF09").requiredOption("--url <url>", "\u7F51\u7AD9 URL\uFF08\u53EF\u7701\u7565 https://\uFF0C\u81EA\u52A8\u8865\u5168\uFF09").option("--token <token>", "JWT\uFF08\u9ED8\u8BA4\u8BFB config / \u73AF\u5883\u53D8\u91CF\uFF09").option("--verbose", "\u6253\u5370\u8BF7\u6C42\u8BE6\u60C5").option("--json-out <dir>", "\u843D\u76D8 cli-manifest + JSON").action(async (opts) => {
121762
121885
  await runWebsiteDiagnosisPerformance({
@@ -121789,7 +121912,7 @@ function registerWebsiteDiagnosisCommands(program2) {
121789
121912
  }
121790
121913
  );
121791
121914
  root.command("render").description(
121792
- "\u6839\u636E\u8BCA\u65AD JSON \u751F\u6210\u5E26\u56FE\u8868\u7684\u5355\u6587\u4EF6 HTML \u62A5\u544A\uFF08website-diagnosis-report.html\uFF09"
121915
+ "\u6839\u636E\u8BCA\u65AD JSON \u751F\u6210 HTML \u7EC8\u7A3F\uFF08\u9ED8\u8BA4\u4EA4\u4ED8\u683C\u5F0F\uFF0Cwebsite-diagnosis-report.html\uFF09"
121793
121916
  ).requiredOption("--data <file>", "\u8BCA\u65AD\u7ED3\u679C JSON\uFF08getWebsiteDiagnosisData \u540C\u7ED3\u6784\uFF09").option("--collect <file>", "\u53EF\u9009\uFF1Acollect \u843D\u76D8 JSON\uFF0C\u7528\u4E8E\u5408\u5E76 lighthouse").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").action(async (opts) => {
121794
121917
  await runWebsiteDiagnosisRender({
121795
121918
  dataFile: opts.data,
@@ -122429,52 +122552,8 @@ var FACEBOOK_SECTION_ALIASES = {
122429
122552
  videos: "material"
122430
122553
  };
122431
122554
 
122432
- // src/commands/facebook-analysis/fetch.ts
122433
- init_auth();
122434
- function normalizeFacebookAccountId(raw) {
122435
- const t = raw.trim();
122436
- if (!t) {
122437
- console.error("\n\u274C --account \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
122438
- process.exit(1);
122439
- }
122440
- const digits = t.startsWith("act_") ? t.slice(4) : t;
122441
- if (!/^\d+$/.test(digits)) {
122442
- console.error(
122443
- "\n\u274C --account \u987B\u4E3A Facebook \u5E7F\u544A\u8D26\u6237 ID\uFF08\u6570\u5B57 mediaCustomerId\uFF0C\u6216 act_<\u6570\u5B57>\uFF0C\u4E0E list-accounts -m MetaAd \u4E00\u81F4\uFF09\u3002\n"
122444
- );
122445
- process.exit(1);
122446
- }
122447
- return { apiId: `act_${digits}`, manifestId: digits };
122448
- }
122449
- function resolveFacebookDateRange(start, end) {
122450
- if (start && end) return { startDate: start, endDate: end };
122451
- if (!start && !end) {
122452
- const endD = /* @__PURE__ */ new Date();
122453
- endD.setDate(endD.getDate() - 1);
122454
- const startD = new Date(endD);
122455
- startD.setDate(startD.getDate() - 6);
122456
- const fmt2 = (d) => d.toISOString().slice(0, 10);
122457
- return { startDate: fmt2(startD), endDate: fmt2(endD) };
122458
- }
122459
- console.error("\n\u274C --start \u4E0E --end \u987B\u540C\u65F6\u4F20\u5165\uFF0C\u6216\u540C\u65F6\u7701\u7565\u4EE5\u4F7F\u7528\u9ED8\u8BA4\u8FD1 7 \u5929\uFF08\u622A\u81F3\u6628\u5929\uFF09\u3002\n");
122460
- process.exit(1);
122461
- }
122462
- function facebookReportingUrl(config, apiId, segment, query) {
122463
- const q = query ? query.startsWith("?") ? query : `?${query}` : "";
122464
- return `${config.apiBaseUrl}/reporting/media-account/FacebookAds/${encodeURIComponent(apiId)}/${segment}${q}`;
122465
- }
122466
- async function fetchFacebookSectionPayload(def, opts, config, apiId) {
122467
- const { startDate, endDate } = resolveFacebookDateRange(opts.start, opts.end);
122468
- const params = new URLSearchParams({ startDate, endDate });
122469
- if (def.countryLimitOption && opts.limit !== void 0 && Number.isFinite(opts.limit)) {
122470
- params.set("limit", String(Math.max(1, Math.floor(opts.limit))));
122471
- }
122472
- const url = facebookReportingUrl(config, apiId, def.segment, params.toString());
122473
- return apiFetch2(url, config, {}, opts.verbose ?? false);
122474
- }
122475
- function endpointHintForFacebookSection(def, apiId) {
122476
- return `GET \u2026/FacebookAds/${apiId}/${def.segment}`;
122477
- }
122555
+ // src/commands/facebook-analysis/register-cli.ts
122556
+ import { Command } from "commander";
122478
122557
 
122479
122558
  // src/commands/facebook-analysis/render-report.ts
122480
122559
  import fs17 from "fs";
@@ -122746,20 +122825,19 @@ async function runFacebookAnalysisRender(opts) {
122746
122825
  await fsPromises2.writeFile(outPath, injectReportData2(html, data), "utf8");
122747
122826
  const runtimeSrc = metaPeriodReportRuntimePath();
122748
122827
  const runtimeOut = path24.join(outDir, TEMPLATE_BASENAMES2.runtime);
122828
+ let runtimeCopied = false;
122749
122829
  try {
122750
122830
  await fsPromises2.copyFile(runtimeSrc, runtimeOut);
122831
+ runtimeCopied = true;
122751
122832
  } catch {
122752
- console.error(`
122753
- \u274C \u672A\u627E\u5230\u62A5\u544A\u8FD0\u884C\u65F6\uFF1A${runtimeSrc}
122754
- \u8BF7\u5148\u6267\u884C npm run build
122755
- `);
122756
- process.exit(1);
122757
122833
  }
122758
122834
  console.log(`
122759
122835
  \u2705 Meta/Facebook \u5468\u671F\u5206\u6790 HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
122760
122836
  `);
122761
- console.log(` \u8FD0\u884C\u65F6\u811A\u672C\uFF1A${runtimeOut}
122837
+ if (runtimeCopied) {
122838
+ console.log(` \u8FD0\u884C\u65F6\u811A\u672C\uFF1A${runtimeOut}
122762
122839
  `);
122840
+ }
122763
122841
  console.log("\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u56FE\u8868\u4E0E\u5B8C\u6574\u7AE0\u8282\uFF08\u9700\u8054\u7F51\u52A0\u8F7D Chart.js CDN\uFF09\u3002\n");
122764
122842
  }
122765
122843
 
@@ -122887,10 +122965,9 @@ async function runAllFacebookSections(opts) {
122887
122965
  }
122888
122966
 
122889
122967
  // src/commands/facebook-analysis/register-cli.ts
122890
- function registerFacebookAnalysisCommands(program2) {
122891
- const sectionHelp = FACEBOOK_SECTION_NAMES.join(", ");
122892
- const root = program2.command("facebook-analysis").description(
122893
- "Facebook Ads \u8D26\u6237\u5206\u6790\u6279\u91CF\u62C9\u53D6\uFF08TSO reporting/media-account/FacebookAds/\u2026\uFF0C7 \u4E2A Section\uFF09"
122968
+ function registerBatchCommand(sectionHelp) {
122969
+ return new Command("run").description(
122970
+ "\u6279\u91CF\u62C9\u53D6 TSO reporting/media-account/FacebookAds Section \u6570\u636E\uFF08\u7701\u7565\u5B50\u547D\u4EE4\u540D\u65F6\u9ED8\u8BA4\u6267\u884C\u672C\u547D\u4EE4\uFF09"
122894
122971
  ).requiredOption(
122895
122972
  "-a, --account <id>",
122896
122973
  "Facebook \u5E7F\u544A\u8D26\u6237 mediaCustomerId\uFF08\u6570\u5B57\u6216 act_<\u6570\u5B57>\uFF09"
@@ -122910,6 +122987,13 @@ function registerFacebookAnalysisCommands(program2) {
122910
122987
  ).option("--concurrency <n>", "\u5E76\u53D1\u6570\uFF0C\u9ED8\u8BA4 5\uFF0C\u4E0A\u9650 16", (v) => parseInt(v, 10)).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
122911
122988
  await runAllFacebookSections(opts);
122912
122989
  });
122990
+ }
122991
+ function registerFacebookAnalysisCommands(program2) {
122992
+ const sectionHelp = FACEBOOK_SECTION_NAMES.join(", ");
122993
+ const root = program2.command("facebook-analysis").description(
122994
+ "Facebook Ads \u8D26\u6237\u5206\u6790\uFF1A\u6279\u91CF\u62C9\u53D6 Section \u6570\u636E\uFF08\u9ED8\u8BA4\uFF09\uFF0C\u6216\u7531 render \u751F\u6210 HTML \u5468\u671F\u62A5\u544A"
122995
+ );
122996
+ root.addCommand(registerBatchCommand(sectionHelp), { isDefault: true });
122913
122997
  root.command("render").description(
122914
122998
  "\u6839\u636E Agent \u64B0\u5199\u7684\u62A5\u544A JSON \u751F\u6210 Meta/Facebook \u5468\u671F\u5206\u6790 HTML \u7EC8\u7A3F\uFF08meta-period-report.html\uFF09"
122915
122999
  ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684 meta-period-report.json").option(
@@ -122938,7 +123022,7 @@ function getVersion() {
122938
123022
  return "0.0.0";
122939
123023
  }
122940
123024
  }
122941
- var program = new Command();
123025
+ var program = new Command2();
122942
123026
  program.name("siluzan-tso").description(
122943
123027
  "Siluzan \u5E7F\u544A\u8D26\u6237\u7BA1\u7406\uFF1A\u8D26\u6237\u67E5\u8BE2\u3001\u4F59\u989D\u3001\u6295\u653E\u6570\u636E\u3001\u5F00\u6237\u7533\u8BF7\uFF08Google/TikTok/Yandex/Bing/Kwai\uFF09\u3001\n\u8D26\u53F7\u5206\u4EAB/\u89E3\u7ED1\u3001\u4F18\u5316\u62A5\u544A\u3001Google \u8D26\u6237\u5206\u6790\u7F51\u5173\uFF08google-analysis\uFF09\u3001\u7F51\u7AD9\u8BCA\u65AD\uFF08website-diagnosis\uFF09\u3001\u6218\u7565\u5E02\u573A\u5206\u6790\uFF08market-analysis\uFF0CAgent \u751F\u6210\u62A5\u544A\uFF09\u3001\u5145\u503C\u8F6C\u8D26\u3001\u5F00\u7968\u3001\u667A\u80FD\u9884\u8B66\u3001Google \u5E7F\u544A\u7BA1\u7406\uFF08\u542B\u5F02\u6B65\u6279\u91CF\uFF09\u3002"
122944
123028
  ).version(getVersion());
@@ -19,6 +19,8 @@ allowed-tools: Bash(siluzan-tso:*) Read Write
19
19
  >
20
20
  > **报告/Excel 交付前**:Read `references/core/deliverable-preflight.md`,**Read 最终产物**并按自检表确认币种与章节完整;币种只认当次 `list-accounts` 的 `currencyCode`。
21
21
  >
22
+ > **HTML 终稿类报告(用户未指定格式时默认 HTML)**:网站诊断 P8(`website-diagnosis render`)、Meta 周期 P4-FB(`facebook-analysis render`)、战略市场 P9(`market-analysis render`)。Agent 只写 JSON,**禁止**仅交付 Markdown 摘要或纯 JSON 充当终稿。
23
+ >
22
24
  > **开户**:首次进入开户话题须先向用户罗列该媒体(或未指明媒体时六平台)**全部必填项**,见 `references/accounts/open-account-by-media.md` §「首次响应硬规范」。
23
25
  >
24
26
  > **Subagent(可选)**:若宿主支持 Task / 子会话,复杂报告(P5/P6/P7)或长 CLI 输出前 Read `references/core/subagent-orchestration.md`,**自行决定**是否委派;写操作确认与对用户的最终交付留在主 Agent。
@@ -84,10 +86,10 @@ Windows:部分 Agent 通过 PowerShell 代执行时可能失败,改在 [Git
84
86
  | 用户意图(关键词) | 必读(Read 后再 `-h` / 执行) |
85
87
  | ------------------ | ----------------------------- |
86
88
  | 任意任务(首次) | `core/agent-conventions.md` |
87
- | 账户列表 / 余额 / 消耗 / 分享 / MCC / 多账户汇总 | `accounts/accounts.md`;金额加 `accounts/currency.md` |
89
+ | 账户列表 / 有多少 / 列出全部 / 余额 / 消耗 / 分享 / MCC | `accounts/accounts.md`(§ list-accounts **Agent 意图速查**:`-m <媒体> --page-size 999 --json-out` 一步拉全);金额加 `accounts/currency.md` |
88
90
  | google ads 拉数 / 报告 / 周报 / `google-analysis` | `analytics/account-analytics.md` + `core/tips.md` + **`core/deliverable-preflight.md`**;多账户加 `analytics/google-analysis-batch.md` |
89
- | Meta/Facebook 周期或诊断报告 / `facebook-analysis` | `report-templates/meta-period-report-excel.md`(Excel Sheet 基准)+ `meta-period-report.md` + `analytics/facebook-analysis-guide.md`;Agent `meta-period-report.json`,`facebook-analysis render` 出 HTML |
90
- | 网站诊断 / 落地页评分 / `website-diagnosis` | `analytics/website-diagnosis-guide.md` + `assets/website-diagnosis-rules.md` + `core/playbooks.md` P8Agent 只写 JSON,`website-diagnosis render` HTML |
91
+ | Meta/Facebook 周期或诊断报告 / `facebook-analysis` | **默认 HTML**:`meta-period-report.md` + **`assets/meta-period-report-rules.md`**(内容丰富度必读)+ `facebook-analysis-guide.md` + `deliverable-preflight.md` → 拉数 → 分析 → JSON(4 条建议≥150字+7维补充+HTML扩展)→ `render`;**仅用户要 Excel** 时加读 `meta-period-report-excel.md` |
92
+ | 网站诊断 / 落地页评分 / `website-diagnosis` | `analytics/website-diagnosis-guide.md` + `assets/website-diagnosis-rules.md` + `core/playbooks.md` P8 + `core/deliverable-preflight.md`;Agent 只写 JSON,**默认交付** `website-diagnosis render` 产出的 **HTML**(禁止仅 Markdown/JSON) |
91
93
  | 市场分析 / 行业分析 / 战略市场报告 / `market-analysis` | `analytics/market-analysis-guide.md` + `assets/market-analysis-rules.md`(原始业务维度清单)+ `report-templates/market-analysis-report.md` + `core/playbooks.md` P9;`collect` → Agent 按维度调研写 `market-report.json` → `render`(缺项校验) |
92
94
  | Google 新建搜索系列 | `google-ads/google-ads-campaign-plan.md` |
93
95
  | Google 广告 CRUD / 拒审 | `google-ads/google-ads.md` |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.28-beta.1",
4
- "publishedAt": 1780888059129
3
+ "version": "1.1.28-beta.3",
4
+ "publishedAt": 1780909080666
5
5
  }
@@ -0,0 +1,169 @@
1
+ # Meta 周期报告撰写规则(Agent 用)
2
+
3
+ > 对齐 **P8 网站诊断** 的「结构化 + 全量覆盖 + 每条必有建议」思路。
4
+ > 源:业务 Excel 模板(`无锡顺晟Facebook4月报告.xlsx`)+ `meta-period-report.html` 深度分析章节。
5
+ > **禁止**只写 KPI 表、4 条一句话建议或空白占位后交付。
6
+
7
+ ---
8
+
9
+ ## 与网站诊断的对照(为何 FB 报告会显得「建议太少」)
10
+
11
+ | 网站诊断(P8) | Meta 周期报告(P4-FB) |
12
+ | -------------- | ---------------------- |
13
+ | `website-diagnosis-rules.md` 规定 29 子项全覆盖 | 本文件规定 **5 维数据 + 叙事 + 建议** 全覆盖 |
14
+ | 每项 `issue` + `suggestion`(较差项以「强烈建议:」开头) | 每条建议 `content` 须 **引用当次数字** + **可执行动作** |
15
+ | `coreIssuesIds` + `PriorityPlan` 高/中/低 | `priorityPlan` + `narrative.recommendations` 4 条 + `supplementaryRecommendations` |
16
+ | HTML 由 `render` 渲染全部章节 | HTML **必须**填 `executiveSummary`、`healthDiagnosis`、`sections.*.insight` 等(见下) |
17
+ | 交付前 Read HTML 对照章节清单 | 交付前 Read HTML/xlsx 对照 `deliverable-preflight.md` § P4-FB |
18
+
19
+ **常见失误(导致建议偏少)**:
20
+
21
+ 1. 只写 Excel 叙事 4 条标题、每条 1~2 句泛化话术,未引用 CPL/花费/国家名。
22
+ 2. HTML 默认路径只填 `narrative`,**未填** `executiveSummary` / `healthDiagnosis` / `sections.insight`(模板原先不展示 `narrative.recommendations`,现已修复)。
23
+ 3. 未按 **7 维数据驱动清单** 逐条给建议,只写「优化素材」「调整预算」等空话。
24
+
25
+ ---
26
+
27
+ ## JSON 结构(摘要)
28
+
29
+ 完整 Schema:`assets/meta-period-report.schema.json`。
30
+ **Excel 与 HTML 共用** `narrative`;**HTML 默认交付**还须填「深度分析扩展」字段。
31
+
32
+ ---
33
+
34
+ ## 一、Excel / 总数据叙事(`narrative` · 必填)
35
+
36
+ ### 1. `narrative.overall`(整体表现)
37
+
38
+ | 要求 | 细则 |
39
+ | ---- | ---- |
40
+ | 字数 | **≥ 120 字**(中文) |
41
+ | 必含数字 | 花费、`results`、CPL(`costPerResult`)、`reach`、`impressions`、`frequency` **至少 5 项** |
42
+ | 必含口径 | `resultType`、`attributionSetting`(写入 `meta` 并在段中提及) |
43
+ | 可选 | 若有 `previousPeriod`,写 1~2 句环比(花费 ±%、CPL ±%) |
44
+
45
+ ### 2. `narrative.regional[]`(区域 / 广告组)
46
+
47
+ | 要求 | 细则 |
48
+ | ---- | ---- |
49
+ | 条数 | **每个** `ad-sets` 中花费 >0 的广告组 **各 1 段**(不得合并成 1 句带过) |
50
+ | 字数 | 每段 **≥ 80 字** |
51
+ | 必含 | 组名、花费占比或绝对值、线索数、`costPerResult`、频次;判断「曝光不足 / 频次过高 / 效率领先」 |
52
+ | 数据依据 | `ad-sets-<id>.json` → `adGroups[]` |
53
+
54
+ ### 3. `narrative.country`(国家报告)
55
+
56
+ | 要求 | 细则 |
57
+ | ---- | ---- |
58
+ | 字数 | **≥ 80 字** |
59
+ | 必含 | CPL **最低** 与 **最高** 国家名及具体 CPL;中间梯队 1~2 国;花费集中度 |
60
+ | 数据依据 | `country-<id>.json` → `countries[]` |
61
+
62
+ ### 4. `narrative.recommendations[]`(固定 4 条标题)
63
+
64
+ **必须恰好 4 条**,`title` 仅限下表枚举;`content` 须**数据驱动**,不得模板空话。
65
+
66
+ | title | content 最低要求(每条 **≥ 150 字**) |
67
+ | ----- | ------------------------------------- |
68
+ | **简化表单问题** | 结合 **高频次 + 高 CPL** 的国家/组(写出名称与数字);建议字段从 N 减到 3;说明预期对 CPL 的影响 |
69
+ | **区域调整** | 按 **具体广告组** 写语言/市场策略(如土/葡/德);引用各组 CPL 对比;写清「加谁、减谁、暂停谁」 |
70
+ | **预算重构** | 给出 **具体比例**(如 4:3:3 或按 spend 占比调整);测试周期(7/14 天);优胜劣汰阈值(CPL > 账户均值 ×1.2 则减 30%) |
71
+ | **素材建议** | 按 **平台**(IG/FB)与 **国家** 分述;素材套数、视频时长、形态;至少点名 1 个高花费低结果创意/版位 |
72
+
73
+ **建议前缀(对齐网站诊断)**:
74
+
75
+ - 问题严重(CPL > 均值 1.3 倍、频次 >2.5、零成效花费 >$50)→ `content` 以 **「强烈建议:」** 开头
76
+ - 其余优化项 → **「推荐优化:」** 开头
77
+
78
+ ---
79
+
80
+ ## 二、数据驱动补充建议(`supplementaryRecommendations` · 必填)
81
+
82
+ 在 4 条固定标题之外,**必须**再写 **7 维清单**,每条 `{ "dimension", "issue", "suggestion" }`:
83
+
84
+ | dimension | issue(发现了什么) | suggestion(怎么办) |
85
+ | --------- | ------------------- | -------------------- |
86
+ | 预算与广告组 | 点名高 `spendPercentage`、差 `costPerResult` 的组 | 降预算 %、暂停、或合并受众 |
87
+ | 平台与版位 | `publisherPlatform` + `platformPosition` 组合 | 减投差版位、加码 winner 平台 |
88
+ | 地域 | 高消耗低结果国家 | geo 排除 / 收窄 / 单独组 |
89
+ | 受众 | 低效 age×gender | 排除或降 bid;扩量高效段 |
90
+ | 创意 | 高花费低 `results` 的 ad | 关停、复制 winner 结构 |
91
+ | 频次与疲劳 | `frequency` >2.5 且转化差 | 扩受众、换创意、降预算 |
92
+ | 接口限制 | 无按日/关键词等 | 一句说明「Meta 接口未提供」,**禁止编造** |
93
+
94
+ 每条 `suggestion` **≥ 60 字**,须含 **至少 1 个当次数字或名称**。
95
+
96
+ ---
97
+
98
+ ## 三、优先级改进计划(`priorityPlan` · HTML 必填 · Excel 推荐)
99
+
100
+ 对齐网站诊断 `PriorityPlan`:
101
+
102
+ ```jsonc
103
+ {
104
+ "priorityPlan": {
105
+ "high": ["…", "…"], // ≥2 条,本周必须做
106
+ "medium": ["…", "…"], // ≥2 条
107
+ "low": ["…", "…"] // ≥2 条,持续优化
108
+ }
109
+ }
110
+ ```
111
+
112
+ 每条 **≥ 40 字**,带责任维度(预算/素材/地域等)。
113
+
114
+ ---
115
+
116
+ ## 四、HTML 默认交付 · 深度分析扩展(必填)
117
+
118
+ 用户未指定 Excel 时,除 §一~§三外 **还必须** 填写:
119
+
120
+ | 字段 | 最低要求 |
121
+ | ---- | -------- |
122
+ | `executiveSummary` | **3~5 段**,每段 **≥ 80 字**;解释「为什么」而不只报数;可拆自 `narrative.overall` 但须加深因果 |
123
+ | `healthDiagnosis.lifecyclePhase` | `test-market` / `find-winner` / `scale` 三选一 |
124
+ | `healthDiagnosis.lifecycleVerdict` | **≥ 60 字**,结合总花费与维度分散度 |
125
+ | `healthDiagnosis.fourQuestions` | **恰好 4 张卡片**(钱花得值不值 / 赢在哪 / 输在哪 / 下月重点) |
126
+ | 每张 `fourQuestions[]` | `verdict` + `evidence` **≥2 条**(含数字)+ `action` **≥ 40 字** |
127
+ | `healthDiagnosis.scorecard` | **≥ 6 行**(平台、国家、广告组、受众等分项红绿灯) |
128
+ | `sections.platform.insight` | **≥ 200 字** |
129
+ | `sections.country.insight` | **≥ 200 字** |
130
+ | `sections.adSets.insight` | **≥ 200 字** |
131
+ | `sections.audience` | `goldenProfile` **≥3 条** + `antiProfile` **≥2 条** + `insight` **≥ 150 字** |
132
+ | `sections.landingPage.rows` | **≥ 3 行**(心理阻碍 / 数据信号 / 推演 / 优先级) |
133
+ | `abTests` | **≥ 3 个**实验(变量、假设、成功标准) |
134
+ | `actionChecklist` | `today` **≥2**、`thisWeek` **≥3**、`thisMonth` **≥3** 条可执行项 |
135
+
136
+ **禁止**:HTML 中大量「(待 Agent 撰写)」占位;交付前 Read HTML 确认无空节。
137
+
138
+ ---
139
+
140
+ ## 五、撰写流程(对齐 P8)
141
+
142
+ 1. **拉数**:`facebook-analysis` + `--json-out`(默认 5 维)。
143
+ 2. **脚本读盘**:聚合 KPI、Top/Bottom 国家、平台、受众、广告组(禁止 Read 业务 JSON 进对话)。
144
+ 3. **先 outline 后 JSON**:列出将引用的数字与 7 维建议要点,再写 `meta-period-report.json`。
145
+ 4. **渲染**:`facebook-analysis render`(HTML)或脚本写 xlsx(Excel)。
146
+ 5. **Read 终稿**:对照 `deliverable-preflight.md` § P4-FB。
147
+
148
+ ---
149
+
150
+ ## 六、自检清单(Agent 交付前勾选)
151
+
152
+ - [ ] `narrative.recommendations` 4 条,每条 content ≥150 字且含真实数字
153
+ - [ ] `supplementaryRecommendations` 7 维齐全
154
+ - [ ] `priorityPlan` high/medium/low 各 ≥2 条
155
+ - [ ] HTML:`executiveSummary` ≥3 段、`fourQuestions` =4、`scorecard` ≥6 行
156
+ - [ ] HTML:`sections.platform/country/adSets.insight` 各 ≥200 字
157
+ - [ ] HTML:`actionChecklist` 三列非空;`abTests` ≥3
158
+ - [ ] Excel:总数据 Sheet 叙事块 4 节齐全,无 1 句话敷衍
159
+ - [ ] 未编造 Meta 无接口维度(按日、关键词等)
160
+
161
+ ---
162
+
163
+ ## 相关文档
164
+
165
+ - `report-templates/meta-period-report.md` — 流程与章节
166
+ - `report-templates/meta-period-report-excel.md` — Excel 五 Sheet
167
+ - `references/analytics/facebook-analysis-guide.md` — API 字段
168
+ - `references/core/deliverable-preflight.md` — 交付审阅
169
+ - `assets/website-diagnosis-rules.md` — 网站诊断(对照参考)
@@ -60,8 +60,9 @@
60
60
  },
61
61
  "recommendations": {
62
62
  "type": "array",
63
- "description": "优化建议固定 4 ",
64
- "minItems": 1,
63
+ "description": "优化建议固定 4 条;每条 content ≥150 字且引用当次数据(见 meta-period-report-rules.md)",
64
+ "minItems": 4,
65
+ "maxItems": 4,
65
66
  "items": {
66
67
  "type": "object",
67
68
  "required": ["title", "content"],
@@ -70,12 +71,39 @@
70
71
  "type": "string",
71
72
  "enum": ["简化表单问题", "区域调整", "预算重构", "素材建议"]
72
73
  },
73
- "content": { "type": "string" }
74
+ "content": {
75
+ "type": "string",
76
+ "description": "≥150 字;严重问题以「强烈建议:」开头,其余以「推荐优化:」开头"
77
+ }
74
78
  }
75
79
  }
76
80
  }
77
81
  }
78
82
  },
83
+ "supplementaryRecommendations": {
84
+ "type": "array",
85
+ "description": "7 维数据驱动补充建议(预算/平台/地域/受众/创意/频次/接口限制);每条 suggestion ≥60 字",
86
+ "minItems": 7,
87
+ "maxItems": 7,
88
+ "items": {
89
+ "type": "object",
90
+ "required": ["dimension", "issue", "suggestion"],
91
+ "properties": {
92
+ "dimension": { "type": "string" },
93
+ "issue": { "type": "string" },
94
+ "suggestion": { "type": "string" }
95
+ }
96
+ }
97
+ },
98
+ "priorityPlan": {
99
+ "type": "object",
100
+ "description": "高/中/低优先级改进计划(对齐网站诊断 PriorityPlan)",
101
+ "properties": {
102
+ "high": { "type": "array", "items": { "type": "string" }, "minItems": 2 },
103
+ "medium": { "type": "array", "items": { "type": "string" }, "minItems": 2 },
104
+ "low": { "type": "array", "items": { "type": "string" }, "minItems": 2 }
105
+ }
106
+ },
79
107
  "tables": {
80
108
  "type": "object",
81
109
  "description": "Sheet2~5 表格数据;可省略,由 --snapshot-dir 合并",
@@ -106,11 +134,12 @@
106
134
  "executiveSummary": {
107
135
  "type": "array",
108
136
  "items": { "type": "string" },
109
- "description": "HTML 深度分析扩展:多段执行摘要(可选,优先用 narrative.overall)"
137
+ "minItems": 3,
138
+ "description": "HTML 默认交付必填:3~5 段执行摘要,每段 ≥80 字;可深化 narrative.overall"
110
139
  },
111
140
  "healthDiagnosis": {
112
141
  "type": "object",
113
- "description": "HTML 深度分析扩展:账户健康诊断(Excel 不含)",
142
+ "description": "HTML 默认交付必填:四问卡片 + 红绿灯表(Excel 不含)",
114
143
  "properties": {
115
144
  "lifecyclePhase": { "enum": ["test-market", "find-winner", "scale"] },
116
145
  "lifecycleVerdict": { "type": "string" },