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.
- package/README.md +1 -1
- package/dist/index.js +1610 -237
- package/dist/skill/SKILL.md +6 -4
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/analytics/market-analysis-guide.md +8 -0
- package/dist/skill/references/core/agent-conventions.md +2 -0
- package/dist/skill/references/core/playbooks.md +8 -6
- package/dist/skill/references/report-templates/google-account-diagnosis-report.md +8 -0
- package/dist/skill/references/report-templates/google-ads-diagnosis.md +62 -3
- package/dist/skill/report-templates/GoogleAdsDiagnosisReport.html +6177 -0
- package/dist/skill/report-templates/google-account-diagnosis-report.md +8 -0
- package/dist/skill/report-templates/google-ads-diagnosis.md +62 -3
- package/dist/skill/scripts/install.ps1 +1 -1
- package/dist/skill/scripts/install.sh +1 -1
- package/eval/cases/accounts-entityid-vs-mediaccustomerid.scenario.json +14 -2
- package/eval/cases/accounts-mcc-bind-inquiry.scenario.json +3 -1
- package/eval/cases/accounts-single-balance-not-bulk.scenario.json +14 -3
- package/eval/cases/budget-display-not-raw-micros.scenario.json +8 -1
- package/eval/cases/clue-meta-leads-json.scenario.json +14 -2
- package/eval/cases/clue-tiktok-leads-json.scenario.json +11 -2
- package/eval/cases/destructive-account-delink-needs-confirm.scenario.json +9 -3
- package/eval/cases/destructive-forewarning-delete-needs-confirm.scenario.json +9 -3
- package/eval/cases/destructive-invoice-apply-needs-confirm.scenario.json +9 -3
- package/eval/cases/facebook-analysis-google-section-aliases.scenario.json +11 -2
- package/eval/cases/facebook-analysis-not-google-keywords.scenario.json +10 -3
- package/eval/cases/facebook-analysis-period-default.scenario.json +14 -2
- package/eval/cases/finance-invoice-info-list.scenario.json +11 -3
- package/eval/cases/forewarning-list-google.scenario.json +14 -3
- package/eval/cases/google-ads-no-structural-without-confirm.scenario.json +6 -2
- package/eval/cases/google-analysis-keywords-route.scenario.json +14 -2
- package/eval/cases/human-p1-multiturn.scenario.json +5 -1
- package/eval/cases/meta-single-balance-not-bulk.scenario.json +17 -3
- package/eval/cases/no-legacy-json-flag.scenario.json +6 -2
- package/eval/cases/open-account-google-noninteractive.scenario.json +3 -1
- package/eval/cases/open-account-tiktok-license-file.scenario.json +3 -1
- package/eval/cases/optimize-list-by-account.scenario.json +11 -3
- package/eval/cases/p1-single-account-profile.scenario.json +11 -1
- package/eval/cases/p2-balance-scan-bulk.scenario.json +9 -2
- package/eval/cases/p3-accounts-digest.scenario.json +5 -1
- package/eval/cases/p4-fb-meta-period-report.scenario.json +12 -2
- package/eval/cases/p4-period-report-window.scenario.json +8 -1
- package/eval/cases/pmax-asset-group-create-with-bg.scenario.json +13 -2
- package/eval/cases/pmax-brand-edit-routing.scenario.json +16 -3
- package/eval/cases/pmax-edit-not-campaign-edit.scenario.json +12 -2
- package/eval/cases/pmax-enable-brand-guidelines.scenario.json +12 -2
- package/eval/cases/pmax-no-assets-update-brand-fields.scenario.json +14 -3
- package/eval/cases/rag-before-keyword-expand.scenario.json +11 -1
- package/eval/cases/rag-list-then-query.scenario.json +14 -2
- package/eval/cases/report-list-google.scenario.json +11 -2
- package/eval/cases/report-push-list-google.scenario.json +11 -2
- package/eval/cases/setup-login-or-env.scenario.json +3 -1
- package/eval/cases/setup-siluzan-data-permission-env.scenario.json +3 -1
- package/eval/cases/tiktok-bc-bind-inquiry.scenario.json +6 -2
- package/eval/cases/time-range-user-delegates-default.scenario.json +8 -1
- package/eval/cases/tips-json-out-filtering.scenario.json +3 -1
- package/eval/cases/tips-large-json-pagination.scenario.json +3 -1
- package/eval/cases/uj-ad-campaign-validate-before-create-stub.scenario.json +11 -2
- package/eval/cases/uj-ad-outdoor-campgear-search-plan.scenario.json +3 -1
- package/eval/cases/uj-analytics-30d-pdf-campaign-device-geo.scenario.json +18 -6
- package/eval/cases/uj-analytics-compare-google-tiktok-last-month-roi.scenario.json +8 -1
- package/eval/cases/uj-analytics-ecommerce-industry-report.scenario.json +25 -0
- package/eval/cases/uj-analytics-google-weekly-trends-campaigns-keywords.scenario.json +11 -2
- package/eval/cases/uj-analytics-report-push-weekly-email.scenario.json +3 -1
- package/eval/cases/uj-finance-invoice-records-this-month.scenario.json +11 -2
- package/eval/cases/uj-life-newbie-siluzan-google-end-to-end.scenario.json +4 -1
- package/eval/cases/uj-ops-google-accounts-list-normal.scenario.json +17 -2
- package/eval/cases/uj-ops-google-yesterday-spend-conversions.scenario.json +14 -2
- package/eval/cases/uj-ops-pause-worst-adgroup-confirm.scenario.json +6 -2
- package/eval/cases/uj-ops-tiktok-leads-last-week.scenario.json +17 -3
- package/eval/cases/uj-patrol-cpc-spike-adgroups-over-15.scenario.json +5 -1
- package/eval/cases/uj-patrol-forewarning-create-daily-cap-3000.scenario.json +3 -1
- package/eval/cases/uj-patrol-forewarning-trigger-records.scenario.json +17 -3
- package/eval/cases/uj-patrol-google-balances-low.scenario.json +11 -2
- package/eval/cases/uj-roi-optimize-records-then-execute-cautiously.scenario.json +14 -3
- package/eval/cases/uj-roi-search-terms-add-negative-keywords.scenario.json +14 -2
- 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
|
|
262
|
-
if (
|
|
263
|
-
return
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4028
|
-
|
|
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
|
|
101383
|
-
import * as
|
|
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 =
|
|
101398
|
-
const stateDir =
|
|
101430
|
+
const runDir = path24.resolve(baseDir, runId);
|
|
101431
|
+
const stateDir = path24.join(runDir, "state");
|
|
101399
101432
|
return {
|
|
101400
101433
|
runDir,
|
|
101401
|
-
runManifestFile:
|
|
101402
|
-
progressFile:
|
|
101403
|
-
accountsFile:
|
|
101404
|
-
resultsDir:
|
|
101405
|
-
errorsDir:
|
|
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:
|
|
101408
|
-
lockFile:
|
|
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 =
|
|
101414
|
-
const errors =
|
|
101415
|
-
await
|
|
101416
|
-
await
|
|
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 =
|
|
101432
|
-
await
|
|
101433
|
-
const tmp =
|
|
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
|
|
101436
|
-
await
|
|
101468
|
+
await fs19.writeFile(tmp, content, "utf8");
|
|
101469
|
+
await fs19.rename(tmp, target);
|
|
101437
101470
|
} catch (e) {
|
|
101438
|
-
await
|
|
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
|
|
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
|
|
101475
|
-
await
|
|
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
|
|
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
|
|
101567
|
+
await fs19.mkdir(paths.stateDir, { recursive: true });
|
|
101535
101568
|
try {
|
|
101536
|
-
const handle = await
|
|
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
|
|
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
|
|
101559
|
-
import * as
|
|
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(
|
|
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:
|
|
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 =
|
|
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
|
|
101948
|
-
await
|
|
101949
|
-
await
|
|
101950
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
102884
|
-
import * as
|
|
102885
|
-
import { fileURLToPath as
|
|
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
|
|
103372
|
-
const umiPath = `${TSO_UMI_ROUTE_PREFIX}/${
|
|
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/${
|
|
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,
|
|
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
|
-
`${
|
|
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,
|
|
110692
|
+
function validateKeywordCore(core, path32, errors, lengthViolations) {
|
|
110657
110693
|
const trimmed = core.trim();
|
|
110658
110694
|
if (!trimmed) {
|
|
110659
|
-
errors.push(`${
|
|
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:
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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,
|
|
110774
|
+
function pushKeywordAutoFixWarning(warnings, path32, fieldLabel, trimmedRaw, formatted, declaredUi, beforeUi, matchType, inferredMatchType) {
|
|
110739
110775
|
if (inferredMatchType) {
|
|
110740
110776
|
warnings.push(
|
|
110741
|
-
`${
|
|
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
|
-
`${
|
|
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(`${
|
|
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,
|
|
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(`${
|
|
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
|
-
|
|
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, `${
|
|
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(`${
|
|
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,
|
|
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
|
-
`${
|
|
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
|
-
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
-
|
|
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, `${
|
|
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(`${
|
|
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
|
|
111045
|
+
const path32 = `campaign.AdGroupsForBatchJob[${gi}].KeywordsForBatchJob[${bi}].KeywordText[${ki}]`;
|
|
111010
111046
|
if (plannedNegCores.has(core)) {
|
|
111011
111047
|
warnings.push(
|
|
111012
|
-
`${
|
|
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
|
-
`${
|
|
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
|
|
113441
|
-
if (
|
|
113442
|
-
return
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
115370
|
-
return campaignId ? `${
|
|
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
|
|
115381
|
-
return suffix ? `${
|
|
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(
|
|
118037
|
-
const cfg = loadPmaxJsonFile(
|
|
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
|
|
125903
|
+
import fs22 from "fs";
|
|
124532
125904
|
import fsPromises2 from "fs/promises";
|
|
124533
|
-
import
|
|
124534
|
-
import { fileURLToPath as
|
|
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
|
|
124538
|
-
import * as
|
|
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
|
|
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) => (
|
|
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 + (
|
|
124563
|
-
const totalResults = adSets.reduce((s, r) => s + (
|
|
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 =
|
|
125937
|
+
let bestSpend = num2(best.spend) ?? 0;
|
|
124566
125938
|
for (const r of adSets) {
|
|
124567
|
-
const sp =
|
|
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 =
|
|
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:
|
|
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 =
|
|
125956
|
+
const cpl = num2(r.costPerResult);
|
|
124585
125957
|
return cpl != null && cpl <= avgCpl * 0.9;
|
|
124586
|
-
}).reduce((s, r) => s + (
|
|
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 =
|
|
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
|
|
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:
|
|
124703
|
-
spend:
|
|
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:
|
|
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) => (
|
|
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) => (
|
|
126124
|
+
(a, b) => (num3(a.costPerResult) ?? Infinity) - (num3(b.costPerResult) ?? Infinity)
|
|
124753
126125
|
);
|
|
124754
126126
|
const best = sorted[0];
|
|
124755
|
-
const sBest = cplSignal(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
124872
|
-
return
|
|
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 + (
|
|
124884
|
-
results: prev.results + (
|
|
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:
|
|
124901
|
-
results:
|
|
124902
|
-
costPerResult:
|
|
124903
|
-
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 =
|
|
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 =
|
|
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 =
|
|
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) => (
|
|
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) =>
|
|
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 &&
|
|
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 =
|
|
124981
|
-
const freq =
|
|
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:
|
|
124987
|
-
results:
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
125066
|
-
const kpis =
|
|
125067
|
-
const narrative =
|
|
125068
|
-
const tables =
|
|
125069
|
-
const charts =
|
|
125070
|
-
const sections =
|
|
125071
|
-
const health =
|
|
125072
|
-
const priorityPlan =
|
|
125073
|
-
const 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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
125521
|
-
const dir =
|
|
126892
|
+
function resolveSkillTemplatePath3(basename11) {
|
|
126893
|
+
const dir = path29.dirname(fileURLToPath6(import.meta.url));
|
|
125522
126894
|
const candidates = [
|
|
125523
|
-
|
|
125524
|
-
|
|
125525
|
-
|
|
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 (
|
|
126900
|
+
if (fs22.existsSync(p)) return p;
|
|
125529
126901
|
}
|
|
125530
126902
|
return candidates[0];
|
|
125531
126903
|
}
|
|
125532
126904
|
function metaPeriodReportTemplatePath() {
|
|
125533
|
-
return
|
|
126905
|
+
return resolveSkillTemplatePath3(TEMPLATE_BASENAMES2.html);
|
|
125534
126906
|
}
|
|
125535
126907
|
function metaPeriodReportRuntimePath() {
|
|
125536
|
-
return
|
|
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
|
|
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 =
|
|
125561
|
-
let data =
|
|
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,
|
|
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 =
|
|
125589
|
-
opts.out ??
|
|
126960
|
+
const outPath = path29.resolve(
|
|
126961
|
+
opts.out ?? path29.join(path29.dirname(dataPath), "meta-period-report.html")
|
|
125590
126962
|
);
|
|
125591
|
-
const outDir =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
125785
|
-
const pkgPath =
|
|
125786
|
-
const pkg = JSON.parse(
|
|
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) {
|