siluzan-tso-cli 1.1.30-beta.2 → 1.1.30-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.
Files changed (76) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1610 -237
  3. package/dist/skill/SKILL.md +6 -4
  4. package/dist/skill/_meta.json +2 -2
  5. package/dist/skill/references/analytics/market-analysis-guide.md +8 -0
  6. package/dist/skill/references/core/agent-conventions.md +2 -0
  7. package/dist/skill/references/core/playbooks.md +8 -6
  8. package/dist/skill/references/report-templates/google-account-diagnosis-report.md +8 -0
  9. package/dist/skill/references/report-templates/google-ads-diagnosis.md +62 -3
  10. package/dist/skill/report-templates/GoogleAdsDiagnosisReport.html +6177 -0
  11. package/dist/skill/report-templates/google-account-diagnosis-report.md +8 -0
  12. package/dist/skill/report-templates/google-ads-diagnosis.md +62 -3
  13. package/dist/skill/scripts/install.ps1 +1 -1
  14. package/dist/skill/scripts/install.sh +1 -1
  15. package/eval/cases/accounts-entityid-vs-mediaccustomerid.scenario.json +14 -2
  16. package/eval/cases/accounts-mcc-bind-inquiry.scenario.json +3 -1
  17. package/eval/cases/accounts-single-balance-not-bulk.scenario.json +14 -3
  18. package/eval/cases/budget-display-not-raw-micros.scenario.json +8 -1
  19. package/eval/cases/clue-meta-leads-json.scenario.json +14 -2
  20. package/eval/cases/clue-tiktok-leads-json.scenario.json +11 -2
  21. package/eval/cases/destructive-account-delink-needs-confirm.scenario.json +9 -3
  22. package/eval/cases/destructive-forewarning-delete-needs-confirm.scenario.json +9 -3
  23. package/eval/cases/destructive-invoice-apply-needs-confirm.scenario.json +9 -3
  24. package/eval/cases/facebook-analysis-google-section-aliases.scenario.json +11 -2
  25. package/eval/cases/facebook-analysis-not-google-keywords.scenario.json +10 -3
  26. package/eval/cases/facebook-analysis-period-default.scenario.json +14 -2
  27. package/eval/cases/finance-invoice-info-list.scenario.json +11 -3
  28. package/eval/cases/forewarning-list-google.scenario.json +14 -3
  29. package/eval/cases/google-ads-no-structural-without-confirm.scenario.json +6 -2
  30. package/eval/cases/google-analysis-keywords-route.scenario.json +14 -2
  31. package/eval/cases/human-p1-multiturn.scenario.json +5 -1
  32. package/eval/cases/meta-single-balance-not-bulk.scenario.json +17 -3
  33. package/eval/cases/no-legacy-json-flag.scenario.json +6 -2
  34. package/eval/cases/open-account-google-noninteractive.scenario.json +3 -1
  35. package/eval/cases/open-account-tiktok-license-file.scenario.json +3 -1
  36. package/eval/cases/optimize-list-by-account.scenario.json +11 -3
  37. package/eval/cases/p1-single-account-profile.scenario.json +11 -1
  38. package/eval/cases/p2-balance-scan-bulk.scenario.json +9 -2
  39. package/eval/cases/p3-accounts-digest.scenario.json +5 -1
  40. package/eval/cases/p4-fb-meta-period-report.scenario.json +12 -2
  41. package/eval/cases/p4-period-report-window.scenario.json +8 -1
  42. package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +13 -2
  43. package/eval/cases/pmax-brand-edit-routing.scenario.json +16 -3
  44. package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +12 -2
  45. package/eval/cases/pmax-enable-brand-guidelines.scenario.json +12 -2
  46. package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +14 -3
  47. package/eval/cases/rag-before-keyword-expand.scenario.json +11 -1
  48. package/eval/cases/rag-list-then-query.scenario.json +14 -2
  49. package/eval/cases/report-list-google.scenario.json +11 -2
  50. package/eval/cases/report-push-list-google.scenario.json +11 -2
  51. package/eval/cases/setup-login-or-env.scenario.json +3 -1
  52. package/eval/cases/setup-siluzan-data-permission-env.scenario.json +3 -1
  53. package/eval/cases/tiktok-bc-bind-inquiry.scenario.json +6 -2
  54. package/eval/cases/time-range-user-delegates-default.scenario.json +8 -1
  55. package/eval/cases/tips-json-out-filtering.scenario.json +3 -1
  56. package/eval/cases/tips-large-json-pagination.scenario.json +3 -1
  57. package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +11 -2
  58. package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +3 -1
  59. package/eval/cases/uj-analytics-30d-pdf-campaign-device-geo.scenario.json +18 -6
  60. package/eval/cases/uj-analytics-compare-google-tiktok-last-month-roi.scenario.json +8 -1
  61. package/eval/cases/uj-analytics-ecommerce-industry-report.scenario.json +25 -0
  62. package/eval/cases/uj-analytics-google-weekly-trends-campaigns-keywords.scenario.json +11 -2
  63. package/eval/cases/uj-analytics-report-push-weekly-email.scenario.json +3 -1
  64. package/eval/cases/uj-finance-invoice-records-this-month.scenario.json +11 -2
  65. package/eval/cases/uj-life-newbie-siluzan-google-end-to-end.scenario.json +4 -1
  66. package/eval/cases/uj-ops-google-accounts-list-normal.scenario.json +17 -2
  67. package/eval/cases/uj-ops-google-yesterday-spend-conversions.scenario.json +14 -2
  68. package/eval/cases/uj-ops-pause-worst-adgroup-confirm.scenario.json +6 -2
  69. package/eval/cases/uj-ops-tiktok-leads-last-week.scenario.json +17 -3
  70. package/eval/cases/uj-patrol-cpc-spike-adgroups-over-15.scenario.json +5 -1
  71. package/eval/cases/uj-patrol-forewarning-create-daily-cap-3000.scenario.json +3 -1
  72. package/eval/cases/uj-patrol-forewarning-trigger-records.scenario.json +17 -3
  73. package/eval/cases/uj-patrol-google-balances-low.scenario.json +11 -2
  74. package/eval/cases/uj-roi-optimize-records-then-execute-cautiously.scenario.json +14 -3
  75. package/eval/cases/uj-roi-search-terms-add-negative-keywords.scenario.json +14 -2
  76. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -258,9 +258,9 @@ var require_semver = __commonJS({
258
258
  } else {
259
259
  this.prerelease = m[4].split(".").map((id) => {
260
260
  if (/^[0-9]+$/.test(id)) {
261
- const num4 = +id;
262
- if (num4 >= 0 && num4 < MAX_SAFE_INTEGER) {
263
- return num4;
261
+ const num5 = +id;
262
+ if (num5 >= 0 && num5 < MAX_SAFE_INTEGER) {
263
+ return num5;
264
264
  }
265
265
  }
266
266
  return id;
@@ -2119,6 +2119,62 @@ function validateBaseUrl(raw) {
2119
2119
  }
2120
2120
  return null;
2121
2121
  }
2122
+ function hasSiluzanAgentCredentials() {
2123
+ const apiKey = process.env.SILUZAN_API_KEY?.trim();
2124
+ const authToken = process.env.SILUZAN_AUTH_TOKEN?.trim();
2125
+ return Boolean(apiKey || authToken);
2126
+ }
2127
+ function isSiluzanAgentEnv() {
2128
+ const raw = process.env.IS_SILUZAN_AGENT_ENV?.trim().toLowerCase();
2129
+ return raw === "true" || raw === "1";
2130
+ }
2131
+ function skipAuthSetupInAgentEnv(commandLabel) {
2132
+ if (!isSiluzanAgentEnv()) return false;
2133
+ if (hasSiluzanAgentCredentials()) {
2134
+ console.log(
2135
+ `
2136
+ \u2139\uFE0F \u68C0\u6D4B\u5230 Siluzan Agent \u73AF\u5883\uFF08IS_SILUZAN_AGENT_ENV=true\uFF09\uFF0C\u51ED\u636E\u5DF2\u7531\u6C99\u7BB1\u6CE8\u5165\uFF0C\u65E0\u9700\u6267\u884C ${commandLabel}\u3002
2137
+ `
2138
+ );
2139
+ } else {
2140
+ console.log(
2141
+ `
2142
+ \u2139\uFE0F \u68C0\u6D4B\u5230 Siluzan Agent \u73AF\u5883\uFF08IS_SILUZAN_AGENT_ENV=true\uFF09\u3002
2143
+ ${commandLabel} \u5DF2\u8DF3\u8FC7\uFF1B\u8BF7\u7531 Agent \u6C99\u7BB1\u6CE8\u5165 SILUZAN_API_KEY \u6216 SILUZAN_AUTH_TOKEN\u3002
2144
+ `
2145
+ );
2146
+ }
2147
+ return true;
2148
+ }
2149
+ function trimOrUndefined(value) {
2150
+ const trimmed = value?.trim();
2151
+ return trimmed ? trimmed : void 0;
2152
+ }
2153
+ function resolveAgentEnvCredentials() {
2154
+ const envToken = trimOrUndefined(process.env.SILUZAN_AUTH_TOKEN);
2155
+ if (envToken) return { authToken: envToken, apiKey: void 0 };
2156
+ const envApiKey = trimOrUndefined(process.env.SILUZAN_API_KEY);
2157
+ if (envApiKey) return { authToken: "", apiKey: envApiKey };
2158
+ return { authToken: "", apiKey: void 0 };
2159
+ }
2160
+ function resolveSiluzanCredentials(opts) {
2161
+ if (isSiluzanAgentEnv()) {
2162
+ return resolveAgentEnvCredentials();
2163
+ }
2164
+ const apiKey = trimOrUndefined(process.env.SILUZAN_API_KEY) ?? trimOrUndefined(opts.configApiKey);
2165
+ const authToken = trimOrUndefined(opts.tokenArg) ?? trimOrUndefined(process.env.SILUZAN_AUTH_TOKEN) ?? trimOrUndefined(opts.configAuthToken) ?? "";
2166
+ return { authToken, apiKey };
2167
+ }
2168
+ function resolveRequestAuth(config) {
2169
+ if (isSiluzanAgentEnv()) {
2170
+ return resolveAgentEnvCredentials();
2171
+ }
2172
+ return { authToken: config.authToken, apiKey: config.apiKey };
2173
+ }
2174
+ function buildSiluzanAuthHeaders(config) {
2175
+ const auth = resolveRequestAuth(config);
2176
+ return auth.apiKey ? { "x-api-key": auth.apiKey } : { Authorization: `Bearer ${auth.authToken}` };
2177
+ }
2122
2178
  function isPerfEnabled() {
2123
2179
  const v = process.env.SILUZAN_HTTP_PERF;
2124
2180
  return v === "1" || v === "true";
@@ -2264,15 +2320,17 @@ function redactSensitive(input) {
2264
2320
  );
2265
2321
  return output;
2266
2322
  }
2323
+ function buildAuthHeaders(config) {
2324
+ return buildSiluzanAuthHeaders(config);
2325
+ }
2267
2326
  async function apiFetch(url, config, options = {}, verbose = false) {
2268
2327
  const method = options.method ?? "GET";
2269
- const authHeaders2 = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
2270
2328
  const reqHeaders = {
2271
2329
  "Content-Type": "application/json",
2272
2330
  "Accept-Language": "zh-CN",
2273
2331
  // 声明支持 gzip/deflate/br;服务端不支持则按 identity 返回,rawRequest 会原样收取
2274
2332
  "Accept-Encoding": "gzip, deflate, br",
2275
- ...authHeaders2,
2333
+ ...buildAuthHeaders(config),
2276
2334
  // dataPermission 仅 TSO 使用;CSO 未设置时为空字符串,服务端忽略该头
2277
2335
  Datapermission: config.dataPermission ?? "",
2278
2336
  ...options.headers ?? {}
@@ -2300,12 +2358,11 @@ async function apiFetch(url, config, options = {}, verbose = false) {
2300
2358
  }
2301
2359
  async function apiFetchWithHeaders(url, config, options = {}, verbose = false) {
2302
2360
  const method = options.method ?? "GET";
2303
- const authHeaders2 = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
2304
2361
  const reqHeaders = {
2305
2362
  "Content-Type": "application/json",
2306
2363
  "Accept-Language": "zh-CN",
2307
2364
  "Accept-Encoding": "gzip, deflate, br",
2308
- ...authHeaders2,
2365
+ ...buildAuthHeaders(config),
2309
2366
  Datapermission: config.dataPermission ?? "",
2310
2367
  ...options.headers ?? {}
2311
2368
  };
@@ -2820,33 +2877,6 @@ function installProcessHandlers() {
2820
2877
  process.exit(1);
2821
2878
  });
2822
2879
  }
2823
- function hasSiluzanAgentCredentials() {
2824
- const apiKey = process.env.SILUZAN_API_KEY?.trim();
2825
- const authToken = process.env.SILUZAN_AUTH_TOKEN?.trim();
2826
- return Boolean(apiKey || authToken);
2827
- }
2828
- function isSiluzanAgentEnv() {
2829
- const raw = process.env.IS_SILUZAN_AGENT_ENV?.trim().toLowerCase();
2830
- return raw === "true" || raw === "1";
2831
- }
2832
- function skipAuthSetupInAgentEnv(commandLabel) {
2833
- if (!isSiluzanAgentEnv()) return false;
2834
- if (hasSiluzanAgentCredentials()) {
2835
- console.log(
2836
- `
2837
- \u2139\uFE0F \u68C0\u6D4B\u5230 Siluzan Agent \u73AF\u5883\uFF08IS_SILUZAN_AGENT_ENV=true\uFF09\uFF0C\u51ED\u636E\u5DF2\u7531\u6C99\u7BB1\u6CE8\u5165\uFF0C\u65E0\u9700\u6267\u884C ${commandLabel}\u3002
2838
- `
2839
- );
2840
- } else {
2841
- console.log(
2842
- `
2843
- \u2139\uFE0F \u68C0\u6D4B\u5230 Siluzan Agent \u73AF\u5883\uFF08IS_SILUZAN_AGENT_ENV=true\uFF09\u3002
2844
- ${commandLabel} \u5DF2\u8DF3\u8FC7\uFF1B\u8BF7\u7531 Agent \u6C99\u7BB1\u6CE8\u5165 SILUZAN_API_KEY \u6216 SILUZAN_AUTH_TOKEN\u3002
2845
- `
2846
- );
2847
- }
2848
- return true;
2849
- }
2850
2880
  function printAuthMissingHelp(binName) {
2851
2881
  if (isSiluzanAgentEnv()) {
2852
2882
  console.error(
@@ -2923,7 +2953,7 @@ function parseMeResponse(text) {
2923
2953
  async function fetchSiluzanCurrentUser(apiBase, config) {
2924
2954
  const mainOrigin = deriveMainApiOrigin(apiBase);
2925
2955
  const meUrl = `${mainOrigin.replace(/\/$/, "")}/query/account/me`;
2926
- const authHeaders2 = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
2956
+ const authHeaders2 = buildSiluzanAuthHeaders(config);
2927
2957
  try {
2928
2958
  const res = await rawRequest(meUrl, {
2929
2959
  method: "GET",
@@ -3704,7 +3734,7 @@ async function fetchDataPermission(mainApiUrl, auth) {
3704
3734
  const headers = {
3705
3735
  "Content-Type": "application/json",
3706
3736
  "Accept-Language": "zh-CN",
3707
- ...auth.apiKey ? { "x-api-key": auth.apiKey } : { Authorization: `Bearer ${auth.authToken}` }
3737
+ ...buildSiluzanAuthHeaders(auth)
3708
3738
  };
3709
3739
  try {
3710
3740
  const res = await fetch(url, { headers });
@@ -3985,7 +4015,7 @@ async function apiDeleteRaw(url, config, options = {}) {
3985
4015
  "Content-Type": "application/json; charset=utf-8",
3986
4016
  Accept: "application/json, text/plain, */*",
3987
4017
  "Accept-Language": "zh-CN",
3988
- ...config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` },
4018
+ ...buildSiluzanAuthHeaders(config),
3989
4019
  Datapermission: config.dataPermission ?? "",
3990
4020
  "Content-Length": String(Buffer.byteLength(bodyStr, "utf8"))
3991
4021
  };
@@ -4024,8 +4054,11 @@ var init_fetch_wrapper = __esm({
4024
4054
  // src/utils/auth.ts
4025
4055
  function loadConfig(tokenArg) {
4026
4056
  const shared = readSharedConfig();
4027
- const apiKey = process.env.SILUZAN_API_KEY ?? (shared.apiKey ? shared.apiKey : void 0);
4028
- const authToken = tokenArg ?? process.env.SILUZAN_AUTH_TOKEN ?? shared.authToken ?? "";
4057
+ const { authToken, apiKey } = resolveSiluzanCredentials({
4058
+ tokenArg,
4059
+ configApiKey: shared.apiKey,
4060
+ configAuthToken: shared.authToken
4061
+ });
4029
4062
  if (!apiKey && !authToken) {
4030
4063
  printAuthMissingHelp("siluzan-tso");
4031
4064
  }
@@ -5121,6 +5154,14 @@ var init_strip_legacy_google_fields = __esm({
5121
5154
  }
5122
5155
  });
5123
5156
 
5157
+ // src/utils/google-analysis-snapshot.ts
5158
+ var init_google_analysis_snapshot = __esm({
5159
+ "src/utils/google-analysis-snapshot.ts"() {
5160
+ "use strict";
5161
+ init_google_analysis();
5162
+ }
5163
+ });
5164
+
5124
5165
  // src/commands/google-analysis/sections.ts
5125
5166
  var SECTIONS;
5126
5167
  var init_sections = __esm({
@@ -101226,14 +101267,6 @@ var init_fetch = __esm({
101226
101267
  }
101227
101268
  });
101228
101269
 
101229
- // src/utils/google-analysis-snapshot.ts
101230
- var init_google_analysis_snapshot = __esm({
101231
- "src/utils/google-analysis-snapshot.ts"() {
101232
- "use strict";
101233
- init_google_analysis();
101234
- }
101235
- });
101236
-
101237
101270
  // src/utils/http-retry.ts
101238
101271
  function classifyError(err) {
101239
101272
  const status = parseHttpStatusFromError(err);
@@ -101379,8 +101412,8 @@ var init_http_retry = __esm({
101379
101412
  });
101380
101413
 
101381
101414
  // src/utils/batch-manifest.ts
101382
- import * as fs14 from "fs/promises";
101383
- import * as path19 from "path";
101415
+ import * as fs19 from "fs/promises";
101416
+ import * as path24 from "path";
101384
101417
  import { randomUUID as randomUUID2 } from "crypto";
101385
101418
  function generateRunId(now = /* @__PURE__ */ new Date()) {
101386
101419
  const pad = (n) => String(n).padStart(2, "0");
@@ -101394,26 +101427,26 @@ function isValidRunId(id) {
101394
101427
  }
101395
101428
  function resolveBatchPaths(baseDir, runId) {
101396
101429
  if (!isValidRunId(runId)) throw new Error(`\u975E\u6CD5 runId\uFF1A${runId}`);
101397
- const runDir = path19.resolve(baseDir, runId);
101398
- const stateDir = path19.join(runDir, "state");
101430
+ const runDir = path24.resolve(baseDir, runId);
101431
+ const stateDir = path24.join(runDir, "state");
101399
101432
  return {
101400
101433
  runDir,
101401
- runManifestFile: path19.join(runDir, "run-manifest.json"),
101402
- progressFile: path19.join(runDir, "progress.json"),
101403
- accountsFile: path19.join(runDir, "accounts.json"),
101404
- resultsDir: path19.join(runDir, "results"),
101405
- errorsDir: path19.join(runDir, "errors"),
101434
+ runManifestFile: path24.join(runDir, "run-manifest.json"),
101435
+ progressFile: path24.join(runDir, "progress.json"),
101436
+ accountsFile: path24.join(runDir, "accounts.json"),
101437
+ resultsDir: path24.join(runDir, "results"),
101438
+ errorsDir: path24.join(runDir, "errors"),
101406
101439
  stateDir,
101407
- tasksLogFile: path19.join(stateDir, "tasks.jsonl"),
101408
- lockFile: path19.join(stateDir, "tasks.lock")
101440
+ tasksLogFile: path24.join(stateDir, "tasks.jsonl"),
101441
+ lockFile: path24.join(stateDir, "tasks.lock")
101409
101442
  };
101410
101443
  }
101411
101444
  async function ensureAccountDirs(paths, accountId) {
101412
101445
  const safe = sanitizeAccountSegment(accountId);
101413
- const results = path19.join(paths.resultsDir, safe);
101414
- const errors = path19.join(paths.errorsDir, safe);
101415
- await fs14.mkdir(results, { recursive: true });
101416
- await fs14.mkdir(errors, { recursive: true });
101446
+ const results = path24.join(paths.resultsDir, safe);
101447
+ const errors = path24.join(paths.errorsDir, safe);
101448
+ await fs19.mkdir(results, { recursive: true });
101449
+ await fs19.mkdir(errors, { recursive: true });
101417
101450
  return { results, errors };
101418
101451
  }
101419
101452
  function sanitizeAccountSegment(accountId) {
@@ -101428,20 +101461,20 @@ function sanitizeAccountSegment(accountId) {
101428
101461
  return slug;
101429
101462
  }
101430
101463
  async function atomicWriteFile(target, content) {
101431
- const dir = path19.dirname(target);
101432
- await fs14.mkdir(dir, { recursive: true });
101433
- const tmp = path19.join(dir, `.tmp-${randomUUID2()}-${path19.basename(target)}`);
101464
+ const dir = path24.dirname(target);
101465
+ await fs19.mkdir(dir, { recursive: true });
101466
+ const tmp = path24.join(dir, `.tmp-${randomUUID2()}-${path24.basename(target)}`);
101434
101467
  try {
101435
- await fs14.writeFile(tmp, content, "utf8");
101436
- await fs14.rename(tmp, target);
101468
+ await fs19.writeFile(tmp, content, "utf8");
101469
+ await fs19.rename(tmp, target);
101437
101470
  } catch (e) {
101438
- await fs14.rm(tmp, { force: true }).catch(() => void 0);
101471
+ await fs19.rm(tmp, { force: true }).catch(() => void 0);
101439
101472
  throw e;
101440
101473
  }
101441
101474
  }
101442
101475
  async function readJsonIfExists(file) {
101443
101476
  try {
101444
- const raw = await fs14.readFile(file, "utf8");
101477
+ const raw = await fs19.readFile(file, "utf8");
101445
101478
  return JSON.parse(raw);
101446
101479
  } catch (e) {
101447
101480
  if (e.code === "ENOENT") return null;
@@ -101471,12 +101504,12 @@ function readProgress(paths) {
101471
101504
  return readJsonIfExists(paths.progressFile);
101472
101505
  }
101473
101506
  async function appendTaskLog(paths, entry) {
101474
- await fs14.mkdir(paths.stateDir, { recursive: true });
101475
- await fs14.appendFile(paths.tasksLogFile, JSON.stringify(entry) + "\n", "utf8");
101507
+ await fs19.mkdir(paths.stateDir, { recursive: true });
101508
+ await fs19.appendFile(paths.tasksLogFile, JSON.stringify(entry) + "\n", "utf8");
101476
101509
  }
101477
101510
  async function readTaskLog(paths) {
101478
101511
  try {
101479
- const raw = await fs14.readFile(paths.tasksLogFile, "utf8");
101512
+ const raw = await fs19.readFile(paths.tasksLogFile, "utf8");
101480
101513
  return raw.split("\n").filter((s) => s.trim() !== "").map((line) => JSON.parse(line));
101481
101514
  } catch (e) {
101482
101515
  if (e.code === "ENOENT") return [];
@@ -101531,9 +101564,9 @@ function emptyProgress(runId, totalTasks, accountConcurrency, sectionConcurrency
101531
101564
  };
101532
101565
  }
101533
101566
  async function tryAcquireLock(paths) {
101534
- await fs14.mkdir(paths.stateDir, { recursive: true });
101567
+ await fs19.mkdir(paths.stateDir, { recursive: true });
101535
101568
  try {
101536
- const handle = await fs14.open(paths.lockFile, "wx");
101569
+ const handle = await fs19.open(paths.lockFile, "wx");
101537
101570
  await handle.writeFile(`${process.pid}@${(/* @__PURE__ */ new Date()).toISOString()}`);
101538
101571
  await handle.close();
101539
101572
  return true;
@@ -101543,7 +101576,7 @@ async function tryAcquireLock(paths) {
101543
101576
  }
101544
101577
  }
101545
101578
  async function releaseLock(paths) {
101546
- await fs14.rm(paths.lockFile, { force: true }).catch(() => void 0);
101579
+ await fs19.rm(paths.lockFile, { force: true }).catch(() => void 0);
101547
101580
  }
101548
101581
  var BATCH_SCHEMA_VERSION;
101549
101582
  var init_batch_manifest = __esm({
@@ -101555,8 +101588,8 @@ var init_batch_manifest = __esm({
101555
101588
 
101556
101589
  // src/utils/batch-runner.ts
101557
101590
  import { performance as performance2 } from "perf_hooks";
101558
- import * as path20 from "path";
101559
- import * as fs15 from "fs/promises";
101591
+ import * as path25 from "path";
101592
+ import * as fs20 from "fs/promises";
101560
101593
  async function runPool(items, concurrencyRef, worker, signal) {
101561
101594
  let cursor = 0;
101562
101595
  const launch = () => {
@@ -101695,7 +101728,7 @@ async function runBatch(opts) {
101695
101728
  }
101696
101729
  const inner = {
101697
101730
  ...emptyProgress(
101698
- opts.paths.runDir.split(path20.sep).pop() ?? "run",
101731
+ opts.paths.runDir.split(path25.sep).pop() ?? "run",
101699
101732
  allTasks.length,
101700
101733
  opts.accountConcurrency,
101701
101734
  opts.sectionConcurrency
@@ -101825,7 +101858,7 @@ async function runBatch(opts) {
101825
101858
  }
101826
101859
  );
101827
101860
  await writer({
101828
- snapshotDir: path20.join(opts.paths.resultsDir, sanitizeAccountSegment(task.accountId)),
101861
+ snapshotDir: path25.join(opts.paths.resultsDir, sanitizeAccountSegment(task.accountId)),
101829
101862
  section: task.section,
101830
101863
  accountId: task.accountId,
101831
101864
  dateRange: result.dateRange,
@@ -101898,7 +101931,7 @@ async function runBatch(opts) {
101898
101931
  };
101899
101932
  }
101900
101933
  async function writeTaskError(paths, errorsDir, task, err, cls) {
101901
- const file = path20.join(errorsDir, `${sanitizeSection(task.section)}.error.json`);
101934
+ const file = path25.join(errorsDir, `${sanitizeSection(task.section)}.error.json`);
101902
101935
  const payload = {
101903
101936
  schemaVersion: 1,
101904
101937
  taskId: task.id,
@@ -101944,10 +101977,10 @@ async function persistRunMetadata(input) {
101944
101977
  accountsTotal: input.accounts.length,
101945
101978
  invocation: input.invocation
101946
101979
  };
101947
- await fs15.mkdir(paths.runDir, { recursive: true });
101948
- await fs15.mkdir(paths.stateDir, { recursive: true });
101949
- await fs15.mkdir(paths.resultsDir, { recursive: true });
101950
- await fs15.mkdir(paths.errorsDir, { recursive: true });
101980
+ await fs20.mkdir(paths.runDir, { recursive: true });
101981
+ await fs20.mkdir(paths.stateDir, { recursive: true });
101982
+ await fs20.mkdir(paths.resultsDir, { recursive: true });
101983
+ await fs20.mkdir(paths.errorsDir, { recursive: true });
101951
101984
  await writeRunManifest(paths, manifest);
101952
101985
  await writeAccounts(paths, input.accounts);
101953
101986
  return paths;
@@ -102010,7 +102043,7 @@ __export(google_analysis_batch_exports, {
102010
102043
  parseAccountIds: () => parseAccountIds,
102011
102044
  registerGoogleAnalysisBatchCommands: () => registerGoogleAnalysisBatchCommands
102012
102045
  });
102013
- import * as path21 from "path";
102046
+ import * as path26 from "path";
102014
102047
  import { performance as performance3 } from "perf_hooks";
102015
102048
  function parseSections(input) {
102016
102049
  if (!input || !input.trim()) return DEFAULT_SECTIONS;
@@ -102557,7 +102590,7 @@ var init_google_analysis_batch = __esm({
102557
102590
  });
102558
102591
 
102559
102592
  // src/commands/google-analysis/register-cli.ts
102560
- import * as path22 from "path";
102593
+ import * as path27 from "path";
102561
102594
  import { performance as performance4 } from "perf_hooks";
102562
102595
  function resolveSectionList(sections, exclude) {
102563
102596
  const allNames = SECTIONS.map((s) => s.name);
@@ -102647,7 +102680,7 @@ async function runAllSections(opts) {
102647
102680
  const results = await runWithConcurrency(tasks, concurrency);
102648
102681
  const succeeded = results.filter((r) => r.ok).length;
102649
102682
  const failed = results.length - succeeded;
102650
- const absoluteSnapshotDir = path22.resolve(opts.jsonOut);
102683
+ const absoluteSnapshotDir = path27.resolve(opts.jsonOut);
102651
102684
  const summary = {
102652
102685
  kind: "siluzan-tso-google-analysis-snapshot-batch",
102653
102686
  absoluteSnapshotDir,
@@ -102880,9 +102913,9 @@ var init_google_analysis3 = __esm({
102880
102913
  // src/index.ts
102881
102914
  init_dist();
102882
102915
  init_dist();
102883
- import * as fs18 from "fs";
102884
- import * as path26 from "path";
102885
- import { fileURLToPath as fileURLToPath6 } from "url";
102916
+ import * as fs23 from "fs";
102917
+ import * as path31 from "path";
102918
+ import { fileURLToPath as fileURLToPath7 } from "url";
102886
102919
  import { Command as Command2 } from "commander";
102887
102920
 
102888
102921
  // src/commands/login/urls.ts
@@ -103368,8 +103401,8 @@ function deriveWebUrl(apiBaseUrl) {
103368
103401
  }
103369
103402
  var TSO_UMI_ROUTE_PREFIX = "/v3umijs/tso";
103370
103403
  function buildTsoPageWebUrl(webUrl, tsoSubRoute, extraQuery) {
103371
- const path27 = tsoSubRoute.replace(/^\/+/, "");
103372
- const umiPath = `${TSO_UMI_ROUTE_PREFIX}/${path27}`;
103404
+ const path32 = tsoSubRoute.replace(/^\/+/, "");
103405
+ const umiPath = `${TSO_UMI_ROUTE_PREFIX}/${path32}`;
103373
103406
  const q = new URLSearchParams({ tso: umiPath });
103374
103407
  if (extraQuery) {
103375
103408
  for (const [key, value] of Object.entries(extraQuery)) {
@@ -103377,14 +103410,17 @@ function buildTsoPageWebUrl(webUrl, tsoSubRoute, extraQuery) {
103377
103410
  if (v) q.set(key, v);
103378
103411
  }
103379
103412
  }
103380
- return `${webUrl.replace(/\/+$/, "")}/v3/foreign_trade/tso/${path27}?${q.toString()}`;
103413
+ return `${webUrl.replace(/\/+$/, "")}/v3/foreign_trade/tso/${path32}?${q.toString()}`;
103381
103414
  }
103382
103415
  function cmdConfigShow() {
103383
103416
  const shared = readSharedConfig();
103417
+ const { authToken: effectiveAuthToken, apiKey: effectiveApiKeyRaw } = resolveSiluzanCredentials({
103418
+ configApiKey: shared.apiKey,
103419
+ configAuthToken: shared.authToken
103420
+ });
103421
+ const effectiveApiKey = effectiveApiKeyRaw ?? "";
103384
103422
  const envApiKey = process.env.SILUZAN_API_KEY;
103385
103423
  const envAuthToken = process.env.SILUZAN_AUTH_TOKEN;
103386
- const effectiveApiKey = envApiKey ?? shared.apiKey ?? "";
103387
- const effectiveAuthToken = envAuthToken ?? shared.authToken ?? "";
103388
103424
  const apiBaseUrl = process.env.SILUZAN_TSO_API_BASE ?? DEFAULT_API_BASE;
103389
103425
  const googleApiUrl = process.env.SILUZAN_GOOGLE_API ?? deriveGoogleApiUrl(apiBaseUrl);
103390
103426
  const webUrl = deriveWebUrl(apiBaseUrl);
@@ -103408,12 +103444,12 @@ function cmdConfigShow() {
103408
103444
  console.log(` googleApiUrl : ${googleApiUrl}`);
103409
103445
  console.log(` webUrl : ${webUrl}`);
103410
103446
  if (effectiveApiKey) {
103411
- const src = envApiKey ? "env:SILUZAN_API_KEY" : "config.json";
103447
+ const src = isSiluzanAgentEnv() ? "env:SILUZAN_API_KEY (agent)" : envApiKey ? "env:SILUZAN_API_KEY" : "config.json";
103412
103448
  const active = " \u2190 \u5F53\u524D\u751F\u6548\uFF08X-Api-Key \u9274\u6743\uFF09";
103413
103449
  console.log(` apiKey : ${maskSecret(effectiveApiKey)} [${src}]${active}`);
103414
103450
  }
103415
103451
  if (effectiveAuthToken) {
103416
- const src = envAuthToken ? "env:SILUZAN_AUTH_TOKEN" : "config.json";
103452
+ const src = isSiluzanAgentEnv() ? "env:SILUZAN_AUTH_TOKEN (agent)" : envAuthToken ? "env:SILUZAN_AUTH_TOKEN" : "config.json";
103417
103453
  const note = effectiveApiKey ? " \uFF08\u5DF2\u88AB apiKey \u8986\u76D6\uFF09" : " \u2190 \u5F53\u524D\u751F\u6548\uFF08Bearer \u9274\u6743\uFF09";
103418
103454
  console.log(` authToken : ${maskSecret(effectiveAuthToken)} [${src}]${note}`);
103419
103455
  }
@@ -110614,7 +110650,7 @@ function cloneKeywordBlockShell(block) {
110614
110650
  delete shell["matchTypeV2"];
110615
110651
  return shell;
110616
110652
  }
110617
- function splitKeywordsForBatchJobBlockIfMixed(block, path27, warnings) {
110653
+ function splitKeywordsForBatchJobBlockIfMixed(block, path32, warnings) {
110618
110654
  const declaredUi = matchTypeV2ToUi(readBlockMatchTypeRaw(block));
110619
110655
  const entries = collectKeywordEntriesFromBlock(block);
110620
110656
  if (entries.length === 0) return [block];
@@ -110633,7 +110669,7 @@ function splitKeywordsForBatchJobBlockIfMixed(block, path27, warnings) {
110633
110669
  (ui) => matchTypeUiToV2(ui)
110634
110670
  );
110635
110671
  warnings.push(
110636
- `${path27} \u542B\u591A\u79CD\u5339\u914D\u7C7B\u578B\uFF0C\u5DF2\u81EA\u52A8\u62C6\u5206\u4E3A ${groups.size} \u4E2A KeywordsForBatchJob \u5757\uFF08${labels.join("\u3001")}\uFF09`
110672
+ `${path32} \u542B\u591A\u79CD\u5339\u914D\u7C7B\u578B\uFF0C\u5DF2\u81EA\u52A8\u62C6\u5206\u4E3A ${groups.size} \u4E2A KeywordsForBatchJob \u5757\uFF08${labels.join("\u3001")}\uFF09`
110637
110673
  );
110638
110674
  const shell = cloneKeywordBlockShell(block);
110639
110675
  const splitBlocks = [];
@@ -110653,16 +110689,16 @@ function splitKeywordsForBatchJobBlockIfMixed(block, path27, warnings) {
110653
110689
  }
110654
110690
  return splitBlocks;
110655
110691
  }
110656
- function validateKeywordCore(core, path27, errors, lengthViolations) {
110692
+ function validateKeywordCore(core, path32, errors, lengthViolations) {
110657
110693
  const trimmed = core.trim();
110658
110694
  if (!trimmed) {
110659
- errors.push(`${path27} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
110695
+ errors.push(`${path32} \u5173\u952E\u8BCD\u8BCD\u5E72\u4E0D\u80FD\u4E3A\u7A7A`);
110660
110696
  return false;
110661
110697
  }
110662
110698
  if (trimmed.length > GOOGLE_KEYWORD_MAX_CORE_LENGTH) {
110663
110699
  if (lengthViolations) {
110664
110700
  pushLengthViolation(lengthViolations, {
110665
- path: path27,
110701
+ path: path32,
110666
110702
  field: "KeywordText",
110667
110703
  kind: "keyword_core",
110668
110704
  limit: GOOGLE_KEYWORD_MAX_CORE_LENGTH,
@@ -110672,13 +110708,13 @@ function validateKeywordCore(core, path27, errors, lengthViolations) {
110672
110708
  });
110673
110709
  }
110674
110710
  errors.push(
110675
- `${path27} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
110711
+ `${path32} \u5173\u952E\u8BCD\u8BCD\u5E72\u8D85\u8FC7 ${GOOGLE_KEYWORD_MAX_CORE_LENGTH} \u5B57\u7B26\uFF08\u5F53\u524D ${trimmed.length}\uFF09\uFF1A"${trimmed}"`
110676
110712
  );
110677
110713
  return false;
110678
110714
  }
110679
110715
  if (!VALID_CORE_REGEX.test(trimmed)) {
110680
110716
  errors.push(
110681
- `${path27} \u542B Google \u4E0D\u5141\u8BB8\u7684\u5B57\u7B26\uFF08\u8BCD\u5E72\u4EC5\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u7A7A\u683C/\u8FDE\u5B57\u7B26/\u53E5\u70B9\uFF09\uFF1A"${trimmed.slice(0, 40)}${trimmed.length > 40 ? "\u2026" : ""}"`
110717
+ `${path32} \u542B Google \u4E0D\u5141\u8BB8\u7684\u5B57\u7B26\uFF08\u8BCD\u5E72\u4EC5\u5141\u8BB8\u5B57\u6BCD/\u6570\u5B57/\u7A7A\u683C/\u8FDE\u5B57\u7B26/\u53E5\u70B9\uFF09\uFF1A"${trimmed.slice(0, 40)}${trimmed.length > 40 ? "\u2026" : ""}"`
110682
110718
  );
110683
110719
  return false;
110684
110720
  }
@@ -110735,24 +110771,24 @@ function canonicalizeKeywordBatchBlock(block, resolvedMatchV2, normalizedTexts,
110735
110771
  }
110736
110772
  }
110737
110773
  }
110738
- function pushKeywordAutoFixWarning(warnings, path27, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
110774
+ function pushKeywordAutoFixWarning(warnings, path32, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
110739
110775
  if (inferredMatchType) {
110740
110776
  warnings.push(
110741
- `${path27}.${fieldLabel} \u672A\u6307\u5B9A MatchTypeV2\uFF0C\u5DF2\u6309\u8BCD\u9762\u63A8\u65AD\u4E3A ${matchTypeUiToV2(matchType)}`
110777
+ `${path32}.${fieldLabel} \u672A\u6307\u5B9A MatchTypeV2\uFF0C\u5DF2\u6309\u8BCD\u9762\u63A8\u65AD\u4E3A ${matchTypeUiToV2(matchType)}`
110742
110778
  );
110743
110779
  return;
110744
110780
  }
110745
110781
  if (declaredUi && beforeUi !== declaredUi) {
110746
110782
  warnings.push(
110747
- `${path27}.${fieldLabel} MatchTypeV2=${matchTypeUiToV2(declaredUi)} \u4E0E\u8BCD\u9762\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\uFF1A${formatted}`
110783
+ `${path32}.${fieldLabel} MatchTypeV2=${matchTypeUiToV2(declaredUi)} \u4E0E\u8BCD\u9762\u4E0D\u4E00\u81F4\uFF0C\u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\uFF1A${formatted}`
110748
110784
  );
110749
110785
  return;
110750
110786
  }
110751
110787
  if (formatted !== trimmedRaw) {
110752
- warnings.push(`${path27}.${fieldLabel} \u5DF2\u81EA\u52A8\u4FEE\u590D\u8BCD\u9762\uFF1A${trimmedRaw} \u2192 ${formatted}`);
110788
+ warnings.push(`${path32}.${fieldLabel} \u5DF2\u81EA\u52A8\u4FEE\u590D\u8BCD\u9762\uFF1A${trimmedRaw} \u2192 ${formatted}`);
110753
110789
  }
110754
110790
  }
110755
- function normalizeKeywordTextList(texts, matchTypeRaw, path27, fieldLabel, errors, warnings, lengthViolations) {
110791
+ function normalizeKeywordTextList(texts, matchTypeRaw, path32, fieldLabel, errors, warnings, lengthViolations) {
110756
110792
  const declaredUi = matchTypeV2ToUi(matchTypeRaw);
110757
110793
  const normalized = [];
110758
110794
  const seen = /* @__PURE__ */ new Set();
@@ -110760,7 +110796,7 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path27, fieldLabel, error
110760
110796
  for (let k = 0; k < texts.length; k++) {
110761
110797
  const raw = texts[k];
110762
110798
  if (typeof raw !== "string" || !raw.trim()) {
110763
- errors.push(`${path27}.${fieldLabel}[${k}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110799
+ errors.push(`${path32}.${fieldLabel}[${k}] \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110764
110800
  continue;
110765
110801
  }
110766
110802
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
@@ -110771,7 +110807,7 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path27, fieldLabel, error
110771
110807
  );
110772
110808
  pushKeywordAutoFixWarning(
110773
110809
  warnings,
110774
- path27,
110810
+ path32,
110775
110811
  fieldLabel,
110776
110812
  trimmedRaw,
110777
110813
  formatted,
@@ -110781,11 +110817,11 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path27, fieldLabel, error
110781
110817
  inferredMatchType
110782
110818
  );
110783
110819
  const core = unwrapKeywordDisplayTextForEdit(formatted);
110784
- if (!validateKeywordCore(core, `${path27}.${fieldLabel}[${k}]`, errors, lengthViolations))
110820
+ if (!validateKeywordCore(core, `${path32}.${fieldLabel}[${k}]`, errors, lengthViolations))
110785
110821
  continue;
110786
110822
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
110787
110823
  if (seen.has(dedupeKey)) {
110788
- warnings.push(`${path27}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110824
+ warnings.push(`${path32}.${fieldLabel}[${k}] \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110789
110825
  continue;
110790
110826
  }
110791
110827
  seen.add(dedupeKey);
@@ -110794,12 +110830,12 @@ function normalizeKeywordTextList(texts, matchTypeRaw, path27, fieldLabel, error
110794
110830
  }
110795
110831
  return { normalized, resolvedUi };
110796
110832
  }
110797
- function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, lengthViolations) {
110833
+ function normalizeKeywordsForBatchJobBlock(block, path32, errors, warnings, lengthViolations) {
110798
110834
  const matchTypeRaw = readBlockMatchTypeRaw(block);
110799
110835
  const declaredUi = matchTypeV2ToUi(matchTypeRaw);
110800
110836
  if (matchTypeRaw !== void 0 && declaredUi === null) {
110801
110837
  errors.push(
110802
- `${path27}.MatchTypeV2 \u65E0\u6548\uFF08${String(matchTypeRaw)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1ABROAD | PHRASE | EXACT`
110838
+ `${path32}.MatchTypeV2 \u65E0\u6548\uFF08${String(matchTypeRaw)}\uFF09\uFF0C\u5408\u6CD5\u503C\uFF1ABROAD | PHRASE | EXACT`
110803
110839
  );
110804
110840
  return;
110805
110841
  }
@@ -110814,7 +110850,7 @@ function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, leng
110814
110850
  const result = normalizeKeywordTextList(
110815
110851
  texts,
110816
110852
  matchTypeRaw,
110817
- path27,
110853
+ path32,
110818
110854
  "KeywordText",
110819
110855
  errors,
110820
110856
  warnings,
@@ -110823,7 +110859,7 @@ function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, leng
110823
110859
  normalizedTexts = result.normalized;
110824
110860
  if (result.resolvedUi) resolvedUi = result.resolvedUi;
110825
110861
  if (normalizedTexts.length === 0 && texts.length > 0) {
110826
- errors.push(`${path27}.KeywordText \u7ECF\u6821\u9A8C\u540E\u65E0\u6709\u6548\u5173\u952E\u8BCD\uFF0C\u8BF7\u4FEE\u6B63\u8BCD\u9762\u6216 MatchTypeV2`);
110862
+ errors.push(`${path32}.KeywordText \u7ECF\u6821\u9A8C\u540E\u65E0\u6709\u6548\u5173\u952E\u8BCD\uFF0C\u8BF7\u4FEE\u6B63\u8BCD\u9762\u6216 MatchTypeV2`);
110827
110863
  }
110828
110864
  }
110829
110865
  if (hasItems && items) {
@@ -110831,12 +110867,12 @@ function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, leng
110831
110867
  for (let k = 0; k < items.length; k++) {
110832
110868
  const item = items[k];
110833
110869
  if (!item || typeof item !== "object") {
110834
- errors.push(`${path27}.Items[${k}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
110870
+ errors.push(`${path32}.Items[${k}] \u5FC5\u987B\u662F\u5BF9\u8C61`);
110835
110871
  continue;
110836
110872
  }
110837
110873
  const raw = readItemText(item);
110838
110874
  if (raw === null || !raw.trim()) {
110839
- errors.push(`${path27}.Items[${k}].Text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110875
+ errors.push(`${path32}.Items[${k}].Text \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32`);
110840
110876
  continue;
110841
110877
  }
110842
110878
  const trimmedRaw = collapseDuplicateSpacesInKeywordText(raw.trim());
@@ -110847,7 +110883,7 @@ function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, leng
110847
110883
  );
110848
110884
  pushKeywordAutoFixWarning(
110849
110885
  warnings,
110850
- path27,
110886
+ path32,
110851
110887
  `Items[${k}].Text`,
110852
110888
  trimmedRaw,
110853
110889
  formatted,
@@ -110857,11 +110893,11 @@ function normalizeKeywordsForBatchJobBlock(block, path27, errors, warnings, leng
110857
110893
  inferredMatchType
110858
110894
  );
110859
110895
  const core = unwrapKeywordDisplayTextForEdit(formatted);
110860
- if (!validateKeywordCore(core, `${path27}.Items[${k}].Text`, errors, lengthViolations))
110896
+ if (!validateKeywordCore(core, `${path32}.Items[${k}].Text`, errors, lengthViolations))
110861
110897
  continue;
110862
110898
  const dedupeKey = `${matchTypeUiToV2(matchType)}:${core.toLowerCase()}`;
110863
110899
  if (seen.has(dedupeKey)) {
110864
- warnings.push(`${path27}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110900
+ warnings.push(`${path32}.Items[${k}].Text \u4E0E\u540C\u7EC4\u91CD\u590D\uFF0C\u5DF2\u8DF3\u8FC7\uFF1A${formatted}`);
110865
110901
  continue;
110866
110902
  }
110867
110903
  seen.add(dedupeKey);
@@ -111006,16 +111042,16 @@ function warnIfNegativeKeywordsInPositiveKeywords(campaign, warnings) {
111006
111042
  const raw = texts[ki];
111007
111043
  if (typeof raw !== "string" || !raw.trim()) continue;
111008
111044
  const core = normalizeKeywordCoreForNegativeCheck(raw);
111009
- const path27 = `campaign.AdGroupsForBatchJob[${gi}].KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
111045
+ const path32 = `campaign.AdGroupsForBatchJob[${gi}].KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
111010
111046
  if (plannedNegCores.has(core)) {
111011
111047
  warnings.push(
111012
- `${path27} \u8BCD\u9762\u300C${raw}\u300D\u5DF2\u51FA\u73B0\u5728 campaign.NegativeKeywordsForBatchJob\uFF0C\u4E0D\u5E94\u91CD\u590D\u5199\u5165\u6B63\u5411 KeywordsForBatchJob\uFF1B\u5426\u8BCD\u53EA\u653E NegativeKeywordsForBatchJob`
111048
+ `${path32} \u8BCD\u9762\u300C${raw}\u300D\u5DF2\u51FA\u73B0\u5728 campaign.NegativeKeywordsForBatchJob\uFF0C\u4E0D\u5E94\u91CD\u590D\u5199\u5165\u6B63\u5411 KeywordsForBatchJob\uFF1B\u5426\u8BCD\u53EA\u653E NegativeKeywordsForBatchJob`
111013
111049
  );
111014
111050
  continue;
111015
111051
  }
111016
111052
  if (looksLikeStandaloneNegativeKeyword(core)) {
111017
111053
  warnings.push(
111018
- `${path27} \u8BCD\u9762\u300C${raw}\u300D\u7591\u4F3C\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u5E38\u89C1\u5426\u8BCD\u8BCD\u6839\uFF09\uFF0C\u5E94\u5199\u5165 campaign.NegativeKeywordsForBatchJob\uFF08MatchTypeV2: BROAD\uFF09\uFF0C\u52FF\u653E\u5165 KeywordsForBatchJob`
111054
+ `${path32} \u8BCD\u9762\u300C${raw}\u300D\u7591\u4F3C\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u5E38\u89C1\u5426\u8BCD\u8BCD\u6839\uFF09\uFF0C\u5E94\u5199\u5165 campaign.NegativeKeywordsForBatchJob\uFF08MatchTypeV2: BROAD\uFF09\uFF0C\u52FF\u653E\u5165 KeywordsForBatchJob`
111019
111055
  );
111020
111056
  }
111021
111057
  }
@@ -113437,9 +113473,9 @@ init_strip_legacy_google_fields();
113437
113473
  init_auth();
113438
113474
  init_cli_json_snapshot();
113439
113475
  function formatBidModifierPercent(multiplier) {
113440
- const pct = Math.round((multiplier - 1) * 1e3) / 10;
113441
- if (pct === 0) return "0%";
113442
- return pct > 0 ? `+${pct}%` : `${pct}%`;
113476
+ const pct2 = Math.round((multiplier - 1) * 1e3) / 10;
113477
+ if (pct2 === 0) return "0%";
113478
+ return pct2 > 0 ? `+${pct2}%` : `${pct2}%`;
113443
113479
  }
113444
113480
  function multiplierToAdGroupBidPercent(multiplier) {
113445
113481
  return Math.round((multiplier - 1) * 100 * 100) / 100;
@@ -114078,12 +114114,12 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
114078
114114
  for (let ki = 0; ki < texts.length; ki++) {
114079
114115
  const t = texts[ki];
114080
114116
  if (typeof t !== "string" || !t.trim()) continue;
114081
- const path27 = `${groupPath}.KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
114117
+ const path32 = `${groupPath}.KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
114082
114118
  const key = keywordKey(t, matchTypeV2);
114083
114119
  if (!liveKwKeys.has(key)) {
114084
114120
  missing.push({
114085
114121
  layer: "keyword",
114086
- path: path27,
114122
+ path: path32,
114087
114123
  adGroupName: groupName,
114088
114124
  plannedContent: `\u8BCD\u9762: ${t} | \u5339\u914D: ${matchTypeV2}`,
114089
114125
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveKwInGroup.length} \u6761\u5173\u952E\u8BCD\uFF0C\u65E0\u952E ${key}`,
@@ -114101,12 +114137,12 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
114101
114137
  for (let ai = 0; ai < ads.length; ai++) {
114102
114138
  const ad = asRecord2(ads[ai]);
114103
114139
  if (!ad) continue;
114104
- const path27 = `${groupPath}.AdsForBatchJob[${ai}]`;
114140
+ const path32 = `${groupPath}.AdsForBatchJob[${ai}]`;
114105
114141
  const primary = pickString(ad["headlinePart1"], ad["AdTitle"]);
114106
114142
  if (!primary) {
114107
114143
  missing.push({
114108
114144
  layer: "ad",
114109
- path: path27,
114145
+ path: path32,
114110
114146
  adGroupName: groupName,
114111
114147
  plannedContent: `AdsForBatchJob[${ai}]\uFF08\u7F3A\u5C11 headlinePart1\uFF0C\u65E0\u6CD5\u6BD4\u5BF9\uFF09`,
114112
114148
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761\u521B\u610F`,
@@ -114119,7 +114155,7 @@ function compareCampaignCreateToLive(cfg, campaignId, live, meta) {
114119
114155
  if (!found) {
114120
114156
  missing.push({
114121
114157
  layer: "ad",
114122
- path: path27,
114158
+ path: path32,
114123
114159
  adGroupName: groupName,
114124
114160
  plannedContent: `RSA \u9996\u6807\u9898: ${primary}${finalUrl ? ` | \u843D\u5730\u9875: ${finalUrl}` : ""}`,
114125
114161
  liveNote: `\u540C\u7EC4\u5DF2\u6709 ${liveAdsInGroup.length} \u6761 RSA\uFF0C\u65E0\u6B64\u9996\u6807\u9898`,
@@ -115366,8 +115402,8 @@ function pmaxChannelTypesUrl(googleApiUrl) {
115366
115402
  }
115367
115403
  function pmaxCampaignUrl(googleApiUrl, accountId, campaignId) {
115368
115404
  const base = googleApiUrl.replace(/\/$/, "");
115369
- const path27 = `${base}/accounts/${accountId}/campaign/pmax`;
115370
- return campaignId ? `${path27}/${campaignId}` : path27;
115405
+ const path32 = `${base}/accounts/${accountId}/campaign/pmax`;
115406
+ return campaignId ? `${path32}/${campaignId}` : path32;
115371
115407
  }
115372
115408
  function pmaxBrandAssetsUrl(googleApiUrl, accountId, campaignId) {
115373
115409
  return `${pmaxCampaignUrl(googleApiUrl, accountId, campaignId)}/brand-assets`;
@@ -115377,8 +115413,8 @@ function pmaxBrandGuidelinesEnableUrl(googleApiUrl, accountId, campaignId) {
115377
115413
  }
115378
115414
  function pmaxAssetGroupUrl(googleApiUrl, accountId, assetGroupId, suffix) {
115379
115415
  const base = googleApiUrl.replace(/\/$/, "");
115380
- const path27 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
115381
- return suffix ? `${path27}/${suffix.replace(/^\//, "")}` : path27;
115416
+ const path32 = `${base}/accounts/${accountId}/campaign/pmax/asset-group/${assetGroupId}`;
115417
+ return suffix ? `${path32}/${suffix.replace(/^\//, "")}` : path32;
115382
115418
  }
115383
115419
  function pmaxCampaignAssetGroupUrl(googleApiUrl, accountId, campaignId) {
115384
115420
  const base = googleApiUrl.replace(/\/$/, "");
@@ -118033,8 +118069,8 @@ async function runAdPmaxImageConvert(opts) {
118033
118069
  }
118034
118070
 
118035
118071
  // src/commands/ad/pmax-brand-mgmt.ts
118036
- function loadBrandConfig(path27, accountId, campaignId) {
118037
- const cfg = loadPmaxJsonFile(path27);
118072
+ function loadBrandConfig(path32, accountId, campaignId) {
118073
+ const cfg = loadPmaxJsonFile(path32);
118038
118074
  return {
118039
118075
  ...cfg,
118040
118076
  account: String(cfg["account"] ?? accountId).trim(),
@@ -124466,6 +124502,1342 @@ function registerMarketAnalysisCommands(program2) {
124466
124502
  });
124467
124503
  }
124468
124504
 
124505
+ // src/commands/google-ads-diagnosis/run-collect.ts
124506
+ init_auth();
124507
+ import fs17 from "fs/promises";
124508
+ import path22 from "path";
124509
+
124510
+ // src/commands/google-ads-diagnosis/shared.ts
124511
+ import fs14 from "fs";
124512
+ import path19 from "path";
124513
+ import { fileURLToPath as fileURLToPath5 } from "url";
124514
+ var GOOGLE_ADS_DIAGNOSIS_TEMPLATE_BASENAME = "GoogleAdsDiagnosisReport.html";
124515
+ var GOOGLE_ADS_DIAGNOSIS_DATA_MARKER = "__REPORT_DATA__";
124516
+ var GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME = "google-ads-diagnosis-collect.json";
124517
+ var GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME = "google-ads-diagnosis.json";
124518
+ var GOOGLE_ADS_DIAGNOSIS_TOP_LEVEL_KEYS = [
124519
+ "accountInfo",
124520
+ "diagnosisOverview",
124521
+ "metrics",
124522
+ "structure",
124523
+ "campaigns",
124524
+ "geographic",
124525
+ "keywords",
124526
+ "conversionCost",
124527
+ "goldAccount",
124528
+ "budgetCompetitiveness",
124529
+ "summary"
124530
+ ];
124531
+ var GOOGLE_ADS_DIAGNOSIS_NARRATIVE_MODULE_KEYS = [
124532
+ "metrics",
124533
+ "campaigns",
124534
+ "geographic",
124535
+ "keywords",
124536
+ "conversionCost",
124537
+ "fullGeographic",
124538
+ "fullDevice",
124539
+ "fullAudience",
124540
+ "fullCustomAudience",
124541
+ "fullKeywords",
124542
+ "fullSearchTerms",
124543
+ "broadKeywordsCount",
124544
+ "biddingStrategy",
124545
+ "adCreativeOptimization",
124546
+ "newFeatures",
124547
+ "landingPageAnalysis"
124548
+ ];
124549
+ function buildGoogleAdsDiagnosisNarrativeChecks() {
124550
+ const checks = [
124551
+ { label: "diagnosisOverview.advantages", path: ["diagnosisOverview", "advantages"] },
124552
+ { label: "diagnosisOverview.disadvantages", path: ["diagnosisOverview", "disadvantages"] },
124553
+ { label: "summary.keyIssues", path: ["summary", "keyIssues"] },
124554
+ { label: "summary.optimizationRoadmap", path: ["summary", "optimizationRoadmap"] },
124555
+ { label: "accountInfo.businessModel", path: ["accountInfo", "businessModel"] },
124556
+ { label: "accountInfo.industry", path: ["accountInfo", "industry"] }
124557
+ ];
124558
+ for (const mod of GOOGLE_ADS_DIAGNOSIS_NARRATIVE_MODULE_KEYS) {
124559
+ checks.push(
124560
+ { label: `${mod}.analysis`, path: [mod, "analysis"], requireParent: mod },
124561
+ { label: `${mod}.suggestions`, path: [mod, "suggestions"], requireParent: mod }
124562
+ );
124563
+ }
124564
+ return checks;
124565
+ }
124566
+ var GOOGLE_ADS_DIAGNOSIS_NARRATIVE_CHECKS = buildGoogleAdsDiagnosisNarrativeChecks();
124567
+ function resolveSkillTemplatePath2(basename11) {
124568
+ const dir = path19.dirname(fileURLToPath5(import.meta.url));
124569
+ const candidates = [
124570
+ path19.join(dir, "skill", "report-templates", basename11),
124571
+ path19.join(dir, "skill", "siluzan-ads", "report-templates", basename11),
124572
+ path19.join(dir, "..", "..", "assets", "siluzan-ads", "report-templates", basename11)
124573
+ ];
124574
+ for (const p of candidates) {
124575
+ if (fs14.existsSync(p)) return p;
124576
+ }
124577
+ return candidates[0];
124578
+ }
124579
+ function googleAdsDiagnosisReportTemplatePath() {
124580
+ return resolveSkillTemplatePath2(GOOGLE_ADS_DIAGNOSIS_TEMPLATE_BASENAME);
124581
+ }
124582
+ function injectGoogleAdsDiagnosisData(html, payload) {
124583
+ if (!html.includes(GOOGLE_ADS_DIAGNOSIS_DATA_MARKER)) {
124584
+ throw new Error(
124585
+ "\u62A5\u544A\u6A21\u677F\u7F3A\u5C11 __REPORT_DATA__ \u951A\u70B9\uFF0C\u8BF7\u66F4\u65B0 GoogleAdsDiagnosisReport.html"
124586
+ );
124587
+ }
124588
+ const json = JSON.stringify(payload).replace(/</g, "\\u003c");
124589
+ return html.replace(GOOGLE_ADS_DIAGNOSIS_DATA_MARKER, json);
124590
+ }
124591
+ function buildGoogleAdsDiagnosisCollectAgentHint(snapDir) {
124592
+ const dir = snapDir ?? "<snap>";
124593
+ return [
124594
+ "Google Ads \u8BCA\u65AD collect \u5DF2\u5B8C\u6210\uFF08\u4EC5\u4E8B\u5B9E\u6570\u636E\uFF0C\u4E0D\u542B\u5EFA\u8BAE\u6027\u6587\u6848\uFF09\u3002",
124595
+ `1) Read ${dir}/${GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME}\uFF08reportData + agentBrief\uFF09\uFF1B`,
124596
+ "2) Read references/report-templates/google-ads-diagnosis.md\uFF0C\u7531 Agent \u64B0\u5199\u5168\u90E8 analysis/suggestions\u3001diagnosisOverview\u3001summary\u3001accountInfo.businessModel/industry\u3001budgetCompetitiveness[].strategy\u3001newFeatures[].optimizerRecommendation\u3001adCreativeOptimization.items[].suggestion \u7B49\uFF1B",
124597
+ `3) \u5C06 reportData \u4E0E\u53D9\u4E8B\u5B57\u6BB5\u5408\u5E76\u5199\u5165 ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}\uFF1B`,
124598
+ `4) siluzan-tso google-ads-diagnosis render --data ${dir}/${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME} --out ${dir}/google-ads-diagnosis-report.html\u3002`,
124599
+ "\u7981\u6B62\u5728\u672A\u7ECF\u8FC7 Agent \u64B0\u5199\u7684\u60C5\u51B5\u4E0B\u76F4\u63A5 render collect \u4EA7\u7269\uFF1B\u7F3A\u4EFB\u4E00\u5EFA\u8BAE\u5B57\u6BB5 render \u4F1A\u5931\u8D25\u3002"
124600
+ ].join("\n");
124601
+ }
124602
+
124603
+ // src/commands/google-ads-diagnosis/build-report-data.ts
124604
+ import fs16 from "fs";
124605
+ import path21 from "path";
124606
+
124607
+ // src/commands/google-ads-diagnosis/agent-brief.ts
124608
+ function buildGoogleAdsDiagnosisAgentBrief(input) {
124609
+ const o = input.overview;
124610
+ return {
124611
+ purpose: "\u53EA\u8BFB\u4E8B\u5B9E\u6458\u8981\uFF1AAgent \u987B\u7ED3\u5408 reportData \u4E0E references/report-templates/google-ads-diagnosis.md \u64B0\u5199\u5168\u90E8 analysis/suggestions\uFF0C\u7981\u6B62\u7167\u642C\u6A21\u677F\u5957\u8BDD\u3002",
124612
+ accountId: input.accountId,
124613
+ period: input.period,
124614
+ currencyCode: input.currencyCode,
124615
+ companyName: input.companyName,
124616
+ website: input.website,
124617
+ snapshotDir: input.snapshotDir,
124618
+ previousComparisonPeriod: input.previousPeriod,
124619
+ accountOverview: {
124620
+ cost: o.cost,
124621
+ impressions: o.impressions,
124622
+ clicks: o.clicks,
124623
+ conversions: o.conversions,
124624
+ ctr: o.ctr,
124625
+ cvr: o.cvr,
124626
+ cpa: o.cpa,
124627
+ previousPeriod: {
124628
+ cost: o.prevCost,
124629
+ conversions: o.prevConversions,
124630
+ ctr: o.prevCtr,
124631
+ cvr: o.prevCvr,
124632
+ cpa: o.prevCpa
124633
+ }
124634
+ },
124635
+ competitiveMetrics: input.dimension,
124636
+ structure: input.structure,
124637
+ goldAccountScore: input.goldAccount.goldAccountScore,
124638
+ goldAccountUnqualifiedCount: input.goldAccount.goldAccountUnqualifiedCount,
124639
+ goldAccountFlags: input.goldAccount,
124640
+ topCampaignsBySpend: input.topCampaigns.slice(0, 5),
124641
+ topGeosBySpend: input.topGeos.slice(0, 5),
124642
+ topKeywordsBySpend: input.topKeywords.slice(0, 10),
124643
+ topSearchTermsBySpend: input.topSearchTerms.slice(0, 10),
124644
+ conversionActions: input.conversionActions,
124645
+ landingPageMetrics: input.landingPage,
124646
+ dataHints: {
124647
+ campaigns: input.campaignHints,
124648
+ keywords: input.keywordHints
124649
+ },
124650
+ agentDeliverable: {
124651
+ outputFile: "google-ads-diagnosis.json",
124652
+ structure: "\u4E0E reportData \u540C\u7ED3\u6784\u7684 sectionData\uFF1B\u987B\u586B diagnosisOverview\u3001metrics.analysis/suggestions\u3001\u5404\u6A21\u5757 analysis/suggestions\u3001summary.keyIssues\u3001summary.optimizationRoadmap[]\uFF08priority/focusArea/actionItems/expectedOutcome\uFF09\u3001budgetCompetitiveness[].strategy\u3001newFeatures[].optimizerRecommendation \u7B49",
124653
+ thenRun: "siluzan-tso google-ads-diagnosis render --data ./<snap>/google-ads-diagnosis.json --out ./<snap>/google-ads-diagnosis-report.html"
124654
+ }
124655
+ };
124656
+ }
124657
+
124658
+ // src/commands/google-ads-diagnosis/default-section-data.ts
124659
+ function createDefaultDiagnosisReportData() {
124660
+ return {
124661
+ accountInfo: {
124662
+ companyId: "",
124663
+ companyName: "",
124664
+ currencyCode: "",
124665
+ period: "",
124666
+ conversionAction: "\u8BE2\u76D8/\u8F6C\u5316",
124667
+ website: "",
124668
+ businessModel: "",
124669
+ industry: ""
124670
+ },
124671
+ diagnosisOverview: { advantages: [], disadvantages: [] },
124672
+ metrics: {
124673
+ cost: 0,
124674
+ impressions: 0,
124675
+ clicks: 0,
124676
+ ctr: 0,
124677
+ conversions: 0,
124678
+ cvr: 0,
124679
+ cpa: 0,
124680
+ averageCost: "",
124681
+ averageConversions: "",
124682
+ averageCtr: [0, 0],
124683
+ averageCvr: [0, 0],
124684
+ averageCpa: [0, 0],
124685
+ analysis: "",
124686
+ suggestions: ""
124687
+ },
124688
+ structure: {
124689
+ campaignCount: 0,
124690
+ adGroupCount: 0,
124691
+ keywordCount: 0,
124692
+ adCount: 0,
124693
+ extensionCount: 0,
124694
+ negativeKeywordCount: 0,
124695
+ countriesWithConversionsCount: 0
124696
+ },
124697
+ conversionGoals: { items: [] },
124698
+ campaigns: { items: [], analysis: "", suggestions: "" },
124699
+ geographic: { items: [], analysis: "", suggestions: "" },
124700
+ keywords: { items: [], analysis: "", suggestions: "" },
124701
+ conversionCost: { items: [], analysis: "", suggestions: "" },
124702
+ goldAccount: {},
124703
+ budgetCompetitiveness: [
124704
+ {
124705
+ metric: "budgetUtilizationRate",
124706
+ name: "\u9884\u7B97\u5229\u7528\u7387",
124707
+ reportValue: "",
124708
+ benchmark: "",
124709
+ strategy: ""
124710
+ },
124711
+ {
124712
+ metric: "searchImpressionShare",
124713
+ name: "\u641C\u7D22\u5C55\u793A\u4EFD\u989D",
124714
+ reportValue: "",
124715
+ benchmark: "",
124716
+ strategy: ""
124717
+ },
124718
+ {
124719
+ metric: "searchBudgetLostImpressionShare",
124720
+ name: "\u56E0\u9884\u7B97\u4E22\u5931\u7684\u4EFD\u989D",
124721
+ reportValue: "",
124722
+ benchmark: "",
124723
+ strategy: ""
124724
+ },
124725
+ {
124726
+ metric: "searchRankLostImpressionShare",
124727
+ name: "\u56E0\u6392\u540D\u4E22\u5931\u7684\u4EFD\u989D",
124728
+ reportValue: "",
124729
+ benchmark: "",
124730
+ strategy: ""
124731
+ },
124732
+ {
124733
+ metric: "coreSeriesISTarget",
124734
+ name: "\u6838\u5FC3\u7CFB\u5217 IS \u76EE\u6807",
124735
+ reportValue: "",
124736
+ benchmark: "",
124737
+ strategy: ""
124738
+ }
124739
+ ],
124740
+ fullGeographic: { items: [], analysis: "", suggestions: "" },
124741
+ fullDevice: { items: [], analysis: "", suggestions: "" },
124742
+ fullAudience: { items: [], analysis: "", suggestions: "" },
124743
+ fullCustomAudience: { items: [], analysis: "", suggestions: "" },
124744
+ landingPageAnalysis: {
124745
+ desktop: {
124746
+ score: 0,
124747
+ firstContentfulPaint: 0,
124748
+ firstMeaningfulPaint: 0,
124749
+ speedIndex: 0
124750
+ },
124751
+ mobile: {
124752
+ score: 0,
124753
+ firstContentfulPaint: 0,
124754
+ firstMeaningfulPaint: 0,
124755
+ speedIndex: 0
124756
+ }
124757
+ },
124758
+ fullKeywords: { items: [], analysis: "", suggestions: "" },
124759
+ fullSearchTerms: { items: [], analysis: "", suggestions: "" },
124760
+ broadKeywordsCount: { broadTotal: 0, total: 0, analysis: "", suggestions: "" },
124761
+ biddingStrategy: { items: [], analysis: "", suggestions: "" },
124762
+ adCreativeOptimization: { items: [], analysis: "", suggestions: "" },
124763
+ newFeatures: {
124764
+ items: [
124765
+ {
124766
+ strategy: "PerformanceMax",
124767
+ strategyName: "\u6548\u679C\u6700\u5927\u5316",
124768
+ accountStatus: false,
124769
+ optimizerRecommendation: "\u5F3A\u70C8\u63A8\u8350\u542F\u7528\u3002\u5229\u7528 Google AI \u81EA\u52A8\u89E6\u8FBE\u5168\u6E20\u9053\u9AD8\u4EF7\u503C\u5BA2\u6237\uFF0C\u6700\u5927\u5316\u8F6C\u5316\u4EF7\u503C\u3002"
124770
+ },
124771
+ {
124772
+ strategy: "KwMatch",
124773
+ strategyName: "\u5173\u952E\u8BCD\u5E7F\u6CDB\u5339\u914D+\u81EA\u52A8\u51FA\u4EF7",
124774
+ accountStatus: false,
124775
+ optimizerRecommendation: "\u7ED3\u5408\u4F18\u5316\u540E\u7684\u5339\u914D\u7C7B\u578B\u548C\u81EA\u52A8\u51FA\u4EF7\u7B56\u7565\uFF0C\u5229\u7528 Google AI \u63D0\u5347\u6548\u7387\u3002"
124776
+ },
124777
+ {
124778
+ strategy: "DemandGen",
124779
+ strategyName: "\u9700\u6C42\u5F00\u53D1\u5E7F\u544A",
124780
+ accountStatus: false,
124781
+ optimizerRecommendation: "\u542F\u7528 Demand Gen \u5E7F\u544A\uFF0C\u901A\u8FC7\u89C6\u9891\u548C\u5C55\u793A\u5E7F\u544A\u5F62\u5F0F\uFF0C\u63D0\u5347\u54C1\u724C\u5BA3\u4F20\u548C\u8F6C\u5316\u6548\u679C\u3002"
124782
+ },
124783
+ {
124784
+ strategy: "Youtube",
124785
+ strategyName: "Youtube",
124786
+ accountStatus: false,
124787
+ optimizerRecommendation: "\u542F\u7528Youtube\u5E7F\u544A\uFF0C\u7528\u89C6\u9891\u6545\u4E8B\u7CBE\u51C6\u9501\u5B9A\u9AD8\u610F\u5411\u53D7\u4F17\uFF0C\u9AD8\u6548\u63D0\u5347\u54C1\u724C\u8BB0\u5FC6\u548C\u8F6C\u5316\u6548\u679C\u3002"
124788
+ }
124789
+ ],
124790
+ analysis: "",
124791
+ suggestions: ""
124792
+ },
124793
+ summary: { keyIssues: [], optimizationRoadmap: [] }
124794
+ };
124795
+ }
124796
+
124797
+ // src/commands/google-ads-diagnosis/empty-narrative.ts
124798
+ function emptyDiagnosisOverview() {
124799
+ return { advantages: [], disadvantages: [] };
124800
+ }
124801
+ function emptyModuleNarrative() {
124802
+ return { analysis: [], suggestions: [] };
124803
+ }
124804
+ function emptySummary() {
124805
+ return { keyIssues: [], optimizationRoadmap: [] };
124806
+ }
124807
+
124808
+ // src/commands/google-ads-diagnosis/snapshot-io.ts
124809
+ init_google_analysis_snapshot();
124810
+ import fs15 from "fs";
124811
+ import path20 from "path";
124812
+ function dateRangeManifest(start, end) {
124813
+ if (start && end) return { mode: "range", start, end };
124814
+ return null;
124815
+ }
124816
+ function findSectionSnapshotFile(snapDir, section, accountId, start, end) {
124817
+ const exactName = googleAnalysisSectionFilename(
124818
+ section,
124819
+ accountId,
124820
+ dateRangeManifest(start, end)
124821
+ );
124822
+ const exactPath = path20.join(snapDir, exactName);
124823
+ if (fs15.existsSync(exactPath)) return exactPath;
124824
+ if (start && end) return void 0;
124825
+ const files = fs15.readdirSync(snapDir);
124826
+ return files.filter((f) => f.startsWith(`${section}-${accountId}`) && f.endsWith(".json")).sort().map((f) => path20.join(snapDir, f)).at(-1);
124827
+ }
124828
+ function loadSectionSnapshot(snapDir, section, accountId, start, end) {
124829
+ const file = findSectionSnapshotFile(snapDir, section, accountId, start, end);
124830
+ if (!file || !fs15.existsSync(file)) return null;
124831
+ try {
124832
+ return JSON.parse(fs15.readFileSync(file, "utf8"));
124833
+ } catch {
124834
+ return null;
124835
+ }
124836
+ }
124837
+ function sectionItems(env) {
124838
+ if (!env?.items || !Array.isArray(env.items)) return [];
124839
+ return env.items.filter(
124840
+ (x) => !!x && typeof x === "object" && !Array.isArray(x)
124841
+ );
124842
+ }
124843
+ function sectionRecord(env) {
124844
+ const r = env?.record;
124845
+ if (r && typeof r === "object" && !Array.isArray(r)) return r;
124846
+ return null;
124847
+ }
124848
+
124849
+ // src/commands/google-ads-diagnosis/build-report-data.ts
124850
+ function num(value) {
124851
+ const n = Number(value);
124852
+ return Number.isFinite(n) ? n : 0;
124853
+ }
124854
+ function rowCost(row) {
124855
+ return num(row.cost ?? row.spend);
124856
+ }
124857
+ function pct(value) {
124858
+ return `${(value * 100).toFixed(2)}%`;
124859
+ }
124860
+ function rateChange(current, previous) {
124861
+ if (previous === 0) return current === 0 ? 0 : null;
124862
+ return (current - previous) / previous;
124863
+ }
124864
+ function formatPeriod(start, end) {
124865
+ return `${start} ~ ${end}`;
124866
+ }
124867
+ function previousDateRange(start, end) {
124868
+ const s = /* @__PURE__ */ new Date(`${start}T00:00:00Z`);
124869
+ const e = /* @__PURE__ */ new Date(`${end}T00:00:00Z`);
124870
+ const days = Math.max(1, Math.round((e.getTime() - s.getTime()) / 864e5) + 1);
124871
+ const prevEnd = new Date(s.getTime() - 864e5);
124872
+ const prevStart = new Date(prevEnd.getTime() - (days - 1) * 864e5);
124873
+ return {
124874
+ start: prevStart.toISOString().slice(0, 10),
124875
+ end: prevEnd.toISOString().slice(0, 10)
124876
+ };
124877
+ }
124878
+ function asPeriodRecord(value) {
124879
+ if (value && typeof value === "object" && !Array.isArray(value)) {
124880
+ return value;
124881
+ }
124882
+ return null;
124883
+ }
124884
+ function toKeyMetricRow(title, current, previous) {
124885
+ const currentCost = rowCost(current);
124886
+ const currentClicks = num(current.clicks);
124887
+ const currentCvr = num(current.conversionRate ?? current.cvr);
124888
+ const previousCost = previous ? rowCost(previous) : null;
124889
+ const previousClicks = previous ? num(previous.clicks) : null;
124890
+ const previousCvr = previous ? num(previous.conversionRate ?? previous.cvr) : null;
124891
+ return {
124892
+ title,
124893
+ currentCost,
124894
+ previousCost,
124895
+ costRateChange: previous !== null && previousCost !== null ? rateChange(currentCost, previousCost) : null,
124896
+ currentClicks,
124897
+ previousClicks,
124898
+ clicksRateChange: previous !== null && previousClicks !== null ? rateChange(currentClicks, previousClicks) : null,
124899
+ currentCvr,
124900
+ previousCvr,
124901
+ cvrRateChange: previous !== null && previousCvr !== null ? rateChange(currentCvr, previousCvr) : null
124902
+ };
124903
+ }
124904
+ function indexByKey(rows, keyFn) {
124905
+ const map = /* @__PURE__ */ new Map();
124906
+ for (const row of rows) {
124907
+ const key = keyFn(row);
124908
+ if (key) map.set(key, row);
124909
+ }
124910
+ return map;
124911
+ }
124912
+ function buildComparisonItems(currentRows, previousRows, titleFn, keyFn) {
124913
+ const prevMap = indexByKey(previousRows, keyFn);
124914
+ return currentRows.map(
124915
+ (row) => toKeyMetricRow(titleFn(row), row, prevMap.get(keyFn(row)) ?? null)
124916
+ );
124917
+ }
124918
+ function toTargetingItem(row, labelKey, label) {
124919
+ const cost = rowCost(row);
124920
+ const clicks = num(row.clicks);
124921
+ const impressions = num(row.impressions);
124922
+ const conversions = num(row.conversions);
124923
+ const ctr = num(row.ctr);
124924
+ const cvr = num(row.conversionRate ?? row.cvr);
124925
+ const cpc = num(row.averageCpc ?? row.cpc);
124926
+ const cpa = conversions > 0 ? cost / conversions : num(row.costPerConversion ?? row.cpa);
124927
+ return {
124928
+ [labelKey]: label,
124929
+ cost,
124930
+ impressions,
124931
+ clicks,
124932
+ conversions,
124933
+ ctr,
124934
+ cvr,
124935
+ cpc,
124936
+ cpa
124937
+ };
124938
+ }
124939
+ function toDailyConversionItem(row) {
124940
+ const cost = rowCost(row);
124941
+ const conversions = num(row.conversions);
124942
+ const rawDate = String(row.date ?? "");
124943
+ const date = rawDate.includes("T") ? rawDate.slice(0, 10) : rawDate.slice(0, 10);
124944
+ const cpa = conversions > 0 ? cost / conversions : num(row.costPerConversion ?? row.cpa);
124945
+ return { date, cost, conversions, cpa, clicks: num(row.clicks), impressions: num(row.impressions) };
124946
+ }
124947
+ function loadCompanyNameFromSnap(snapDir) {
124948
+ const file = path21.join(snapDir, "list-accounts-google.json");
124949
+ if (!fs16.existsSync(file)) return void 0;
124950
+ try {
124951
+ const parsed = JSON.parse(fs16.readFileSync(file, "utf8"));
124952
+ const item = parsed.items?.[0];
124953
+ return item?.ma?.mediaCustomerName?.trim() || item?.mag?.advertiserName?.trim() || void 0;
124954
+ } catch {
124955
+ return void 0;
124956
+ }
124957
+ }
124958
+ function inferBusinessProfile(campaignRows, adRows, website) {
124959
+ const blob = [
124960
+ ...campaignRows.map((r) => String(r.campaignName ?? "")),
124961
+ ...adRows.map((r) => String(r.adTitle ?? r.adDescription ?? "")),
124962
+ website
124963
+ ].join(" ").toLowerCase();
124964
+ if (/drone|tether|uav|系留|清洗|cleaning|facade/.test(blob)) {
124965
+ return {
124966
+ businessModel: "B2B \u5916\u8D38\uFF08\u5DE5\u4E1A\u65E0\u4EBA\u673A / \u7CFB\u7559\u7CFB\u7EDF / \u6E05\u6D01\u8BBE\u5907\u5236\u9020\uFF0C\u4EE5\u8BE2\u76D8\u4E0E\u8868\u5355\u8F6C\u5316\u4E3A\u76EE\u6807\uFF09",
124967
+ industry: "\u5DE5\u4E1A\u65E0\u4EBA\u673A\u4E0E\u9AD8\u7A7A\u6E05\u6D01\u8BBE\u5907"
124968
+ };
124969
+ }
124970
+ if (/b2b|factory|manufacturer|oem/.test(blob)) {
124971
+ return {
124972
+ businessModel: "B2B \u5916\u8D38\u8BE2\u76D8\u83B7\u5BA2",
124973
+ industry: "\u5236\u9020\u4E1A\u51FA\u53E3"
124974
+ };
124975
+ }
124976
+ return {
124977
+ businessModel: "B2B \u5916\u8D38\uFF08Google \u641C\u7D22\u83B7\u5BA2 + \u843D\u5730\u9875\u8F6C\u5316\uFF09",
124978
+ industry: "\u8DE8\u5883 B2B"
124979
+ };
124980
+ }
124981
+ function primaryConversionLabel(rows) {
124982
+ const sorted = [...rows].sort((a, b) => num(b.allConversions) - num(a.allConversions));
124983
+ const top = sorted.find((r) => num(r.allConversions) > 0 && String(r.status) === "Enabled");
124984
+ if (top) return String(top.name ?? "\u8BE2\u76D8/\u8F6C\u5316");
124985
+ const enabled = rows.find((r) => String(r.status) === "Enabled");
124986
+ return enabled ? String(enabled.name ?? "\u8BE2\u76D8/\u8F6C\u5316") : "\u8BE2\u76D8/\u8F6C\u5316";
124987
+ }
124988
+ function campaignDurationDays(row, endDate) {
124989
+ const startRaw = String(row.startDate ?? "");
124990
+ if (!startRaw) return 0;
124991
+ const start = /* @__PURE__ */ new Date(`${startRaw}T00:00:00Z`);
124992
+ const end = /* @__PURE__ */ new Date(`${endDate}T00:00:00Z`);
124993
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return 0;
124994
+ return Math.max(1, Math.round((end.getTime() - start.getTime()) / 864e5) + 1);
124995
+ }
124996
+ function buildBiddingStrategyItems(campaignRows, endDate, gold) {
124997
+ const policyOk = gold?.biddingPolicyQualified === true;
124998
+ return campaignRows.map((row) => {
124999
+ const strategy = String(row.biddingStrategyTypeV2 ?? row.biddingStrategyType ?? "UNKNOWN");
125000
+ const status = String(row.campaignStatus ?? row.campaignStatusV2 ?? "");
125001
+ const isPaused = /paused/i.test(status);
125002
+ const recommended = num(row.conversions) > 0 ? "MAXIMIZE_CONVERSIONS" : "TARGET_SPEND";
125003
+ const isCorrect = policyOk && !isPaused && strategy === recommended;
125004
+ return {
125005
+ campaignName: String(row.campaignName ?? "\u672A\u547D\u540D\u7CFB\u5217"),
125006
+ duration: campaignDurationDays(row, endDate),
125007
+ biddingStrategyType: strategy,
125008
+ recommendedStrategy: recommended,
125009
+ recommendationReason: "",
125010
+ isCorrect
125011
+ };
125012
+ });
125013
+ }
125014
+ function buildStructureCounts(resourceRecord, campaignRows, keywordRows, searchRows) {
125015
+ const campaignCount = Math.max(num(resourceRecord?.campaignCount), campaignRows.length);
125016
+ const keywordCount = Math.max(num(resourceRecord?.keywordCount), keywordRows.length);
125017
+ return {
125018
+ campaignCount,
125019
+ adGroupCount: num(resourceRecord?.adGroupCount),
125020
+ keywordCount,
125021
+ adCount: num(resourceRecord?.adCount),
125022
+ extensionCount: num(resourceRecord?.extensionCount),
125023
+ negativeKeywordCount: num(resourceRecord?.negativeKeywordCount),
125024
+ countriesWithConversionsCount: num(resourceRecord?.countriesWithConversionsCount),
125025
+ searchTermCount: searchRows.length
125026
+ };
125027
+ }
125028
+ function loadWebsiteFromAds(snapDir, accountId, start, end) {
125029
+ const ads = sectionItems(loadSectionSnapshot(snapDir, "ads", accountId, start, end));
125030
+ for (const ad of ads) {
125031
+ const url = String(ad.finalurl ?? ad.destinationUrl ?? "").trim();
125032
+ if (url) return url;
125033
+ }
125034
+ return "";
125035
+ }
125036
+ function buildKeywordShareItems(rows) {
125037
+ const totalCost = rows.reduce((sum, row) => sum + rowCost(row), 0);
125038
+ const totalConversions = rows.reduce((sum, row) => sum + num(row.conversions), 0);
125039
+ return rows.map((row) => {
125040
+ const cost = rowCost(row);
125041
+ const conversions = num(row.conversions);
125042
+ const keywordText = String(row.keywordText ?? row.keyword ?? "").trim();
125043
+ return {
125044
+ keyword: keywordText || "\u672A\u547D\u540D",
125045
+ cpc: num(row.averageCpc ?? row.cpc),
125046
+ matchType: String(
125047
+ row.keywordMatchTypeZh ?? row.keywordMatchType ?? row.matchType ?? ""
125048
+ ),
125049
+ impressions: num(row.impressions),
125050
+ clicks: num(row.clicks),
125051
+ cost,
125052
+ conversions,
125053
+ costShare: totalCost > 0 ? cost / totalCost * 100 : 0,
125054
+ conversionsShare: totalConversions > 0 ? conversions / totalConversions * 100 : 0,
125055
+ cpa: conversions > 0 ? cost / conversions : 0,
125056
+ ctr: num(row.ctr),
125057
+ cvr: num(row.conversionRate ?? row.cvr)
125058
+ };
125059
+ });
125060
+ }
125061
+ function buildSearchTermItems(rows) {
125062
+ const sorted = [...rows].sort((a, b) => rowCost(b) - rowCost(a));
125063
+ return sorted.slice(0, 50).map((row) => {
125064
+ const cost = rowCost(row);
125065
+ const conversions = num(row.conversions);
125066
+ const cpa = conversions > 0 ? num(row.costPerConversion) || cost / conversions : void 0;
125067
+ return {
125068
+ searchTerm: String(row.searchTermText ?? row.searchTerm ?? "\u672A\u547D\u540D"),
125069
+ keyword: String(row.keywordText ?? row.keyword ?? ""),
125070
+ cost,
125071
+ conversions,
125072
+ clicks: num(row.clicks),
125073
+ impressions: num(row.impressions),
125074
+ ctr: num(row.ctr),
125075
+ cvr: num(row.conversionRate ?? row.cvr),
125076
+ cpa
125077
+ };
125078
+ });
125079
+ }
125080
+ function countBroadKeywords(rows) {
125081
+ let broadTotal = 0;
125082
+ for (const row of rows) {
125083
+ const match = String(row.keywordMatchType ?? row.matchType ?? "").toLowerCase();
125084
+ if (match.includes("broad")) broadTotal += 1;
125085
+ }
125086
+ return { broadTotal, total: rows.length };
125087
+ }
125088
+ function buildAdCreativeItems(rows) {
125089
+ return rows.slice(0, 20).map((row) => {
125090
+ const headlines = [
125091
+ row.headlinePart1,
125092
+ row.headlinePart2,
125093
+ row.headlinePart3,
125094
+ ...Array.isArray(row.addtionalHeadlines) ? row.addtionalHeadlines : [],
125095
+ ...Array.isArray(row.headlines) ? row.headlines : []
125096
+ ].filter((x) => String(x ?? "").trim());
125097
+ const descriptions = [
125098
+ row.adDescription,
125099
+ row.adDescription2,
125100
+ ...Array.isArray(row.addtionalAdDescriptions) ? row.addtionalAdDescriptions : [],
125101
+ ...Array.isArray(row.descriptions) ? row.descriptions : []
125102
+ ].filter((x) => String(x ?? "").trim());
125103
+ const headlinesCount = headlines.length;
125104
+ const descriptionsCount = descriptions.length;
125105
+ return {
125106
+ adTitle: String(row.adTitle ?? row.campaign ?? "\u672A\u547D\u540D\u5E7F\u544A"),
125107
+ headlinesCount,
125108
+ headlinesStatus: headlinesCount >= 3 ? "\u826F\u597D" : headlinesCount >= 2 ? "\u5F85\u4F18\u5316" : "\u4E0D\u8FBE\u6807",
125109
+ descriptionsCount,
125110
+ descriptionsStatus: descriptionsCount >= 8 ? "\u826F\u597D" : descriptionsCount >= 4 ? "\u5F85\u4F18\u5316" : "\u4E0D\u8FBE\u6807",
125111
+ suggestion: ""
125112
+ };
125113
+ });
125114
+ }
125115
+ function buildBudgetCompetitiveness(overviewRecord, dimensionRecord) {
125116
+ const current = asPeriodRecord(overviewRecord?.currentPeriod);
125117
+ const totalCost = num(overviewRecord?.totalCost ?? current?.spend);
125118
+ const activeDays = num(overviewRecord?.activeDays);
125119
+ const avgDaily = num(overviewRecord?.averageDailyCost ?? (activeDays > 0 ? totalCost / activeDays : 0));
125120
+ const remainingBudget = num(overviewRecord?.remainingAccountBudget);
125121
+ const utilization = remainingBudget > 0 && avgDaily > 0 ? Math.min(100, avgDaily / (avgDaily + remainingBudget / Math.max(activeDays, 1)) * 100) : avgDaily > 0 ? 85 : 0;
125122
+ const sis = num(
125123
+ dimensionRecord?.searchImpressionShare ?? current?.searchImpressionShare
125124
+ );
125125
+ const budgetLost = num(
125126
+ dimensionRecord?.searchBudgetLostImpressionShare ?? current?.searchBudgetLostImpressionShare
125127
+ );
125128
+ const rankLost = num(
125129
+ dimensionRecord?.searchRankLostImpressionShare ?? current?.searchRankLostImpressionShare
125130
+ );
125131
+ return [
125132
+ {
125133
+ metric: "budgetUtilizationRate",
125134
+ name: "\u9884\u7B97\u5229\u7528\u7387",
125135
+ reportValue: `${utilization.toFixed(1)}%`,
125136
+ benchmark: "\u226585%",
125137
+ strategy: ""
125138
+ },
125139
+ {
125140
+ metric: "searchImpressionShare",
125141
+ name: "\u641C\u7D22\u5C55\u793A\u4EFD\u989D",
125142
+ reportValue: pct(sis),
125143
+ benchmark: "\u226540%",
125144
+ strategy: ""
125145
+ },
125146
+ {
125147
+ metric: "searchBudgetLostImpressionShare",
125148
+ name: "\u56E0\u9884\u7B97\u4E22\u5931\u7684\u4EFD\u989D",
125149
+ reportValue: pct(budgetLost),
125150
+ benchmark: "\u226410%",
125151
+ strategy: ""
125152
+ },
125153
+ {
125154
+ metric: "searchRankLostImpressionShare",
125155
+ name: "\u56E0\u6392\u540D\u4E22\u5931\u7684\u4EFD\u989D",
125156
+ reportValue: pct(rankLost),
125157
+ benchmark: "\u226430%",
125158
+ strategy: ""
125159
+ },
125160
+ {
125161
+ metric: "coreSeriesISTarget",
125162
+ name: "\u6838\u5FC3\u7CFB\u5217 IS \u76EE\u6807",
125163
+ reportValue: pct(sis),
125164
+ benchmark: "\u226550%",
125165
+ strategy: ""
125166
+ }
125167
+ ];
125168
+ }
125169
+ function buildGoogleAdsDiagnosisCollectPayload(opts) {
125170
+ const { snapDir, accountId, start, end } = opts;
125171
+ const prev = previousDateRange(start, end);
125172
+ const data = createDefaultDiagnosisReportData();
125173
+ const overviewEnv = loadSectionSnapshot(snapDir, "overview", accountId, start, end);
125174
+ const overviewRecord = sectionRecord(overviewEnv);
125175
+ const currentPeriod = asPeriodRecord(overviewRecord?.currentPeriod);
125176
+ const previousPeriod = asPeriodRecord(overviewRecord?.previousPeriod);
125177
+ const dimensionRecord = sectionRecord(
125178
+ loadSectionSnapshot(snapDir, "dimension-summary", accountId, start, end)
125179
+ );
125180
+ const resourceRecord = sectionRecord(
125181
+ loadSectionSnapshot(snapDir, "resource-counts", accountId, start, end)
125182
+ );
125183
+ const goldRecord = sectionRecord(loadSectionSnapshot(snapDir, "gold-account", accountId, start, end)) ?? {};
125184
+ const campaignCurrent = sectionItems(
125185
+ loadSectionSnapshot(snapDir, "campaigns", accountId, start, end)
125186
+ );
125187
+ const adRows = sectionItems(loadSectionSnapshot(snapDir, "ads", accountId, start, end));
125188
+ const kwCurrent = sectionItems(
125189
+ loadSectionSnapshot(snapDir, "keywords", accountId, start, end)
125190
+ );
125191
+ const searchRows = sectionItems(
125192
+ loadSectionSnapshot(snapDir, "search-terms", accountId, start, end)
125193
+ );
125194
+ const conversionRows = sectionItems(
125195
+ loadSectionSnapshot(snapDir, "conversion-actions", accountId, start, end)
125196
+ );
125197
+ const companyName = opts.companyName?.trim() || loadCompanyNameFromSnap(snapDir) || "";
125198
+ const website = opts.website?.trim() || loadWebsiteFromAds(snapDir, accountId, start, end);
125199
+ const currencyCode = String(overviewRecord?.currencyCode ?? "USD");
125200
+ const profile = inferBusinessProfile(campaignCurrent, adRows, website);
125201
+ const conversionAction = primaryConversionLabel(conversionRows);
125202
+ data.accountInfo = {
125203
+ companyId: accountId,
125204
+ companyName,
125205
+ currencyCode,
125206
+ period: formatPeriod(start, end),
125207
+ conversionAction,
125208
+ website,
125209
+ businessModel: "",
125210
+ industry: ""
125211
+ };
125212
+ data.reportDate = end;
125213
+ const cost = rowCost(currentPeriod ?? overviewRecord ?? {});
125214
+ const impressions = num(currentPeriod?.impressions);
125215
+ const clicks = num(currentPeriod?.clicks);
125216
+ const conversions = num(currentPeriod?.conversions);
125217
+ const ctr = num(currentPeriod?.ctr);
125218
+ const cvr = num(currentPeriod?.conversionRate ?? currentPeriod?.cvr);
125219
+ const cpa = conversions > 0 ? cost / conversions : num(currentPeriod?.costPerConversion);
125220
+ const prevCost = previousPeriod ? rowCost(previousPeriod) : 0;
125221
+ const prevConversions = previousPeriod ? num(previousPeriod.conversions) : 0;
125222
+ const prevCtr = previousPeriod ? num(previousPeriod.ctr) : 0;
125223
+ const prevCvr = previousPeriod ? num(previousPeriod.conversionRate ?? previousPeriod.cvr) : 0;
125224
+ const prevCpa = prevConversions > 0 ? rowCost(previousPeriod) / prevConversions : num(previousPeriod?.costPerConversion);
125225
+ data.metrics = {
125226
+ cost,
125227
+ impressions,
125228
+ clicks,
125229
+ ctr,
125230
+ conversions,
125231
+ cvr,
125232
+ cpa,
125233
+ averageCost: prevCost.toFixed(2),
125234
+ averageConversions: String(Math.round(prevConversions)),
125235
+ averageCtr: [prevCtr, ctr],
125236
+ averageCvr: [prevCvr, cvr],
125237
+ averageCpa: [prevCpa, cpa],
125238
+ analysis: "",
125239
+ suggestions: ""
125240
+ };
125241
+ const structureCounts = buildStructureCounts(
125242
+ resourceRecord,
125243
+ campaignCurrent,
125244
+ kwCurrent,
125245
+ searchRows
125246
+ );
125247
+ data.structure = {
125248
+ campaignCount: structureCounts.campaignCount,
125249
+ adGroupCount: structureCounts.adGroupCount,
125250
+ keywordCount: structureCounts.keywordCount,
125251
+ adCount: structureCounts.adCount,
125252
+ extensionCount: structureCounts.extensionCount,
125253
+ negativeKeywordCount: structureCounts.negativeKeywordCount,
125254
+ countriesWithConversionsCount: structureCounts.countriesWithConversionsCount
125255
+ };
125256
+ data.conversionGoals = {
125257
+ items: conversionRows.map((row) => ({
125258
+ name: String(row.name ?? row.eventName ?? "\u672A\u77E5\u4E8B\u4EF6"),
125259
+ status: String(row.status ?? ""),
125260
+ allConversions: num(row.allConversions),
125261
+ allConversionsValue: num(row.allConversionsValue)
125262
+ }))
125263
+ };
125264
+ const campaignPrevious = sectionItems(
125265
+ loadSectionSnapshot(snapDir, "campaigns", accountId, prev.start, prev.end)
125266
+ );
125267
+ const campaignItems = buildComparisonItems(
125268
+ campaignCurrent,
125269
+ campaignPrevious,
125270
+ (row) => String(row.campaignName ?? "\u672A\u547D\u540D\u7CFB\u5217"),
125271
+ (row) => String(row.campaignId ?? row.campaignName ?? "")
125272
+ );
125273
+ data.campaigns = { items: campaignItems, ...emptyModuleNarrative() };
125274
+ const geoCurrent = sectionItems(
125275
+ loadSectionSnapshot(snapDir, "geographic", accountId, start, end)
125276
+ );
125277
+ const geoPrevious = sectionItems(
125278
+ loadSectionSnapshot(snapDir, "geographic", accountId, prev.start, prev.end)
125279
+ );
125280
+ const geoItems = buildComparisonItems(
125281
+ geoCurrent,
125282
+ geoPrevious,
125283
+ (row) => String(row.countryOrRegionZh ?? row.countryNameZh ?? row.countryOrRegion ?? "\u672A\u77E5\u5730\u57DF"),
125284
+ (row) => String(row.countryCode ?? row.countryOrRegion ?? row.countryOrRegionZh ?? "")
125285
+ );
125286
+ data.geographic = { items: geoItems, ...emptyModuleNarrative() };
125287
+ const kwPrevious = sectionItems(
125288
+ loadSectionSnapshot(snapDir, "keywords", accountId, prev.start, prev.end)
125289
+ );
125290
+ const kwItems = buildComparisonItems(
125291
+ kwCurrent,
125292
+ kwPrevious,
125293
+ (row) => String(row.keyword ?? "\u672A\u547D\u540D\u5173\u952E\u8BCD"),
125294
+ (row) => String(row.id ?? row.keyword ?? "")
125295
+ );
125296
+ data.keywords = { items: kwItems, ...emptyModuleNarrative() };
125297
+ const dailyRows = sectionItems(
125298
+ loadSectionSnapshot(snapDir, "daily-metrics", accountId, start, end)
125299
+ );
125300
+ const conversionCostItems = dailyRows.map(toDailyConversionItem);
125301
+ data.conversionCost = {
125302
+ items: conversionCostItems,
125303
+ ...emptyModuleNarrative()
125304
+ };
125305
+ data.goldAccount = goldRecord;
125306
+ data.budgetCompetitiveness = buildBudgetCompetitiveness(overviewRecord, dimensionRecord);
125307
+ data.diagnosisOverview = emptyDiagnosisOverview();
125308
+ const geoTargeting = geoCurrent.map(
125309
+ (row) => toTargetingItem(
125310
+ row,
125311
+ "geographic",
125312
+ String(row.countryOrRegionZh ?? row.countryNameZh ?? row.countryOrRegion ?? "\u672A\u77E5\u5730\u57DF")
125313
+ )
125314
+ );
125315
+ data.fullGeographic = {
125316
+ items: geoTargeting,
125317
+ ...emptyModuleNarrative()
125318
+ };
125319
+ const deviceRows = sectionItems(
125320
+ loadSectionSnapshot(snapDir, "devices", accountId, start, end)
125321
+ );
125322
+ const deviceItems = deviceRows.map(
125323
+ (row) => toTargetingItem(row, "deviceType", String(row.deviceType ?? "\u672A\u77E5\u8BBE\u5907"))
125324
+ );
125325
+ data.fullDevice = {
125326
+ items: deviceItems,
125327
+ ...emptyModuleNarrative()
125328
+ };
125329
+ const audienceRows = sectionItems(
125330
+ loadSectionSnapshot(snapDir, "audience", accountId, start, end)
125331
+ );
125332
+ data.fullAudience = {
125333
+ items: audienceRows.map(
125334
+ (row) => toTargetingItem(row, "audience", String(row.audienceName ?? row.audience ?? "\u53D7\u4F17"))
125335
+ ),
125336
+ ...emptyModuleNarrative()
125337
+ };
125338
+ data.fullCustomAudience = {
125339
+ items: [],
125340
+ ...emptyModuleNarrative()
125341
+ };
125342
+ const lighthouse = opts.landingPage ?? {};
125343
+ data.landingPageAnalysis = {
125344
+ desktop: {
125345
+ score: num(lighthouse.desktop?.score),
125346
+ firstContentfulPaint: num(lighthouse.desktop?.firstContentfulPaint),
125347
+ firstMeaningfulPaint: num(lighthouse.desktop?.firstMeaningfulPaint),
125348
+ speedIndex: num(lighthouse.desktop?.speedIndex)
125349
+ },
125350
+ mobile: {
125351
+ score: num(lighthouse.mobile?.score),
125352
+ firstContentfulPaint: num(lighthouse.mobile?.firstContentfulPaint),
125353
+ firstMeaningfulPaint: num(lighthouse.mobile?.firstMeaningfulPaint),
125354
+ speedIndex: num(lighthouse.mobile?.speedIndex)
125355
+ }
125356
+ };
125357
+ const keywordShareItems = buildKeywordShareItems(kwCurrent);
125358
+ data.fullKeywords = {
125359
+ items: keywordShareItems,
125360
+ ...emptyModuleNarrative()
125361
+ };
125362
+ data.fullSearchTerms = {
125363
+ items: buildSearchTermItems(searchRows),
125364
+ ...emptyModuleNarrative()
125365
+ };
125366
+ const broad = countBroadKeywords(kwCurrent);
125367
+ data.broadKeywordsCount = {
125368
+ broadTotal: broad.broadTotal,
125369
+ total: broad.total,
125370
+ ...emptyModuleNarrative()
125371
+ };
125372
+ const biddingItems = buildBiddingStrategyItems(campaignCurrent, end, goldRecord);
125373
+ data.biddingStrategy = {
125374
+ items: biddingItems,
125375
+ ...emptyModuleNarrative()
125376
+ };
125377
+ data.adCreativeOptimization = {
125378
+ items: buildAdCreativeItems(adRows),
125379
+ ...emptyModuleNarrative()
125380
+ };
125381
+ const campaignTypes = sectionRecord(
125382
+ loadSectionSnapshot(snapDir, "campaign-types", accountId)
125383
+ );
125384
+ const nfItems = (Array.isArray(data.newFeatures.items) ? data.newFeatures.items : []).filter((item) => String(item.strategy ?? "") !== "AiMax");
125385
+ data.newFeatures = {
125386
+ items: nfItems.map((item) => {
125387
+ const strategy = String(item.strategy ?? "");
125388
+ const enabled = strategy === "PerformanceMax" ? num(campaignTypes?.PerformanceMax) > 0 : strategy === "KwMatch" ? num(campaignTypes?.Search) > 0 : strategy === "DemandGen" ? num(campaignTypes?.DemandGen) > 0 : strategy === "Youtube" ? num(campaignTypes?.Video) > 0 || num(campaignTypes?.Youtube) > 0 : false;
125389
+ return {
125390
+ ...item,
125391
+ accountStatus: enabled,
125392
+ optimizerRecommendation: ""
125393
+ };
125394
+ }),
125395
+ ...emptyModuleNarrative()
125396
+ };
125397
+ data.summary = emptySummary();
125398
+ const zeroConvKw = kwCurrent.filter((r) => rowCost(r) > 50 && num(r.conversions) === 0);
125399
+ const agentBrief = buildGoogleAdsDiagnosisAgentBrief({
125400
+ accountId,
125401
+ period: formatPeriod(start, end),
125402
+ currencyCode,
125403
+ companyName,
125404
+ website,
125405
+ snapshotDir: snapDir,
125406
+ previousPeriod: prev,
125407
+ overview: {
125408
+ cost,
125409
+ impressions,
125410
+ clicks,
125411
+ conversions,
125412
+ ctr,
125413
+ cvr,
125414
+ cpa,
125415
+ prevCost,
125416
+ prevConversions,
125417
+ prevCtr,
125418
+ prevCvr,
125419
+ prevCpa
125420
+ },
125421
+ dimension: {
125422
+ searchImpressionShare: num(dimensionRecord?.searchImpressionShare),
125423
+ searchBudgetLostImpressionShare: num(dimensionRecord?.searchBudgetLostImpressionShare),
125424
+ searchRankLostImpressionShare: num(dimensionRecord?.searchRankLostImpressionShare)
125425
+ },
125426
+ structure: structureCounts,
125427
+ goldAccount: goldRecord,
125428
+ topCampaigns: campaignCurrent,
125429
+ topGeos: geoCurrent,
125430
+ topKeywords: kwCurrent,
125431
+ topSearchTerms: searchRows,
125432
+ conversionActions: conversionRows,
125433
+ landingPage: data.landingPageAnalysis,
125434
+ campaignHints: [
125435
+ profile.businessModel,
125436
+ profile.industry,
125437
+ ...campaignCurrent.map((r) => String(r.campaignName ?? ""))
125438
+ ].filter(Boolean),
125439
+ keywordHints: zeroConvKw.slice(0, 5).map((r) => String(r.keyword ?? ""))
125440
+ });
125441
+ return { reportData: data, agentBrief };
125442
+ }
125443
+
125444
+ // src/commands/google-ads-diagnosis/invoke-google-analysis.ts
125445
+ import { spawnSync as spawnSync2 } from "child_process";
125446
+ function invokeGoogleAnalysisCli(opts) {
125447
+ const cliEntry = process.argv[1];
125448
+ if (!cliEntry) {
125449
+ return { ok: false, status: 1, stdout: "", stderr: "\u65E0\u6CD5\u5B9A\u4F4D CLI \u5165\u53E3 process.argv[1]" };
125450
+ }
125451
+ const args = [cliEntry, "google-analysis", "-a", opts.account, "--json-out", opts.jsonOut];
125452
+ if (opts.start) args.push("--start", opts.start);
125453
+ if (opts.end) args.push("--end", opts.end);
125454
+ if (opts.sections) args.push("--sections", opts.sections);
125455
+ if (opts.token) args.push("--token", opts.token);
125456
+ if (opts.verbose) args.push("--verbose");
125457
+ const result = spawnSync2(process.execPath, args, {
125458
+ encoding: "utf8",
125459
+ stdio: ["ignore", "pipe", "pipe"]
125460
+ });
125461
+ return {
125462
+ ok: result.status === 0,
125463
+ status: result.status ?? 1,
125464
+ stdout: result.stdout ?? "",
125465
+ stderr: result.stderr ?? ""
125466
+ };
125467
+ }
125468
+
125469
+ // src/commands/google-ads-diagnosis/run-collect.ts
125470
+ function previousDateRange2(start, end) {
125471
+ const s = /* @__PURE__ */ new Date(`${start}T00:00:00Z`);
125472
+ const e = /* @__PURE__ */ new Date(`${end}T00:00:00Z`);
125473
+ const days = Math.max(1, Math.round((e.getTime() - s.getTime()) / 864e5) + 1);
125474
+ const prevEnd = new Date(s.getTime() - 864e5);
125475
+ const prevStart = new Date(prevEnd.getTime() - (days - 1) * 864e5);
125476
+ return {
125477
+ start: prevStart.toISOString().slice(0, 10),
125478
+ end: prevEnd.toISOString().slice(0, 10)
125479
+ };
125480
+ }
125481
+ async function resolveWebsiteUrl(opts, snapDir, accountId) {
125482
+ if (opts.website?.trim()) return normalizeWebsiteUrl(opts.website);
125483
+ const ads = sectionItems(
125484
+ loadSectionSnapshot(snapDir, "ads", accountId, opts.start, opts.end)
125485
+ );
125486
+ for (const ad of ads) {
125487
+ const url = String(ad.finalurl ?? ad.destinationUrl ?? "").trim();
125488
+ if (url) return normalizeWebsiteUrl(url);
125489
+ }
125490
+ return "";
125491
+ }
125492
+ async function fetchLandingPagePerformance(websiteUrl, token, verbose) {
125493
+ if (!websiteUrl) return void 0;
125494
+ try {
125495
+ const config = loadConfig(token);
125496
+ return await apiFetch2(
125497
+ `${config.apiBaseUrl}/api/WebsiteDiagnosisReports/performance?url=${encodeURIComponent(websiteUrl)}`,
125498
+ config,
125499
+ {},
125500
+ verbose
125501
+ );
125502
+ } catch (err) {
125503
+ const message = err instanceof Error ? err.message : String(err);
125504
+ console.error(`
125505
+ \u26A0\uFE0F \u7740\u9646\u9875 Lighthouse \u62C9\u53D6\u5931\u8D25\uFF1A${message}
125506
+ `);
125507
+ return void 0;
125508
+ }
125509
+ }
125510
+ async function runGoogleAdsDiagnosisCollect(opts) {
125511
+ const accountId = opts.account.trim();
125512
+ if (!/^\d+$/.test(accountId)) {
125513
+ console.error("\n\u274C --account \u987B\u4E3A\u6570\u5B57 mediaCustomerId\u3002\n");
125514
+ process.exit(1);
125515
+ }
125516
+ if (!opts.start || !opts.end) {
125517
+ console.error("\n\u274C collect \u987B\u540C\u65F6\u6307\u5B9A --start \u4E0E --end\u3002\n");
125518
+ process.exit(1);
125519
+ }
125520
+ const snapDir = path22.resolve(opts.jsonOut);
125521
+ await fs17.mkdir(snapDir, { recursive: true });
125522
+ if (!opts.skipFetch) {
125523
+ console.error(`
125524
+ \u{1F4E5} google-analysis \u62C9\u6570\uFF1A\u8D26\u6237 ${accountId}\uFF0C${opts.start} ~ ${opts.end}
125525
+ `);
125526
+ const main = invokeGoogleAnalysisCli({
125527
+ account: accountId,
125528
+ start: opts.start,
125529
+ end: opts.end,
125530
+ jsonOut: snapDir,
125531
+ token: opts.token,
125532
+ verbose: opts.verbose
125533
+ });
125534
+ if (!main.ok) {
125535
+ console.error(main.stderr || main.stdout);
125536
+ console.error("\n\u274C google-analysis \u62C9\u6570\u5931\u8D25\u3002\n");
125537
+ process.exit(main.status || 1);
125538
+ }
125539
+ if (opts.fetchPreviousPeriod !== false) {
125540
+ const prev = previousDateRange2(opts.start, opts.end);
125541
+ console.error(
125542
+ `
125543
+ \u{1F4E5} \u62C9\u53D6\u5BF9\u6BD4\u5468\u671F campaigns/geographic/keywords\uFF1A${prev.start} ~ ${prev.end}
125544
+ `
125545
+ );
125546
+ const prevRun = invokeGoogleAnalysisCli({
125547
+ account: accountId,
125548
+ start: prev.start,
125549
+ end: prev.end,
125550
+ jsonOut: snapDir,
125551
+ sections: "campaigns,geographic,keywords",
125552
+ token: opts.token,
125553
+ verbose: opts.verbose
125554
+ });
125555
+ if (!prevRun.ok) {
125556
+ console.error(prevRun.stderr || prevRun.stdout);
125557
+ console.error("\n\u26A0\uFE0F \u4E0A\u4E00\u5468\u671F\u5BF9\u6BD4\u7EF4\u5EA6\u62C9\u53D6\u5931\u8D25\uFF0C\u73AF\u6BD4\u5217\u53EF\u80FD\u4E3A\u7A7A\u3002\n");
125558
+ }
125559
+ }
125560
+ }
125561
+ const websiteUrl = await resolveWebsiteUrl(opts, snapDir, accountId);
125562
+ let landingPage;
125563
+ if (!opts.skipLandingPage && websiteUrl) {
125564
+ console.error(`
125565
+ \u{1F4E5} \u7740\u9646\u9875 Lighthouse\uFF1A${websiteUrl}
125566
+ `);
125567
+ landingPage = await fetchLandingPagePerformance(websiteUrl, opts.token, opts.verbose);
125568
+ }
125569
+ const { reportData, agentBrief } = buildGoogleAdsDiagnosisCollectPayload({
125570
+ snapDir,
125571
+ accountId,
125572
+ start: opts.start,
125573
+ end: opts.end,
125574
+ companyName: opts.companyName,
125575
+ website: websiteUrl || opts.website,
125576
+ landingPage
125577
+ });
125578
+ const collectPayload = {
125579
+ kind: "siluzan-tso-google-ads-diagnosis-collect",
125580
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
125581
+ accountId,
125582
+ period: `${opts.start} ~ ${opts.end}`,
125583
+ absoluteSnapshotDir: snapDir,
125584
+ reportData,
125585
+ agentBrief,
125586
+ agentHint: buildGoogleAdsDiagnosisCollectAgentHint(snapDir)
125587
+ };
125588
+ const collectPath = path22.join(snapDir, GOOGLE_ADS_DIAGNOSIS_COLLECT_BASENAME);
125589
+ await fs17.writeFile(collectPath, `${JSON.stringify(collectPayload, null, 2)}
125590
+ `, "utf8");
125591
+ console.log(
125592
+ JSON.stringify({
125593
+ kind: collectPayload.kind,
125594
+ absoluteSnapshotDir: snapDir,
125595
+ collectFile: collectPath,
125596
+ reportDataFile: GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME,
125597
+ accountId,
125598
+ period: collectPayload.period
125599
+ })
125600
+ );
125601
+ console.error(`
125602
+ \u2705 collect\uFF08\u4EC5\u4E8B\u5B9E\u6570\u636E\uFF09\uFF1A${collectPath}`);
125603
+ console.error(
125604
+ ` Agent\uFF1A\u8BFB agentBrief + reportData\uFF0C\u6309 google-ads-diagnosis.md \u64B0\u5199 narrative\uFF0C\u4FDD\u5B58 ${GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME}`
125605
+ );
125606
+ console.error(
125607
+ ` \u7EC8\u7A3F\uFF1Asiluzan-tso google-ads-diagnosis render --data ${path22.join(snapDir, GOOGLE_ADS_DIAGNOSIS_REPORT_BASENAME)}
125608
+ `
125609
+ );
125610
+ }
125611
+
125612
+ // src/commands/google-ads-diagnosis/render-report.ts
125613
+ import fs18 from "fs/promises";
125614
+ import path23 from "path";
125615
+
125616
+ // src/commands/google-ads-diagnosis/report-content.ts
125617
+ function asRecord5(value) {
125618
+ if (value && typeof value === "object" && !Array.isArray(value)) {
125619
+ return value;
125620
+ }
125621
+ return null;
125622
+ }
125623
+ function getNestedValue(root, path32) {
125624
+ let cur = root;
125625
+ for (const key of path32) {
125626
+ const rec = asRecord5(cur);
125627
+ if (!rec) return void 0;
125628
+ cur = rec[key];
125629
+ }
125630
+ return cur;
125631
+ }
125632
+ function hasNarrativeContent(value) {
125633
+ if (Array.isArray(value)) return value.length > 0;
125634
+ return String(value ?? "").trim().length > 0;
125635
+ }
125636
+ function validateBudgetCompetitivenessStrategies(data) {
125637
+ const items = data.budgetCompetitiveness;
125638
+ if (!Array.isArray(items)) {
125639
+ return ["budgetCompetitiveness \u987B\u4E3A\u6570\u7EC4"];
125640
+ }
125641
+ const warnings = [];
125642
+ for (let i = 0; i < items.length; i++) {
125643
+ const row = asRecord5(items[i]);
125644
+ const name2 = String(row?.name ?? row?.metric ?? i);
125645
+ if (!hasNarrativeContent(row?.strategy)) {
125646
+ warnings.push(`budgetCompetitiveness[${name2}].strategy \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
125647
+ }
125648
+ }
125649
+ return warnings;
125650
+ }
125651
+ function validateNewFeatureRecommendations(data) {
125652
+ const nf = asRecord5(data.newFeatures);
125653
+ if (!nf) return [];
125654
+ const items = nf.items;
125655
+ if (!Array.isArray(items)) return [];
125656
+ const warnings = [];
125657
+ for (let i = 0; i < items.length; i++) {
125658
+ const row = asRecord5(items[i]);
125659
+ const name2 = String(row?.strategyName ?? row?.strategy ?? i);
125660
+ if (!hasNarrativeContent(row?.optimizerRecommendation)) {
125661
+ warnings.push(`newFeatures.items[${name2}].optimizerRecommendation \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`);
125662
+ }
125663
+ }
125664
+ return warnings;
125665
+ }
125666
+ function validateAdCreativeItemSuggestions(data) {
125667
+ const creative = asRecord5(data.adCreativeOptimization);
125668
+ if (!creative) return [];
125669
+ const items = creative.items;
125670
+ if (!Array.isArray(items) || items.length === 0) return [];
125671
+ const warnings = [];
125672
+ for (let i = 0; i < items.length; i++) {
125673
+ const row = asRecord5(items[i]);
125674
+ const title = String(row?.adTitle ?? i);
125675
+ if (!hasNarrativeContent(row?.suggestion ?? row?.suggestions)) {
125676
+ warnings.push(
125677
+ `adCreativeOptimization.items[${title}].suggestion \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u586B\u5199\uFF09`
125678
+ );
125679
+ }
125680
+ }
125681
+ return warnings;
125682
+ }
125683
+ function validateGoogleAdsDiagnosisContent(data) {
125684
+ const missingKeys = GOOGLE_ADS_DIAGNOSIS_TOP_LEVEL_KEYS.filter(
125685
+ (key) => data[key] === void 0
125686
+ );
125687
+ const warnings = [];
125688
+ const accountInfo = asRecord5(data.accountInfo);
125689
+ if (accountInfo && !accountInfo.companyName && !accountInfo.period) {
125690
+ warnings.push("accountInfo \u7F3A\u5C11 companyName \u4E0E period\uFF0C\u9875\u7709\u53EF\u80FD\u4E3A\u7A7A");
125691
+ }
125692
+ for (const check of GOOGLE_ADS_DIAGNOSIS_NARRATIVE_CHECKS) {
125693
+ if (check.requireParent && data[check.requireParent] === void 0) {
125694
+ continue;
125695
+ }
125696
+ const value = getNestedValue(data, check.path);
125697
+ if (!hasNarrativeContent(value)) {
125698
+ warnings.push(`${check.label} \u4E3A\u7A7A\uFF08\u987B\u7531 Agent \u64B0\u5199\u540E\u518D render\uFF09`);
125699
+ }
125700
+ }
125701
+ warnings.push(
125702
+ ...validateBudgetCompetitivenessStrategies(data),
125703
+ ...validateNewFeatureRecommendations(data),
125704
+ ...validateAdCreativeItemSuggestions(data)
125705
+ );
125706
+ return {
125707
+ ok: missingKeys.length === 0,
125708
+ missingKeys: [...missingKeys],
125709
+ warnings
125710
+ };
125711
+ }
125712
+ function formatGoogleAdsDiagnosisContentErrors(check) {
125713
+ const parts = [];
125714
+ if (check.missingKeys.length > 0) {
125715
+ parts.push(`\u7F3A\u5C11\u9876\u5C42\u5B57\u6BB5\uFF1A${check.missingKeys.join(", ")}`);
125716
+ }
125717
+ if (check.warnings.length > 0) {
125718
+ parts.push(`\u8B66\u544A\uFF1A${check.warnings.join("\uFF1B")}`);
125719
+ }
125720
+ return parts.join("\n");
125721
+ }
125722
+
125723
+ // src/commands/google-ads-diagnosis/render-report.ts
125724
+ function asRecord6(value) {
125725
+ if (value && typeof value === "object" && !Array.isArray(value)) {
125726
+ return value;
125727
+ }
125728
+ throw new Error("\u8BCA\u65AD\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
125729
+ }
125730
+ async function runGoogleAdsDiagnosisRender(opts) {
125731
+ const dataPath = path23.resolve(opts.dataFile);
125732
+ let dataRaw;
125733
+ try {
125734
+ dataRaw = JSON.parse(await fs18.readFile(dataPath, "utf8"));
125735
+ } catch {
125736
+ console.error(`
125737
+ \u274C \u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${dataPath}
125738
+ `);
125739
+ process.exit(1);
125740
+ }
125741
+ const data = asRecord6(dataRaw);
125742
+ const contentCheck = validateGoogleAdsDiagnosisContent(data);
125743
+ if (!contentCheck.ok) {
125744
+ console.error(`
125745
+ \u274C ${formatGoogleAdsDiagnosisContentErrors(contentCheck)}
125746
+ `);
125747
+ console.error(
125748
+ " \u8BF7\u53C2\u8003 references/report-templates/google-ads-diagnosis.md \u4E0E MarkAI sectionData \u7ED3\u6784\u8865\u5168 JSON\u3002\n"
125749
+ );
125750
+ process.exit(1);
125751
+ }
125752
+ if (contentCheck.warnings.length > 0 && !opts.lenient) {
125753
+ console.error(`
125754
+ \u274C ${formatGoogleAdsDiagnosisContentErrors(contentCheck)}
125755
+ `);
125756
+ console.error(
125757
+ " \u987B\u5148\u7531 Agent \u8865\u5168\u5168\u90E8 analysis/suggestions \u53CA budgetCompetitiveness[].strategy \u7B49\u5EFA\u8BAE\u5B57\u6BB5\u540E\u518D render\uFF1B\u8C03\u8BD5\u53EF\u52A0 --lenient\u3002\n"
125758
+ );
125759
+ process.exit(1);
125760
+ }
125761
+ const templatePath = googleAdsDiagnosisReportTemplatePath();
125762
+ let html;
125763
+ try {
125764
+ html = await fs18.readFile(templatePath, "utf8");
125765
+ } catch {
125766
+ console.error(`
125767
+ \u274C \u672A\u627E\u5230\u62A5\u544A\u6A21\u677F\uFF1A${templatePath}
125768
+ \u8BF7\u5148\u6267\u884C npm run build
125769
+ `);
125770
+ process.exit(1);
125771
+ }
125772
+ let rendered;
125773
+ try {
125774
+ rendered = injectGoogleAdsDiagnosisData(html, data);
125775
+ } catch (err) {
125776
+ const message = err instanceof Error ? err.message : String(err);
125777
+ console.error(`
125778
+ \u274C ${message}
125779
+ `);
125780
+ process.exit(1);
125781
+ }
125782
+ const outPath = path23.resolve(
125783
+ opts.out ?? path23.join(path23.dirname(dataPath), "google-ads-diagnosis-report.html")
125784
+ );
125785
+ await fs18.mkdir(path23.dirname(outPath), { recursive: true });
125786
+ await fs18.writeFile(outPath, rendered, "utf8");
125787
+ console.log(`
125788
+ \u2705 Google \u5E7F\u544A\u8BCA\u65AD HTML \u62A5\u544A\u5DF2\u751F\u6210\uFF1A${outPath}
125789
+ `);
125790
+ if (contentCheck.warnings.length > 0) {
125791
+ console.log(` \u26A0\uFE0F ${contentCheck.warnings.join("\uFF1B")}
125792
+ `);
125793
+ }
125794
+ console.log(
125795
+ "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 HTML \u6587\u4EF6\u5373\u53EF\u67E5\u770B\u5B8C\u6574\u62A5\u544A\uFF08\u6837\u5F0F\u4E0E MarkAI GoogleAdsDiagnosisReport.html \u4E00\u81F4\uFF1B\u56FE\u8868\uFF1AChart.js CDN\uFF09\u3002\n"
125796
+ );
125797
+ }
125798
+
125799
+ // src/commands/google-ads-diagnosis/register.ts
125800
+ function registerGoogleAdsDiagnosisCommands(program2) {
125801
+ const root = program2.command("google-ads-diagnosis").description(
125802
+ "Google \u5E7F\u544A\u8D26\u6237\u8BCA\u65AD\u62A5\u544A\uFF1Acollect \u4EC5\u8F93\u51FA\u4E8B\u5B9E\u6570\u636E\uFF0CAgent \u64B0\u5199 narrative \u540E render HTML"
125803
+ );
125804
+ root.command("collect").description(
125805
+ "\u62C9 google-analysis \u5FEB\u7167\u5E76\u8F93\u51FA google-ads-diagnosis-collect.json\uFF08\u4EC5\u4E8B\u5B9E + agentBrief\uFF0C\u4E0D\u542B\u5EFA\u8BAE\u6587\u6848\uFF09"
125806
+ ).requiredOption("-a, --account <id>", "Google mediaCustomerId").requiredOption("--start <date>", "\u8BCA\u65AD\u5F00\u59CB\u65E5\u671F YYYY-MM-DD").requiredOption("--end <date>", "\u8BCA\u65AD\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("--company-name <name>", "\u8D26\u6237\u540D\u79F0\uFF08\u9ED8\u8BA4\u4ECE list-accounts \u5FEB\u7167\u8BFB\u53D6\uFF09").option("--website <url>", "\u5B98\u7F51 URL\uFF08\u9ED8\u8BA4\u4ECE ads \u5FEB\u7167 finalurl \u8BFB\u53D6\uFF09").option(
125807
+ "--json-out <dir>",
125808
+ "\u843D\u76D8\u76EE\u5F55\uFF08google-analysis \u5FEB\u7167 + google-ads-diagnosis-collect.json\uFF09"
125809
+ ).option("--skip-fetch", "\u4EC5\u6620\u5C04\u76EE\u5F55\u5185\u5DF2\u6709 google-analysis \u5FEB\u7167\uFF0C\u4E0D\u53D1\u8D77 API \u8BF7\u6C42").option("--skip-landing-page", "\u4E0D\u62C9\u53D6 WebsiteDiagnosisReports/performance").option("--no-fetch-previous-period", "\u4E0D\u62C9\u4E0A\u4E00\u5468\u671F campaigns/geographic/keywords \u5BF9\u6BD4\u6570\u636E").option("--token <token>", "JWT").option("--verbose", "\u6253\u5370\u8BF7\u6C42\u8BE6\u60C5").action(
125810
+ async (opts) => {
125811
+ if (!opts.jsonOut?.trim()) {
125812
+ console.error("\n\u274C collect \u987B\u6307\u5B9A --json-out <\u76EE\u5F55>\u3002\n");
125813
+ process.exit(1);
125814
+ }
125815
+ await runGoogleAdsDiagnosisCollect({
125816
+ account: opts.account,
125817
+ start: opts.start,
125818
+ end: opts.end,
125819
+ companyName: opts.companyName,
125820
+ website: opts.website,
125821
+ jsonOut: opts.jsonOut,
125822
+ skipFetch: opts.skipFetch,
125823
+ skipLandingPage: opts.skipLandingPage,
125824
+ fetchPreviousPeriod: opts.fetchPreviousPeriod,
125825
+ token: opts.token,
125826
+ verbose: opts.verbose
125827
+ });
125828
+ }
125829
+ );
125830
+ root.command("render").description(
125831
+ "\u5C06 google-ads-diagnosis.json \u6CE8\u5165 GoogleAdsDiagnosisReport.html\uFF0C\u8F93\u51FA\u4E0E MarkAI \u4E00\u81F4\u7684 HTML \u7EC8\u7A3F"
125832
+ ).requiredOption("--data <file>", "Agent \u4EA7\u51FA\u7684\u8BCA\u65AD JSON\uFF08\u4E0E MarkAI sectionData \u540C\u7ED3\u6784\uFF09").option("--out <file>", "\u8F93\u51FA HTML \u8DEF\u5F84\uFF08\u9ED8\u8BA4\u540C --data \u76EE\u5F55\uFF09").option("--lenient", "\u7ED3\u6784\u8B66\u544A\uFF08\u5982\u7F3A analysis\uFF09\u65F6\u4E0D\u963B\u65AD\u6E32\u67D3", false).action(async (opts) => {
125833
+ await runGoogleAdsDiagnosisRender({
125834
+ dataFile: opts.data,
125835
+ out: opts.out,
125836
+ lenient: opts.lenient
125837
+ });
125838
+ });
125839
+ }
125840
+
124469
125841
  // src/index.ts
124470
125842
  init_google_analysis3();
124471
125843
  init_google_analysis_batch();
@@ -124528,14 +125900,14 @@ var FACEBOOK_SECTION_ALIASES = {
124528
125900
  import { Command } from "commander";
124529
125901
 
124530
125902
  // src/commands/facebook-analysis/render-report.ts
124531
- import fs17 from "fs";
125903
+ import fs22 from "fs";
124532
125904
  import fsPromises2 from "fs/promises";
124533
- import path24 from "path";
124534
- import { fileURLToPath as fileURLToPath5 } from "url";
125905
+ import path29 from "path";
125906
+ import { fileURLToPath as fileURLToPath6 } from "url";
124535
125907
 
124536
125908
  // src/commands/facebook-analysis/merge-snapshot.ts
124537
- import * as fs16 from "fs/promises";
124538
- import * as path23 from "path";
125909
+ import * as fs21 from "fs/promises";
125910
+ import * as path28 from "path";
124539
125911
 
124540
125912
  // src/commands/facebook-analysis/lifecycle-autofill.ts
124541
125913
  var LIFECYCLE_PHASES = ["test-market", "find-winner", "scale"];
@@ -124544,7 +125916,7 @@ var PHASE_LABEL = {
124544
125916
  "find-winner": "\u627E\u8D62\u5BB6",
124545
125917
  scale: "\u653E\u91CF"
124546
125918
  };
124547
- function num(value) {
125919
+ function num2(value) {
124548
125920
  const n = Number(value);
124549
125921
  return Number.isFinite(n) ? n : void 0;
124550
125922
  }
@@ -124555,35 +125927,35 @@ function money(value, currency = "USD") {
124555
125927
  }
124556
125928
  function adSetsWithSpend(tables) {
124557
125929
  const rows = Array.isArray(tables?.adSets) ? tables.adSets : [];
124558
- return rows.filter((r) => (num(r.spend) ?? 0) > 0);
125930
+ return rows.filter((r) => (num2(r.spend) ?? 0) > 0);
124559
125931
  }
124560
125932
  function topAdSetConcentration(adSets) {
124561
125933
  if (!adSets.length) return null;
124562
- const totalSpend = adSets.reduce((s, r) => s + (num(r.spend) ?? 0), 0);
124563
- const totalResults = adSets.reduce((s, r) => s + (num(r.results) ?? 0), 0);
125934
+ const totalSpend = adSets.reduce((s, r) => s + (num2(r.spend) ?? 0), 0);
125935
+ const totalResults = adSets.reduce((s, r) => s + (num2(r.results) ?? 0), 0);
124564
125936
  let best = adSets[0];
124565
- let bestSpend = num(best.spend) ?? 0;
125937
+ let bestSpend = num2(best.spend) ?? 0;
124566
125938
  for (const r of adSets) {
124567
- const sp = num(r.spend) ?? 0;
125939
+ const sp = num2(r.spend) ?? 0;
124568
125940
  if (sp > bestSpend) {
124569
125941
  best = r;
124570
125942
  bestSpend = sp;
124571
125943
  }
124572
125944
  }
124573
- const results = num(best.results) ?? 0;
125945
+ const results = num2(best.results) ?? 0;
124574
125946
  return {
124575
125947
  name: String(best.adGroupName ?? best.name ?? "\u2014"),
124576
125948
  spendShare: totalSpend > 0 ? bestSpend / totalSpend : 0,
124577
125949
  resultsShare: totalResults > 0 ? results / totalResults : 0,
124578
- cpl: num(best.costPerResult)
125950
+ cpl: num2(best.costPerResult)
124579
125951
  };
124580
125952
  }
124581
125953
  function winnerSpendShare(adSets, avgCpl, totalSpend) {
124582
125954
  if (totalSpend <= 0 || avgCpl <= 0) return 0;
124583
125955
  const winnerSpend = adSets.filter((r) => {
124584
- const cpl = num(r.costPerResult);
125956
+ const cpl = num2(r.costPerResult);
124585
125957
  return cpl != null && cpl <= avgCpl * 0.9;
124586
- }).reduce((s, r) => s + (num(r.spend) ?? 0), 0);
125958
+ }).reduce((s, r) => s + (num2(r.spend) ?? 0), 0);
124587
125959
  return winnerSpend / totalSpend;
124588
125960
  }
124589
125961
  function countActiveDimensions(charts, tables) {
@@ -124600,7 +125972,7 @@ function asArray(value) {
124600
125972
  }
124601
125973
  function inferLifecyclePhase(input) {
124602
125974
  const { avgCpl, currency = "USD" } = input;
124603
- const spend = num(input.spend) ?? 0;
125975
+ const spend = num2(input.spend) ?? 0;
124604
125976
  const adSets = adSetsWithSpend(input.tables);
124605
125977
  const activeDims = countActiveDimensions(input.charts, input.tables);
124606
125978
  const winShare = winnerSpendShare(adSets, avgCpl, spend);
@@ -124659,7 +126031,7 @@ function ensureLifecycleFromSnapshot(healthDiagnosis, merged) {
124659
126031
  }
124660
126032
 
124661
126033
  // src/commands/facebook-analysis/scorecard-autofill.ts
124662
- function num2(value) {
126034
+ function num3(value) {
124663
126035
  const n = Number(value);
124664
126036
  return Number.isFinite(n) ? n : void 0;
124665
126037
  }
@@ -124699,8 +126071,8 @@ function buildScorecardFromMergedData(input) {
124699
126071
  if (pLabels.length) {
124700
126072
  const indexed = pLabels.map((label, i) => ({
124701
126073
  label: String(label),
124702
- cpl: num2(pCpl[i]),
124703
- spend: num2(pSpend[i]) ?? 0
126074
+ cpl: num3(pCpl[i]),
126075
+ spend: num3(pSpend[i]) ?? 0
124704
126076
  })).filter((x) => x.spend > 0 || x.cpl != null && x.cpl > 0);
124705
126077
  const sorted = [...indexed].sort((a, b) => (a.cpl ?? Infinity) - (b.cpl ?? Infinity));
124706
126078
  if (sorted[0]) {
@@ -124725,7 +126097,7 @@ function buildScorecardFromMergedData(input) {
124725
126097
  const cLabels = Array.isArray(country?.labels) ? country.labels : [];
124726
126098
  const cCpl = Array.isArray(country?.cpl) ? country.cpl : [];
124727
126099
  if (cLabels.length) {
124728
- const indexed = cLabels.map((label, i) => ({ label: String(label), cpl: num2(cCpl[i]) })).filter((x) => x.cpl != null && x.cpl > 0);
126100
+ const indexed = cLabels.map((label, i) => ({ label: String(label), cpl: num3(cCpl[i]) })).filter((x) => x.cpl != null && x.cpl > 0);
124729
126101
  const sorted = [...indexed].sort((a, b) => (a.cpl ?? 0) - (b.cpl ?? 0));
124730
126102
  if (sorted[0]) {
124731
126103
  const s = cplSignal(sorted[0].cpl, avgCpl);
@@ -124746,24 +126118,24 @@ function buildScorecardFromMergedData(input) {
124746
126118
  }
124747
126119
  }
124748
126120
  const adSets = Array.isArray(tables.adSets) ? tables.adSets : [];
124749
- const adWithSpend = adSets.filter((g) => (num2(g.spend) ?? 0) > 0);
126121
+ const adWithSpend = adSets.filter((g) => (num3(g.spend) ?? 0) > 0);
124750
126122
  if (adWithSpend.length) {
124751
126123
  const sorted = [...adWithSpend].sort(
124752
- (a, b) => (num2(a.costPerResult) ?? Infinity) - (num2(b.costPerResult) ?? Infinity)
126124
+ (a, b) => (num3(a.costPerResult) ?? Infinity) - (num3(b.costPerResult) ?? Infinity)
124753
126125
  );
124754
126126
  const best = sorted[0];
124755
- const sBest = cplSignal(num2(best.costPerResult), avgCpl);
126127
+ const sBest = cplSignal(num3(best.costPerResult), avgCpl);
124756
126128
  pushRow(rows, seen, {
124757
126129
  item: String(best.name ?? "\u5E7F\u544A\u7EC4"),
124758
- data: `CPL ${money2(num2(best.costPerResult), currency)}`,
126130
+ data: `CPL ${money2(num3(best.costPerResult), currency)}`,
124759
126131
  ...sBest
124760
126132
  });
124761
126133
  if (sorted.length > 1) {
124762
126134
  const worst = sorted[sorted.length - 1];
124763
- const sWorst = cplSignal(num2(worst.costPerResult), avgCpl);
126135
+ const sWorst = cplSignal(num3(worst.costPerResult), avgCpl);
124764
126136
  pushRow(rows, seen, {
124765
126137
  item: String(worst.name ?? "\u5E7F\u544A\u7EC4"),
124766
- data: `CPL ${money2(num2(worst.costPerResult), currency)}`,
126138
+ data: `CPL ${money2(num3(worst.costPerResult), currency)}`,
124767
126139
  ...sWorst
124768
126140
  });
124769
126141
  }
@@ -124771,18 +126143,18 @@ function buildScorecardFromMergedData(input) {
124771
126143
  const topAud = Array.isArray(tables.audienceTop) ? tables.audienceTop : [];
124772
126144
  const bottomAud = Array.isArray(tables.audienceBottom) ? tables.audienceBottom : [];
124773
126145
  if (topAud[0]) {
124774
- const s = cplSignal(num2(topAud[0].costPerResult), avgCpl);
126146
+ const s = cplSignal(num3(topAud[0].costPerResult), avgCpl);
124775
126147
  pushRow(rows, seen, {
124776
126148
  item: `\u53D7\u4F17\xB7${String(topAud[0].label ?? "Top")}`,
124777
- data: `CPL ${money2(num2(topAud[0].costPerResult), currency)}`,
126149
+ data: `CPL ${money2(num3(topAud[0].costPerResult), currency)}`,
124778
126150
  ...s
124779
126151
  });
124780
126152
  }
124781
126153
  if (bottomAud[0]) {
124782
- const s = cplSignal(num2(bottomAud[0].costPerResult), avgCpl);
126154
+ const s = cplSignal(num3(bottomAud[0].costPerResult), avgCpl);
124783
126155
  pushRow(rows, seen, {
124784
126156
  item: `\u53D7\u4F17\xB7${String(bottomAud[0].label ?? "Bottom")}`,
124785
- data: `CPL ${money2(num2(bottomAud[0].costPerResult), currency)}`,
126157
+ data: `CPL ${money2(num3(bottomAud[0].costPerResult), currency)}`,
124786
126158
  ...s
124787
126159
  });
124788
126160
  }
@@ -124820,13 +126192,13 @@ function ensureScorecardFromSnapshot(healthDiagnosis, merged) {
124820
126192
  }
124821
126193
 
124822
126194
  // src/commands/facebook-analysis/merge-snapshot.ts
124823
- function asRecord5(value) {
126195
+ function asRecord7(value) {
124824
126196
  if (value && typeof value === "object" && !Array.isArray(value)) {
124825
126197
  return value;
124826
126198
  }
124827
126199
  return null;
124828
126200
  }
124829
- function num3(value) {
126201
+ function num4(value) {
124830
126202
  const n = Number(value);
124831
126203
  return Number.isFinite(n) ? n : void 0;
124832
126204
  }
@@ -124848,7 +126220,7 @@ function statusFromCpl(cpl, avg) {
124848
126220
  return { tag: "yellow", label: "\u89C2\u5BDF" };
124849
126221
  }
124850
126222
  async function readManifest(snapshotDir) {
124851
- const entries = await fs16.readdir(snapshotDir);
126223
+ const entries = await fs21.readdir(snapshotDir);
124852
126224
  const manifestNames = entries.filter(
124853
126225
  (n) => n.startsWith("report-manifest") && n.endsWith(".json")
124854
126226
  );
@@ -124858,7 +126230,7 @@ async function readManifest(snapshotDir) {
124858
126230
  ];
124859
126231
  for (const name2 of ordered) {
124860
126232
  try {
124861
- const raw = await fs16.readFile(path23.join(snapshotDir, name2), "utf8");
126233
+ const raw = await fs21.readFile(path28.join(snapshotDir, name2), "utf8");
124862
126234
  const parsed = JSON.parse(raw);
124863
126235
  if (parsed?.artifacts?.length) return parsed;
124864
126236
  } catch {
@@ -124868,8 +126240,8 @@ async function readManifest(snapshotDir) {
124868
126240
  }
124869
126241
  async function loadSectionFile(snapshotDir, file) {
124870
126242
  try {
124871
- const raw = await fs16.readFile(path23.join(snapshotDir, file), "utf8");
124872
- return asRecord5(JSON.parse(raw));
126243
+ const raw = await fs21.readFile(path28.join(snapshotDir, file), "utf8");
126244
+ return asRecord7(JSON.parse(raw));
124873
126245
  } catch {
124874
126246
  return null;
124875
126247
  }
@@ -124880,8 +126252,8 @@ function aggregatePlatform(networks) {
124880
126252
  const platform = String(row.publisherPlatform ?? row.network ?? "unknown");
124881
126253
  const prev = map.get(platform) ?? { spend: 0, results: 0 };
124882
126254
  map.set(platform, {
124883
- spend: prev.spend + (num3(row.spend) ?? 0),
124884
- results: prev.results + (num3(row.results) ?? 0)
126255
+ spend: prev.spend + (num4(row.spend) ?? 0),
126256
+ results: prev.results + (num4(row.results) ?? 0)
124885
126257
  });
124886
126258
  }
124887
126259
  const labels = [];
@@ -124897,14 +126269,14 @@ function aggregatePlatform(networks) {
124897
126269
  function buildAudienceRows(audiences) {
124898
126270
  return audiences.map((a) => ({
124899
126271
  label: audienceLabel(a.age, a.gender),
124900
- spend: num3(a.spend) ?? 0,
124901
- results: num3(a.results) ?? 0,
124902
- costPerResult: num3(a.costPerResult) ?? (num3(a.results) ? (num3(a.spend) ?? 0) / (num3(a.results) ?? 1) : 0),
124903
- frequency: num3(a.frequency)
126272
+ spend: num4(a.spend) ?? 0,
126273
+ results: num4(a.results) ?? 0,
126274
+ costPerResult: num4(a.costPerResult) ?? (num4(a.results) ? (num4(a.spend) ?? 0) / (num4(a.results) ?? 1) : 0),
126275
+ frequency: num4(a.frequency)
124904
126276
  })).filter((r) => r.spend > 0 || r.results > 0);
124905
126277
  }
124906
126278
  async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
124907
- const absDir = path23.resolve(snapshotDir);
126279
+ const absDir = path28.resolve(snapshotDir);
124908
126280
  const manifest = await readManifest(snapshotDir);
124909
126281
  if (!manifest) {
124910
126282
  throw new Error(
@@ -124917,7 +126289,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
124917
126289
  if (data) sectionMap.set(art.section, data);
124918
126290
  }
124919
126291
  const overview = sectionMap.get("overview");
124920
- const current = asRecord5(overview?.currentPeriod);
126292
+ const current = asRecord7(overview?.currentPeriod);
124921
126293
  const accountName = typeof overview?.accountName === "string" ? overview.accountName : void 0;
124922
126294
  const accountId = manifest.accountId ?? (typeof overview?.accountId === "string" ? overview.accountId : void 0);
124923
126295
  const kpis = { ...payload.kpis ?? {} };
@@ -124939,7 +126311,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
124939
126311
  }
124940
126312
  if (!meta.resultType && current?.resultType) meta.resultType = String(current.resultType);
124941
126313
  const charts = { ...payload.charts ?? {} };
124942
- const avgCpl = num3(kpis.costPerResult) ?? 0;
126314
+ const avgCpl = num4(kpis.costPerResult) ?? 0;
124943
126315
  if (!charts.platform) {
124944
126316
  const platform = sectionMap.get("platform");
124945
126317
  const networks = Array.isArray(platform?.networks) ? platform.networks : [];
@@ -124949,10 +126321,10 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
124949
126321
  const country = sectionMap.get("country");
124950
126322
  const countries = Array.isArray(country?.countries) ? country.countries : [];
124951
126323
  if (countries.length) {
124952
- const sorted = [...countries].sort((a, b) => (num3(b.spend) ?? 0) - (num3(a.spend) ?? 0));
126324
+ const sorted = [...countries].sort((a, b) => (num4(b.spend) ?? 0) - (num4(a.spend) ?? 0));
124953
126325
  charts.country = {
124954
126326
  labels: sorted.map((c) => String(c.countryOrRegion ?? "\u2014")),
124955
- cpl: sorted.map((c) => num3(c.costPerResult) ?? 0)
126327
+ cpl: sorted.map((c) => num4(c.costPerResult) ?? 0)
124956
126328
  };
124957
126329
  }
124958
126330
  }
@@ -124968,7 +126340,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
124968
126340
  };
124969
126341
  }
124970
126342
  }
124971
- if (!charts.funnel && num3(kpis.reach) != null) {
126343
+ if (!charts.funnel && num4(kpis.reach) != null) {
124972
126344
  charts.funnel = { reach: kpis.reach, results: kpis.results ?? 0 };
124973
126345
  }
124974
126346
  const tables = { ...payload.tables ?? {} };
@@ -124977,14 +126349,14 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
124977
126349
  const groups = Array.isArray(adSets?.adGroups) ? adSets.adGroups : [];
124978
126350
  if (groups.length) {
124979
126351
  tables.adSets = groups.map((g) => {
124980
- const cpl = num3(g.costPerResult);
124981
- const freq = num3(g.frequency);
126352
+ const cpl = num4(g.costPerResult);
126353
+ const freq = num4(g.frequency);
124982
126354
  const fatigue = fatigueFromFrequency(freq);
124983
126355
  const status = statusFromCpl(cpl, avgCpl);
124984
126356
  return {
124985
126357
  name: String(g.adGroupName ?? g.campaignName ?? "\u2014"),
124986
- spend: num3(g.spend) ?? 0,
124987
- results: num3(g.results) ?? 0,
126358
+ spend: num4(g.spend) ?? 0,
126359
+ results: num4(g.results) ?? 0,
124988
126360
  costPerResult: cpl ?? 0,
124989
126361
  frequency: freq,
124990
126362
  fatigueLevel: fatigue.level,
@@ -125008,7 +126380,7 @@ async function mergeFacebookSnapshotIntoReport(payload, snapshotDir) {
125008
126380
  }
125009
126381
  const currency = typeof meta.currency === "string" ? meta.currency : "USD";
125010
126382
  const mergeCtx = {
125011
- spend: num3(kpis.spend),
126383
+ spend: num4(kpis.spend),
125012
126384
  avgCpl,
125013
126385
  currency,
125014
126386
  charts,
@@ -125039,7 +126411,7 @@ function isFiniteNumber(value) {
125039
126411
  function asArray2(value) {
125040
126412
  return Array.isArray(value) ? value : [];
125041
126413
  }
125042
- function asRecord6(value) {
126414
+ function asRecord8(value) {
125043
126415
  if (value && typeof value === "object" && !Array.isArray(value)) {
125044
126416
  return value;
125045
126417
  }
@@ -125051,26 +126423,26 @@ function pushMissing(missing, id, chapter, dimension, hint) {
125051
126423
  function countAdSetsWithSpend(tables) {
125052
126424
  const rows = asArray2(tables?.adSets);
125053
126425
  return rows.filter((row) => {
125054
- const r = asRecord6(row);
126426
+ const r = asRecord8(row);
125055
126427
  const spend = r?.spend;
125056
126428
  return isFiniteNumber(spend) && spend > 0;
125057
126429
  }).length;
125058
126430
  }
125059
126431
  function chartLabels(chart) {
125060
- const c = asRecord6(chart);
126432
+ const c = asRecord8(chart);
125061
126433
  return asArray2(c?.labels).length;
125062
126434
  }
125063
126435
  function validateMetaPeriodReportContent(data) {
125064
126436
  const missing = [];
125065
- const meta = asRecord6(data.meta) ?? {};
125066
- const kpis = asRecord6(data.kpis) ?? {};
125067
- const narrative = asRecord6(data.narrative) ?? {};
125068
- const tables = asRecord6(data.tables);
125069
- const charts = asRecord6(data.charts);
125070
- const sections = asRecord6(data.sections);
125071
- const health = asRecord6(data.healthDiagnosis);
125072
- const priorityPlan = asRecord6(data.priorityPlan);
125073
- const actionChecklist = asRecord6(data.actionChecklist);
126437
+ const meta = asRecord8(data.meta) ?? {};
126438
+ const kpis = asRecord8(data.kpis) ?? {};
126439
+ const narrative = asRecord8(data.narrative) ?? {};
126440
+ const tables = asRecord8(data.tables);
126441
+ const charts = asRecord8(data.charts);
126442
+ const sections = asRecord8(data.sections);
126443
+ const health = asRecord8(data.healthDiagnosis);
126444
+ const priorityPlan = asRecord8(data.priorityPlan);
126445
+ const actionChecklist = asRecord8(data.actionChecklist);
125074
126446
  const adSetsWithSpend2 = countAdSetsWithSpend(tables ?? void 0);
125075
126447
  const regionalNarratives = asArray2(narrative.regional).length;
125076
126448
  const recommendations = asArray2(narrative.recommendations);
@@ -125142,7 +126514,7 @@ function validateMetaPeriodReportContent(data) {
125142
126514
  "charts.audience.labels \u81F3\u5C11 1 \u9879"
125143
126515
  );
125144
126516
  }
125145
- const funnel = asRecord6(charts?.funnel);
126517
+ const funnel = asRecord8(charts?.funnel);
125146
126518
  if (!isFiniteNumber(funnel?.reach)) {
125147
126519
  pushMissing(
125148
126520
  missing,
@@ -125206,7 +126578,7 @@ function validateMetaPeriodReportContent(data) {
125206
126578
  );
125207
126579
  } else {
125208
126580
  for (let i = 0; i < regionalNarratives; i++) {
125209
- const row = asRecord6(asArray2(narrative.regional)[i]);
126581
+ const row = asRecord8(asArray2(narrative.regional)[i]);
125210
126582
  if (textLen2(row?.text) < 80) {
125211
126583
  pushMissing(
125212
126584
  missing,
@@ -125236,7 +126608,7 @@ function validateMetaPeriodReportContent(data) {
125236
126608
  "\u5FC5\u987B\u6070\u597D 4 \u6761\uFF08\u7B80\u5316\u8868\u5355/\u533A\u57DF\u8C03\u6574/\u9884\u7B97\u91CD\u6784/\u7D20\u6750\u5EFA\u8BAE\uFF09"
125237
126609
  );
125238
126610
  } else {
125239
- const titles = recommendations.map((r) => asRecord6(r)?.title);
126611
+ const titles = recommendations.map((r) => asRecord8(r)?.title);
125240
126612
  for (const title of RECOMMENDATION_TITLES) {
125241
126613
  if (!titles.includes(title)) {
125242
126614
  pushMissing(
@@ -125249,7 +126621,7 @@ function validateMetaPeriodReportContent(data) {
125249
126621
  }
125250
126622
  }
125251
126623
  for (let i = 0; i < recommendations.length; i++) {
125252
- const rec = asRecord6(recommendations[i]);
126624
+ const rec = asRecord8(recommendations[i]);
125253
126625
  if (textLen2(rec?.content) < 150) {
125254
126626
  pushMissing(
125255
126627
  missing,
@@ -125271,7 +126643,7 @@ function validateMetaPeriodReportContent(data) {
125271
126643
  );
125272
126644
  } else {
125273
126645
  for (let i = 0; i < supplementary.length; i++) {
125274
- const row = asRecord6(supplementary[i]);
126646
+ const row = asRecord8(supplementary[i]);
125275
126647
  if (!textLen2(row?.dimension) || !textLen2(row?.issue)) {
125276
126648
  pushMissing(
125277
126649
  missing,
@@ -125353,7 +126725,7 @@ function validateMetaPeriodReportContent(data) {
125353
126725
  );
125354
126726
  } else {
125355
126727
  for (let i = 0; i < fourQuestions.length; i++) {
125356
- const q = asRecord6(fourQuestions[i]);
126728
+ const q = asRecord8(fourQuestions[i]);
125357
126729
  const evidence = asArray2(q?.evidence).filter((e) => textLen2(e) > 0);
125358
126730
  if (evidence.length < 2) {
125359
126731
  pushMissing(
@@ -125377,7 +126749,7 @@ function validateMetaPeriodReportContent(data) {
125377
126749
  }
125378
126750
  let scorecardValidRows = 0;
125379
126751
  for (let i = 0; i < scorecard.length; i++) {
125380
- const row = asRecord6(scorecard[i]);
126752
+ const row = asRecord8(scorecard[i]);
125381
126753
  const itemOk = textLen2(row?.item) > 0;
125382
126754
  const dataOk = textLen2(row?.data) > 0;
125383
126755
  const signalOk = SCORECARD_SIGNALS.includes(row?.signal);
@@ -125419,7 +126791,7 @@ function validateMetaPeriodReportContent(data) {
125419
126791
  );
125420
126792
  }
125421
126793
  for (const key of ["platform", "country", "adSets"]) {
125422
- const insight = textLen2(asRecord6(sections?.[key])?.insight);
126794
+ const insight = textLen2(asRecord8(sections?.[key])?.insight);
125423
126795
  if (insight < 200) {
125424
126796
  pushMissing(
125425
126797
  missing,
@@ -125430,7 +126802,7 @@ function validateMetaPeriodReportContent(data) {
125430
126802
  );
125431
126803
  }
125432
126804
  }
125433
- const audience = asRecord6(sections?.audience);
126805
+ const audience = asRecord8(sections?.audience);
125434
126806
  if (asArray2(audience?.goldenProfile).filter((s) => textLen2(s) > 0).length < 3) {
125435
126807
  pushMissing(
125436
126808
  missing,
@@ -125458,7 +126830,7 @@ function validateMetaPeriodReportContent(data) {
125458
126830
  "\u2265150 \u5B57"
125459
126831
  );
125460
126832
  }
125461
- const landingRows = asArray2(asRecord6(sections?.landingPage)?.rows);
126833
+ const landingRows = asArray2(asRecord8(sections?.landingPage)?.rows);
125462
126834
  if (landingRows.length < 3) {
125463
126835
  pushMissing(
125464
126836
  missing,
@@ -125517,23 +126889,23 @@ var TEMPLATE_BASENAMES2 = {
125517
126889
  runtime: "meta-period-report.runtime.js"
125518
126890
  };
125519
126891
  var INJECT_MARKER = "window.__META_PERIOD_REPORT__ = window.__META_PERIOD_REPORT__ || null;";
125520
- function resolveSkillTemplatePath2(basename11) {
125521
- const dir = path24.dirname(fileURLToPath5(import.meta.url));
126892
+ function resolveSkillTemplatePath3(basename11) {
126893
+ const dir = path29.dirname(fileURLToPath6(import.meta.url));
125522
126894
  const candidates = [
125523
- path24.join(dir, "skill", "report-templates", basename11),
125524
- path24.join(dir, "skill", "siluzan-ads", "report-templates", basename11),
125525
- path24.join(dir, "..", "..", "assets", "siluzan-ads", "report-templates", basename11)
126895
+ path29.join(dir, "skill", "report-templates", basename11),
126896
+ path29.join(dir, "skill", "siluzan-ads", "report-templates", basename11),
126897
+ path29.join(dir, "..", "..", "assets", "siluzan-ads", "report-templates", basename11)
125526
126898
  ];
125527
126899
  for (const p of candidates) {
125528
- if (fs17.existsSync(p)) return p;
126900
+ if (fs22.existsSync(p)) return p;
125529
126901
  }
125530
126902
  return candidates[0];
125531
126903
  }
125532
126904
  function metaPeriodReportTemplatePath() {
125533
- return resolveSkillTemplatePath2(TEMPLATE_BASENAMES2.html);
126905
+ return resolveSkillTemplatePath3(TEMPLATE_BASENAMES2.html);
125534
126906
  }
125535
126907
  function metaPeriodReportRuntimePath() {
125536
- return resolveSkillTemplatePath2(TEMPLATE_BASENAMES2.runtime);
126908
+ return resolveSkillTemplatePath3(TEMPLATE_BASENAMES2.runtime);
125537
126909
  }
125538
126910
  async function readJsonFile2(filePath) {
125539
126911
  const text = await fsPromises2.readFile(filePath, "utf8");
@@ -125543,7 +126915,7 @@ async function readJsonFile2(filePath) {
125543
126915
  throw new Error(`\u65E0\u6CD5\u89E3\u6790 JSON\uFF1A${filePath}`);
125544
126916
  }
125545
126917
  }
125546
- function asRecord7(value) {
126918
+ function asRecord9(value) {
125547
126919
  if (value && typeof value === "object" && !Array.isArray(value)) {
125548
126920
  return value;
125549
126921
  }
@@ -125557,10 +126929,10 @@ function injectReportData2(html, payload) {
125557
126929
  return html.replace(INJECT_MARKER, `window.__META_PERIOD_REPORT__ = ${json};`);
125558
126930
  }
125559
126931
  async function runFacebookAnalysisRender(opts) {
125560
- const dataPath = path24.resolve(opts.dataFile);
125561
- let data = asRecord7(await readJsonFile2(dataPath));
126932
+ const dataPath = path29.resolve(opts.dataFile);
126933
+ let data = asRecord9(await readJsonFile2(dataPath));
125562
126934
  if (opts.snapshotDir?.trim()) {
125563
- data = await mergeFacebookSnapshotIntoReport(data, path24.resolve(opts.snapshotDir));
126935
+ data = await mergeFacebookSnapshotIntoReport(data, path29.resolve(opts.snapshotDir));
125564
126936
  }
125565
126937
  const contentCheck = validateMetaPeriodReportContent(data);
125566
126938
  if (!contentCheck.ok) {
@@ -125585,14 +126957,14 @@ async function runFacebookAnalysisRender(opts) {
125585
126957
  `);
125586
126958
  process.exit(1);
125587
126959
  }
125588
- const outPath = path24.resolve(
125589
- opts.out ?? path24.join(path24.dirname(dataPath), "meta-period-report.html")
126960
+ const outPath = path29.resolve(
126961
+ opts.out ?? path29.join(path29.dirname(dataPath), "meta-period-report.html")
125590
126962
  );
125591
- const outDir = path24.dirname(outPath);
126963
+ const outDir = path29.dirname(outPath);
125592
126964
  await fsPromises2.mkdir(outDir, { recursive: true });
125593
126965
  await fsPromises2.writeFile(outPath, injectReportData2(html, data), "utf8");
125594
126966
  const runtimeSrc = metaPeriodReportRuntimePath();
125595
- const runtimeOut = path24.join(outDir, TEMPLATE_BASENAMES2.runtime);
126967
+ const runtimeOut = path29.join(outDir, TEMPLATE_BASENAMES2.runtime);
125596
126968
  let runtimeCopied = false;
125597
126969
  try {
125598
126970
  await fsPromises2.copyFile(runtimeSrc, runtimeOut);
@@ -125618,7 +126990,7 @@ async function runFacebookAnalysisRender(opts) {
125618
126990
 
125619
126991
  // src/commands/facebook-analysis/run-batch.ts
125620
126992
  init_auth();
125621
- import * as path25 from "path";
126993
+ import * as path30 from "path";
125622
126994
  import { performance as performance5 } from "perf_hooks";
125623
126995
  init_version();
125624
126996
 
@@ -125720,7 +127092,7 @@ async function runAllFacebookSections(opts) {
125720
127092
  const results = await runWithConcurrency2(tasks, concurrency);
125721
127093
  const succeeded = results.filter((r) => r.ok).length;
125722
127094
  const failed = results.length - succeeded;
125723
- const absoluteSnapshotDir = path25.resolve(opts.jsonOut);
127095
+ const absoluteSnapshotDir = path30.resolve(opts.jsonOut);
125724
127096
  const summary = {
125725
127097
  kind: "siluzan-tso-facebook-analysis-snapshot-batch",
125726
127098
  absoluteSnapshotDir,
@@ -125781,9 +127153,9 @@ init_cli_json_snapshot();
125781
127153
  installProcessHandlers();
125782
127154
  function getVersion() {
125783
127155
  try {
125784
- const __dirname3 = path26.dirname(fileURLToPath6(import.meta.url));
125785
- const pkgPath = path26.join(__dirname3, "..", "package.json");
125786
- const pkg = JSON.parse(fs18.readFileSync(pkgPath, "utf8"));
127156
+ const __dirname3 = path31.dirname(fileURLToPath7(import.meta.url));
127157
+ const pkgPath = path31.join(__dirname3, "..", "package.json");
127158
+ const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf8"));
125787
127159
  return pkg.version ?? "0.0.0";
125788
127160
  } catch {
125789
127161
  return "0.0.0";
@@ -125800,7 +127172,7 @@ configureCliJsonSnapshot({
125800
127172
  });
125801
127173
  var program = new Command2();
125802
127174
  program.name("siluzan-tso").description(
125803
- "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"
127175
+ "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\u3001Google \u5E7F\u544A\u8BCA\u65AD\u62A5\u544A\uFF08google-ads-diagnosis render\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"
125804
127176
  ).version(getVersion());
125805
127177
  program.option("--commit <text>", "\u5199\u64CD\u4F5C\u8BF4\u660E\uFF1B\u8BB0\u5165\u672C\u673A\u5199\u5BA1\u8BA1 JSONL");
125806
127178
  program.hook("preAction", async () => {
@@ -125845,7 +127217,8 @@ var REGISTRARS = [
125845
127217
  register24,
125846
127218
  register25,
125847
127219
  registerWebsiteDiagnosisCommands,
125848
- registerMarketAnalysisCommands
127220
+ registerMarketAnalysisCommands,
127221
+ registerGoogleAdsDiagnosisCommands
125849
127222
  ];
125850
127223
  for (const reg of REGISTRARS) reg(program);
125851
127224
  function argvHasDeprecatedJsonFlag(argv) {