siluzan-tso-cli 1.1.45-beta.4 → 1.1.45
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 -2
- package/dist/index.js +195 -144
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/accounts/accounts-list.md +1 -1
- package/dist/skill/references/accounts/accounts-permissions.md +2 -2
- package/dist/skill/references/accounts/finance.md +1 -1
- package/dist/skill/references/accounts/open-account-by-media.md +1 -1
- package/dist/skill/references/accounts/open-account-google-ui.md +1 -1
- package/dist/skill/references/analytics/account-analytics.md +1 -1
- package/dist/skill/references/analytics/rag.md +1 -1
- package/dist/skill/references/analytics/reporting.md +5 -5
- package/dist/skill/references/core/setup.md +5 -5
- package/dist/skill/references/misc/tso-home.md +2 -2
- package/dist/skill/references/report-templates/yandex-period-report-excel.md +3 -2
- package/dist/skill/references/report-templates/yandex-period-report.md +22 -26
- package/dist/skill/report-templates/yandex-period-report-excel.md +3 -2
- package/dist/skill/report-templates/yandex-period-report.html +28 -57
- package/dist/skill/report-templates/yandex-period-report.md +22 -26
- package/dist/skill/scripts/install.ps1 +3 -3
- package/dist/skill/scripts/install.sh +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ HTML 报告模板外部资源:**ECharts** 统一使用 `https://staticpn.siluz
|
|
|
43
43
|
在**用户的目标项目根目录**执行(根据用户使用的助手选择 `--ai`):
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
npm install -g siluzan-tso-cli
|
|
46
|
+
npm install -g siluzan-tso-cli
|
|
47
47
|
siluzan-tso init --ai cursor # 写入 Cursor(默认)
|
|
48
48
|
siluzan-tso init --ai cursor,claude # 同时写入多个平台
|
|
49
49
|
siluzan-tso init --ai all # 写入所有支持的平台
|
|
@@ -51,7 +51,6 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
|
|
|
51
51
|
siluzan-tso init --force # 强制覆盖已存在文件
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
> **注意**:当前为测试版(1.1.45-beta.4),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
55
54
|
|
|
56
55
|
| 助手 | 建议 `--ai` |
|
|
57
56
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -4026,7 +4026,7 @@ var DEFAULT_API_BASE;
|
|
|
4026
4026
|
var init_defaults = __esm({
|
|
4027
4027
|
"src/config/defaults.ts"() {
|
|
4028
4028
|
"use strict";
|
|
4029
|
-
DEFAULT_API_BASE = "https://tso-api
|
|
4029
|
+
DEFAULT_API_BASE = "https://tso-api.siluzan.com";
|
|
4030
4030
|
}
|
|
4031
4031
|
});
|
|
4032
4032
|
|
|
@@ -134013,7 +134013,7 @@ var YANDEX_SECTIONS = [
|
|
|
134013
134013
|
},
|
|
134014
134014
|
{
|
|
134015
134015
|
name: "daily",
|
|
134016
|
-
description: "\u6309\u65E5\u7EE9\u6548\
|
|
134016
|
+
description: "\u6309\u65E5\u7EE9\u6548\uFF1B\u843D\u76D8\u542B weekly[]\uFF08ISO \u5468\uFF09\uFF0C\u7981\u6B62\u81EA\u884C\u805A\u5468",
|
|
134017
134017
|
path: "daily"
|
|
134018
134018
|
},
|
|
134019
134019
|
{
|
|
@@ -134061,7 +134061,7 @@ var YANDEX_SECTION_ALIASES = {
|
|
|
134061
134061
|
|
|
134062
134062
|
// src/commands/yandex-analysis/run-batch.ts
|
|
134063
134063
|
init_auth();
|
|
134064
|
-
import * as
|
|
134064
|
+
import * as path44 from "path";
|
|
134065
134065
|
import { performance as performance8 } from "perf_hooks";
|
|
134066
134066
|
init_version();
|
|
134067
134067
|
|
|
@@ -134193,133 +134193,9 @@ function endpointHintForYandexSection(def) {
|
|
|
134193
134193
|
return `GET \u2026/yandex-analysis/${def.path}?account=&start=&end=`;
|
|
134194
134194
|
}
|
|
134195
134195
|
|
|
134196
|
-
// src/commands/yandex-analysis/resolve-sections.ts
|
|
134197
|
-
function normalizeSectionToken4(raw) {
|
|
134198
|
-
const t = raw.trim();
|
|
134199
|
-
if (!t) return null;
|
|
134200
|
-
if (YANDEX_SECTION_NAMES.includes(t)) {
|
|
134201
|
-
return t;
|
|
134202
|
-
}
|
|
134203
|
-
return YANDEX_SECTION_ALIASES[t] ?? null;
|
|
134204
|
-
}
|
|
134205
|
-
function resolveSectionList5(sections, exclude) {
|
|
134206
|
-
const splitClean = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
134207
|
-
const include = splitClean(sections);
|
|
134208
|
-
const dropRaw = splitClean(exclude);
|
|
134209
|
-
const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken4(n) === null);
|
|
134210
|
-
if (unknown.length > 0) {
|
|
134211
|
-
console.error(
|
|
134212
|
-
`
|
|
134213
|
-
\u274C \u672A\u77E5\u7684 section \u540D\u79F0\uFF1A${unknown.join(", ")}
|
|
134214
|
-
\u53EF\u9009\uFF1A${YANDEX_SECTION_NAMES.join(", ")}
|
|
134215
|
-
\u522B\u540D\uFF1A${Object.keys(YANDEX_SECTION_ALIASES).join(", ")}
|
|
134216
|
-
`
|
|
134217
|
-
);
|
|
134218
|
-
process.exit(1);
|
|
134219
|
-
}
|
|
134220
|
-
const includeCanonical = new Set(include.map((n) => normalizeSectionToken4(n)).filter(Boolean));
|
|
134221
|
-
const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken4(n)).filter(Boolean));
|
|
134222
|
-
const base = include.length > 0 ? YANDEX_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...YANDEX_SECTIONS];
|
|
134223
|
-
return base.filter((s) => !dropCanonical.has(s.name));
|
|
134224
|
-
}
|
|
134225
|
-
|
|
134226
|
-
// src/commands/yandex-analysis/run-batch.ts
|
|
134227
|
-
async function runWithConcurrency6(tasks, concurrency) {
|
|
134228
|
-
const results = new Array(tasks.length);
|
|
134229
|
-
let cursor = 0;
|
|
134230
|
-
const workers = new Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(async () => {
|
|
134231
|
-
while (true) {
|
|
134232
|
-
const i = cursor++;
|
|
134233
|
-
if (i >= tasks.length) return;
|
|
134234
|
-
results[i] = await tasks[i]();
|
|
134235
|
-
}
|
|
134236
|
-
});
|
|
134237
|
-
await Promise.all(workers);
|
|
134238
|
-
return results;
|
|
134239
|
-
}
|
|
134240
|
-
async function runAllYandexSections(opts) {
|
|
134241
|
-
const id = assertYandexAccountId(opts.account);
|
|
134242
|
-
const config = loadConfig(opts.token);
|
|
134243
|
-
if (!opts.jsonOut?.trim()) {
|
|
134244
|
-
console.error("\n\u274C \u5FC5\u987B\u4F20 --json-out <\u76EE\u5F55>\uFF08\u5404\u7EF4\u5EA6\u843D\u76D8\u540E\u7531 report-manifest \u7D22\u5F15\uFF09\u3002\n");
|
|
134245
|
-
process.exit(1);
|
|
134246
|
-
}
|
|
134247
|
-
const targets = resolveSectionList5(opts.sections, opts.exclude);
|
|
134248
|
-
if (targets.length === 0) {
|
|
134249
|
-
console.error("\n\u274C \u6CA1\u6709\u8981\u6267\u884C\u7684 section\uFF08\u8BF7\u68C0\u67E5 --sections / --exclude\uFF09\u3002\n");
|
|
134250
|
-
process.exit(1);
|
|
134251
|
-
}
|
|
134252
|
-
const { startDate, endDate } = resolveYandexDateRange(opts.start, opts.end);
|
|
134253
|
-
const concurrency = Math.max(
|
|
134254
|
-
1,
|
|
134255
|
-
Math.min(typeof opts.concurrency === "number" ? opts.concurrency : 5, 16)
|
|
134256
|
-
);
|
|
134257
|
-
const cliVersion = getCurrentVersion2();
|
|
134258
|
-
const overallT0 = performance8.now();
|
|
134259
|
-
const tasks = targets.map((def) => async () => {
|
|
134260
|
-
const t0 = performance8.now();
|
|
134261
|
-
const hint = endpointHintForYandexSection(def);
|
|
134262
|
-
try {
|
|
134263
|
-
const payload = await fetchYandexSectionPayload(
|
|
134264
|
-
def,
|
|
134265
|
-
{ ...opts, start: startDate, end: endDate },
|
|
134266
|
-
config,
|
|
134267
|
-
id
|
|
134268
|
-
);
|
|
134269
|
-
const summary2 = await writeReportAnalysisSnapshot({
|
|
134270
|
-
snapshotDir: opts.jsonOut,
|
|
134271
|
-
section: def.name,
|
|
134272
|
-
accountId: id,
|
|
134273
|
-
dateRange: { start: startDate, end: endDate },
|
|
134274
|
-
payload,
|
|
134275
|
-
cliVersion
|
|
134276
|
-
});
|
|
134277
|
-
return {
|
|
134278
|
-
section: def.name,
|
|
134279
|
-
ok: true,
|
|
134280
|
-
elapsedMs: performance8.now() - t0,
|
|
134281
|
-
file: summary2.writtenFiles[0],
|
|
134282
|
-
endpointHint: hint
|
|
134283
|
-
};
|
|
134284
|
-
} catch (err) {
|
|
134285
|
-
return {
|
|
134286
|
-
section: def.name,
|
|
134287
|
-
ok: false,
|
|
134288
|
-
elapsedMs: performance8.now() - t0,
|
|
134289
|
-
error: err instanceof Error ? err.message : String(err),
|
|
134290
|
-
endpointHint: hint
|
|
134291
|
-
};
|
|
134292
|
-
}
|
|
134293
|
-
});
|
|
134294
|
-
const results = await runWithConcurrency6(tasks, concurrency);
|
|
134295
|
-
const succeeded = results.filter((r) => r.ok).length;
|
|
134296
|
-
const failed = results.length - succeeded;
|
|
134297
|
-
const absoluteSnapshotDir = path43.resolve(opts.jsonOut);
|
|
134298
|
-
const summary = {
|
|
134299
|
-
kind: "siluzan-tso-yandex-analysis-snapshot-batch",
|
|
134300
|
-
absoluteSnapshotDir,
|
|
134301
|
-
manifestFile: reportManifestFile(id),
|
|
134302
|
-
accountId: id,
|
|
134303
|
-
totalSections: results.length,
|
|
134304
|
-
succeeded,
|
|
134305
|
-
failed,
|
|
134306
|
-
concurrency,
|
|
134307
|
-
elapsedMs: performance8.now() - overallT0,
|
|
134308
|
-
results
|
|
134309
|
-
};
|
|
134310
|
-
console.log(JSON.stringify(summary));
|
|
134311
|
-
if (failed > 0) process.exit(2);
|
|
134312
|
-
}
|
|
134313
|
-
|
|
134314
|
-
// src/commands/yandex-analysis/render-report.ts
|
|
134315
|
-
import fs34 from "fs";
|
|
134316
|
-
import fsPromises7 from "fs/promises";
|
|
134317
|
-
import path45 from "path";
|
|
134318
|
-
import { fileURLToPath as fileURLToPath11 } from "url";
|
|
134319
|
-
|
|
134320
134196
|
// src/commands/yandex-analysis/merge-snapshot.ts
|
|
134321
134197
|
import * as fs33 from "fs/promises";
|
|
134322
|
-
import * as
|
|
134198
|
+
import * as path43 from "path";
|
|
134323
134199
|
function asRecord26(value) {
|
|
134324
134200
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
134325
134201
|
return value;
|
|
@@ -134364,7 +134240,7 @@ function pickMetrics2(row, extras = []) {
|
|
|
134364
134240
|
}
|
|
134365
134241
|
async function loadSectionPayload3(snapshotDir, file) {
|
|
134366
134242
|
try {
|
|
134367
|
-
const raw = await fs33.readFile(
|
|
134243
|
+
const raw = await fs33.readFile(path43.join(snapshotDir, file), "utf8");
|
|
134368
134244
|
return JSON.parse(raw);
|
|
134369
134245
|
} catch {
|
|
134370
134246
|
return null;
|
|
@@ -134384,7 +134260,7 @@ async function readManifest6(snapshotDir, accountId) {
|
|
|
134384
134260
|
}
|
|
134385
134261
|
for (const name2 of candidates) {
|
|
134386
134262
|
try {
|
|
134387
|
-
const raw = await fs33.readFile(
|
|
134263
|
+
const raw = await fs33.readFile(path43.join(snapshotDir, name2), "utf8");
|
|
134388
134264
|
const parsed = JSON.parse(raw);
|
|
134389
134265
|
if (Array.isArray(parsed.artifacts) && parsed.artifacts.length > 0) return parsed;
|
|
134390
134266
|
} catch {
|
|
@@ -134392,8 +134268,12 @@ async function readManifest6(snapshotDir, accountId) {
|
|
|
134392
134268
|
}
|
|
134393
134269
|
return null;
|
|
134394
134270
|
}
|
|
134271
|
+
function normalizeYandexDailyDate(value) {
|
|
134272
|
+
const m = /(\d{4}-\d{2}-\d{2})/.exec(String(value ?? "").trim());
|
|
134273
|
+
return m?.[1] ?? "";
|
|
134274
|
+
}
|
|
134395
134275
|
function parseYmd2(value) {
|
|
134396
|
-
const s =
|
|
134276
|
+
const s = normalizeYandexDailyDate(value);
|
|
134397
134277
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
134398
134278
|
if (!m) return null;
|
|
134399
134279
|
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
@@ -134410,26 +134290,28 @@ function isoWeekKey(d) {
|
|
|
134410
134290
|
const dow = (day.getDay() + 6) % 7;
|
|
134411
134291
|
const weekStart = new Date(day);
|
|
134412
134292
|
weekStart.setDate(weekStart.getDate() - dow);
|
|
134293
|
+
const weekEnd = new Date(weekStart);
|
|
134294
|
+
weekEnd.setDate(weekEnd.getDate() + 6);
|
|
134413
134295
|
const thursday = new Date(weekStart);
|
|
134414
134296
|
thursday.setDate(thursday.getDate() + 3);
|
|
134415
134297
|
const yearStart = new Date(thursday.getFullYear(), 0, 1);
|
|
134416
134298
|
const week = Math.floor((thursday.getTime() - yearStart.getTime()) / 864e5 / 7) + 1;
|
|
134417
134299
|
const label = `W${String(week).padStart(2, "0")}`;
|
|
134418
|
-
return { key: `${thursday.getFullYear()}-${label}`, label, weekStart };
|
|
134300
|
+
return { key: `${thursday.getFullYear()}-${label}`, label, weekStart, weekEnd };
|
|
134419
134301
|
}
|
|
134420
134302
|
function aggregateDailyToWeekly(dailyRows) {
|
|
134421
134303
|
const map = /* @__PURE__ */ new Map();
|
|
134422
134304
|
for (const row of dailyRows) {
|
|
134423
134305
|
const d = parseYmd2(row.date);
|
|
134424
134306
|
if (!d) continue;
|
|
134425
|
-
const { key, label, weekStart } = isoWeekKey(d);
|
|
134307
|
+
const { key, label, weekStart, weekEnd } = isoWeekKey(d);
|
|
134426
134308
|
let acc = map.get(key);
|
|
134427
134309
|
if (!acc) {
|
|
134428
134310
|
acc = {
|
|
134429
134311
|
label,
|
|
134430
134312
|
weekStart,
|
|
134431
|
-
|
|
134432
|
-
|
|
134313
|
+
weekEnd,
|
|
134314
|
+
days: 0,
|
|
134433
134315
|
spend: 0,
|
|
134434
134316
|
impressions: 0,
|
|
134435
134317
|
clicks: 0,
|
|
@@ -134437,20 +134319,21 @@ function aggregateDailyToWeekly(dailyRows) {
|
|
|
134437
134319
|
};
|
|
134438
134320
|
map.set(key, acc);
|
|
134439
134321
|
}
|
|
134440
|
-
|
|
134441
|
-
if (d > acc.maxDate) acc.maxDate = d;
|
|
134322
|
+
acc.days += 1;
|
|
134442
134323
|
acc.spend += num10(row.spend) ?? 0;
|
|
134443
134324
|
acc.impressions += num10(row.impressions) ?? 0;
|
|
134444
134325
|
acc.clicks += num10(row.clicks) ?? 0;
|
|
134445
134326
|
acc.conversions += num10(row.conversions) ?? 0;
|
|
134446
134327
|
}
|
|
134447
134328
|
return [...map.values()].sort((a, b) => a.weekStart.getTime() - b.weekStart.getTime()).map((acc) => {
|
|
134448
|
-
const
|
|
134329
|
+
const startDate = formatYmd2(acc.weekStart);
|
|
134330
|
+
const endDate = formatYmd2(acc.weekEnd);
|
|
134449
134331
|
return {
|
|
134450
134332
|
weekLabel: acc.label,
|
|
134451
|
-
|
|
134452
|
-
|
|
134453
|
-
|
|
134333
|
+
isoWeek: acc.label,
|
|
134334
|
+
startDate,
|
|
134335
|
+
endDate,
|
|
134336
|
+
days: acc.days,
|
|
134454
134337
|
spend: round25(acc.spend),
|
|
134455
134338
|
impressions: Math.round(acc.impressions),
|
|
134456
134339
|
clicks: Math.round(acc.clicks),
|
|
@@ -134461,10 +134344,25 @@ function aggregateDailyToWeekly(dailyRows) {
|
|
|
134461
134344
|
};
|
|
134462
134345
|
});
|
|
134463
134346
|
}
|
|
134347
|
+
function prepareYandexDailyPayload(payload) {
|
|
134348
|
+
const obj = asRecord26(payload) ?? {};
|
|
134349
|
+
const items2 = asItems(obj).map((r) => {
|
|
134350
|
+
const date = normalizeYandexDailyDate(r.date);
|
|
134351
|
+
return date ? { ...r, date } : r;
|
|
134352
|
+
});
|
|
134353
|
+
return { ...obj, items: items2, weekly: aggregateDailyToWeekly(items2) };
|
|
134354
|
+
}
|
|
134355
|
+
var YANDEX_DAILY_OUTLINE_HINTS = [
|
|
134356
|
+
"// `items[].date` \u5DF2\u89C4\u8303\u4E3A YYYY-MM-DD\u3002\u63A5\u53E3\u53EF\u80FD\u8FD4\u56DE ISO \u65F6\u95F4\u6233\uFF08\u5982 2026-07-01T00:00:00+00:00\uFF09\uFF0CCLI \u5DF2\u622A\u65AD\uFF1B\u7981\u6B62\u518D\u62FC\u63A5 T00:00:00Z\uFF0C\u5426\u5219 new Date() \u4F1A Invalid Date\uFF0C\u5468\u6807\u7B7E\u53D8\u6210 NaN\u3002",
|
|
134357
|
+
"// \u5468\u8D8B\u52BF\u8BFB\u672C\u6587\u4EF6 `weekly[]`\uFF08ISO \u5468\u4E00~\u5468\u65E5\uFF1B`days` \u4E3A\u6709\u6570\u636E\u7684\u5929\u6570\uFF09\u3002\u7981\u6B62\u81EA\u884C\u6309 date \u805A\u5468\u3002"
|
|
134358
|
+
];
|
|
134359
|
+
function buildYandexOutlineHints(section) {
|
|
134360
|
+
if (section === "daily") return YANDEX_DAILY_OUTLINE_HINTS;
|
|
134361
|
+
return void 0;
|
|
134362
|
+
}
|
|
134464
134363
|
function mapDailyRows(payload) {
|
|
134465
134364
|
return asItems(payload).map((r) => {
|
|
134466
|
-
const
|
|
134467
|
-
const date = typeof dateRaw === "string" ? dateRaw.slice(0, 10) : dateRaw != null ? String(dateRaw).slice(0, 10) : "";
|
|
134365
|
+
const date = normalizeYandexDailyDate(r.date);
|
|
134468
134366
|
return pickMetrics2({ ...r, date }, ["date"]);
|
|
134469
134367
|
});
|
|
134470
134368
|
}
|
|
@@ -134518,7 +134416,7 @@ function mapDeviceRows2(payload) {
|
|
|
134518
134416
|
return asItems(payload).map((r) => pickMetrics2(r, ["device", "network"]));
|
|
134519
134417
|
}
|
|
134520
134418
|
async function mergeYandexAnalysisSnapshotIntoReport(payload, snapshotDir) {
|
|
134521
|
-
const absDir =
|
|
134419
|
+
const absDir = path43.resolve(snapshotDir);
|
|
134522
134420
|
const metaAccountId = typeof asRecord26(payload.meta)?.accountId === "string" ? asRecord26(payload.meta)?.accountId : void 0;
|
|
134523
134421
|
const manifest = await readManifest6(snapshotDir, metaAccountId);
|
|
134524
134422
|
if (!manifest) {
|
|
@@ -134595,6 +134493,132 @@ async function mergeYandexAnalysisSnapshotIntoReport(payload, snapshotDir) {
|
|
|
134595
134493
|
return { ...payload, meta, kpis, tables };
|
|
134596
134494
|
}
|
|
134597
134495
|
|
|
134496
|
+
// src/commands/yandex-analysis/resolve-sections.ts
|
|
134497
|
+
function normalizeSectionToken4(raw) {
|
|
134498
|
+
const t = raw.trim();
|
|
134499
|
+
if (!t) return null;
|
|
134500
|
+
if (YANDEX_SECTION_NAMES.includes(t)) {
|
|
134501
|
+
return t;
|
|
134502
|
+
}
|
|
134503
|
+
return YANDEX_SECTION_ALIASES[t] ?? null;
|
|
134504
|
+
}
|
|
134505
|
+
function resolveSectionList5(sections, exclude) {
|
|
134506
|
+
const splitClean = (s) => (s ?? "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
134507
|
+
const include = splitClean(sections);
|
|
134508
|
+
const dropRaw = splitClean(exclude);
|
|
134509
|
+
const unknown = [...include, ...dropRaw].filter((n) => normalizeSectionToken4(n) === null);
|
|
134510
|
+
if (unknown.length > 0) {
|
|
134511
|
+
console.error(
|
|
134512
|
+
`
|
|
134513
|
+
\u274C \u672A\u77E5\u7684 section \u540D\u79F0\uFF1A${unknown.join(", ")}
|
|
134514
|
+
\u53EF\u9009\uFF1A${YANDEX_SECTION_NAMES.join(", ")}
|
|
134515
|
+
\u522B\u540D\uFF1A${Object.keys(YANDEX_SECTION_ALIASES).join(", ")}
|
|
134516
|
+
`
|
|
134517
|
+
);
|
|
134518
|
+
process.exit(1);
|
|
134519
|
+
}
|
|
134520
|
+
const includeCanonical = new Set(include.map((n) => normalizeSectionToken4(n)).filter(Boolean));
|
|
134521
|
+
const dropCanonical = new Set(dropRaw.map((n) => normalizeSectionToken4(n)).filter(Boolean));
|
|
134522
|
+
const base = include.length > 0 ? YANDEX_SECTIONS.filter((s) => includeCanonical.has(s.name)) : [...YANDEX_SECTIONS];
|
|
134523
|
+
return base.filter((s) => !dropCanonical.has(s.name));
|
|
134524
|
+
}
|
|
134525
|
+
|
|
134526
|
+
// src/commands/yandex-analysis/run-batch.ts
|
|
134527
|
+
async function runWithConcurrency6(tasks, concurrency) {
|
|
134528
|
+
const results = new Array(tasks.length);
|
|
134529
|
+
let cursor = 0;
|
|
134530
|
+
const workers = new Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(async () => {
|
|
134531
|
+
while (true) {
|
|
134532
|
+
const i = cursor++;
|
|
134533
|
+
if (i >= tasks.length) return;
|
|
134534
|
+
results[i] = await tasks[i]();
|
|
134535
|
+
}
|
|
134536
|
+
});
|
|
134537
|
+
await Promise.all(workers);
|
|
134538
|
+
return results;
|
|
134539
|
+
}
|
|
134540
|
+
async function runAllYandexSections(opts) {
|
|
134541
|
+
const id = assertYandexAccountId(opts.account);
|
|
134542
|
+
const config = loadConfig(opts.token);
|
|
134543
|
+
if (!opts.jsonOut?.trim()) {
|
|
134544
|
+
console.error("\n\u274C \u5FC5\u987B\u4F20 --json-out <\u76EE\u5F55>\uFF08\u5404\u7EF4\u5EA6\u843D\u76D8\u540E\u7531 report-manifest \u7D22\u5F15\uFF09\u3002\n");
|
|
134545
|
+
process.exit(1);
|
|
134546
|
+
}
|
|
134547
|
+
const targets = resolveSectionList5(opts.sections, opts.exclude);
|
|
134548
|
+
if (targets.length === 0) {
|
|
134549
|
+
console.error("\n\u274C \u6CA1\u6709\u8981\u6267\u884C\u7684 section\uFF08\u8BF7\u68C0\u67E5 --sections / --exclude\uFF09\u3002\n");
|
|
134550
|
+
process.exit(1);
|
|
134551
|
+
}
|
|
134552
|
+
const { startDate, endDate } = resolveYandexDateRange(opts.start, opts.end);
|
|
134553
|
+
const concurrency = Math.max(
|
|
134554
|
+
1,
|
|
134555
|
+
Math.min(typeof opts.concurrency === "number" ? opts.concurrency : 5, 16)
|
|
134556
|
+
);
|
|
134557
|
+
const cliVersion = getCurrentVersion2();
|
|
134558
|
+
const overallT0 = performance8.now();
|
|
134559
|
+
const tasks = targets.map((def) => async () => {
|
|
134560
|
+
const t0 = performance8.now();
|
|
134561
|
+
const hint = endpointHintForYandexSection(def);
|
|
134562
|
+
try {
|
|
134563
|
+
const raw = await fetchYandexSectionPayload(
|
|
134564
|
+
def,
|
|
134565
|
+
{ ...opts, start: startDate, end: endDate },
|
|
134566
|
+
config,
|
|
134567
|
+
id
|
|
134568
|
+
);
|
|
134569
|
+
const payload = def.name === "daily" ? prepareYandexDailyPayload(raw) : raw;
|
|
134570
|
+
const summary2 = await writeReportAnalysisSnapshot({
|
|
134571
|
+
snapshotDir: opts.jsonOut,
|
|
134572
|
+
section: def.name,
|
|
134573
|
+
accountId: id,
|
|
134574
|
+
dateRange: { start: startDate, end: endDate },
|
|
134575
|
+
payload,
|
|
134576
|
+
cliVersion,
|
|
134577
|
+
outlineExtraHints: buildYandexOutlineHints(def.name)
|
|
134578
|
+
});
|
|
134579
|
+
return {
|
|
134580
|
+
section: def.name,
|
|
134581
|
+
ok: true,
|
|
134582
|
+
elapsedMs: performance8.now() - t0,
|
|
134583
|
+
file: summary2.writtenFiles[0],
|
|
134584
|
+
endpointHint: hint
|
|
134585
|
+
};
|
|
134586
|
+
} catch (err) {
|
|
134587
|
+
return {
|
|
134588
|
+
section: def.name,
|
|
134589
|
+
ok: false,
|
|
134590
|
+
elapsedMs: performance8.now() - t0,
|
|
134591
|
+
error: err instanceof Error ? err.message : String(err),
|
|
134592
|
+
endpointHint: hint
|
|
134593
|
+
};
|
|
134594
|
+
}
|
|
134595
|
+
});
|
|
134596
|
+
const results = await runWithConcurrency6(tasks, concurrency);
|
|
134597
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
134598
|
+
const failed = results.length - succeeded;
|
|
134599
|
+
const absoluteSnapshotDir = path44.resolve(opts.jsonOut);
|
|
134600
|
+
const summary = {
|
|
134601
|
+
kind: "siluzan-tso-yandex-analysis-snapshot-batch",
|
|
134602
|
+
absoluteSnapshotDir,
|
|
134603
|
+
manifestFile: reportManifestFile(id),
|
|
134604
|
+
accountId: id,
|
|
134605
|
+
totalSections: results.length,
|
|
134606
|
+
succeeded,
|
|
134607
|
+
failed,
|
|
134608
|
+
concurrency,
|
|
134609
|
+
elapsedMs: performance8.now() - overallT0,
|
|
134610
|
+
results
|
|
134611
|
+
};
|
|
134612
|
+
console.log(JSON.stringify(summary));
|
|
134613
|
+
if (failed > 0) process.exit(2);
|
|
134614
|
+
}
|
|
134615
|
+
|
|
134616
|
+
// src/commands/yandex-analysis/render-report.ts
|
|
134617
|
+
import fs34 from "fs";
|
|
134618
|
+
import fsPromises7 from "fs/promises";
|
|
134619
|
+
import path45 from "path";
|
|
134620
|
+
import { fileURLToPath as fileURLToPath11 } from "url";
|
|
134621
|
+
|
|
134598
134622
|
// src/commands/yandex-analysis/report-content.ts
|
|
134599
134623
|
var SECTION_KEYS4 = [
|
|
134600
134624
|
"overview",
|
|
@@ -134735,6 +134759,19 @@ function validateYandexPeriodReportContent(data) {
|
|
|
134735
134759
|
);
|
|
134736
134760
|
}
|
|
134737
134761
|
const tables = asRecord27(data.tables) ?? {};
|
|
134762
|
+
const weeklyRows = asArray7(tables.weekly).map((r) => asRecord27(r)).filter((r) => r != null);
|
|
134763
|
+
for (const [i, row] of weeklyRows.entries()) {
|
|
134764
|
+
const label = `${row.weekLabel ?? ""} ${row.startDate ?? ""} ${row.endDate ?? ""}`;
|
|
134765
|
+
if (/nan/i.test(label) || !normalizeYandexDailyDate(row.startDate) || !normalizeYandexDailyDate(row.endDate)) {
|
|
134766
|
+
pushMissing6(
|
|
134767
|
+
missing,
|
|
134768
|
+
`tables-weekly-${i}`,
|
|
134769
|
+
"\u2467\u65E5\u671F/\u5468\u62A5",
|
|
134770
|
+
`tables.weekly[${i}]`,
|
|
134771
|
+
"\u5468\u6807\u7B7E/\u8D77\u6B62\u65E5\u671F\u65E0\u6548\uFF08\u542B NaN \u6216\u975E YYYY-MM-DD\uFF09\u3002\u8BF7\u4F20 --snapshot-dir\uFF0C\u7531 CLI \u4ECE daily \u805A\u5408 weekly\uFF0C\u7981\u6B62\u81EA\u884C\u6309 date \u62FC T00:00:00Z \u805A\u5468\u3002"
|
|
134772
|
+
);
|
|
134773
|
+
}
|
|
134774
|
+
}
|
|
134738
134775
|
const rowInsights = asRecord27(narrative.rowInsights) ?? {};
|
|
134739
134776
|
const rowInsightCoverage = {};
|
|
134740
134777
|
for (const spec of ROW_INSIGHT_SPECS) {
|
|
@@ -134822,6 +134859,19 @@ function asRecord28(value) {
|
|
|
134822
134859
|
}
|
|
134823
134860
|
throw new Error("\u62A5\u544A\u6570\u636E\u987B\u4E3A JSON \u5BF9\u8C61");
|
|
134824
134861
|
}
|
|
134862
|
+
function refreshWeeklyFromDaily(data) {
|
|
134863
|
+
const tables = data.tables;
|
|
134864
|
+
if (!tables || typeof tables !== "object" || Array.isArray(tables)) return data;
|
|
134865
|
+
const daily = Array.isArray(tables.daily) ? tables.daily : [];
|
|
134866
|
+
if (daily.length === 0) return data;
|
|
134867
|
+
return {
|
|
134868
|
+
...data,
|
|
134869
|
+
tables: {
|
|
134870
|
+
...tables,
|
|
134871
|
+
weekly: aggregateDailyToWeekly(daily)
|
|
134872
|
+
}
|
|
134873
|
+
};
|
|
134874
|
+
}
|
|
134825
134875
|
function injectReportData7(html, payload) {
|
|
134826
134876
|
const json = JSON.stringify(payload).replace(/</g, "\\u003c");
|
|
134827
134877
|
if (!html.includes(INJECT_MARKER6)) {
|
|
@@ -134835,6 +134885,7 @@ async function runYandexAnalysisRender(opts) {
|
|
|
134835
134885
|
if (opts.snapshotDir?.trim()) {
|
|
134836
134886
|
data = await mergeYandexAnalysisSnapshotIntoReport(data, path45.resolve(opts.snapshotDir));
|
|
134837
134887
|
}
|
|
134888
|
+
data = refreshWeeklyFromDaily(data);
|
|
134838
134889
|
const contentCheck = validateYandexPeriodReportContent(data);
|
|
134839
134890
|
if (!contentCheck.ok) {
|
|
134840
134891
|
console.error(`
|
package/dist/skill/_meta.json
CHANGED
|
@@ -167,7 +167,7 @@ siluzan-tso account-history --start 2026-03-01 --end 2026-03-31 --json-out ./sna
|
|
|
167
167
|
| 状态 | 含义 | 下一步操作 |
|
|
168
168
|
| ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
169
169
|
| `Pending` | 审核中 | 等待,可反复运行此命令轮询;审核周期因媒体而异 |
|
|
170
|
-
| `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `https://www
|
|
170
|
+
| `Approved` | 审核通过 | 运行 `list-accounts -m <媒体>` 确认账户已出现;引导用户充值激活(`config show` 取 `webUrl`,按 `finance.md` 打开对应媒体充值页;例如 Google 为 `https://www.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google`;Kwai、Yandex 当前没有对应充值界面) |
|
|
171
171
|
| `Rejected` | 被拒 | 查看 `--json-out` 落盘中的 `reason` 字段了解拒绝原因;修改资料后重新提交;若原因不明,引导用户联系丝路赞客服 |
|
|
172
172
|
|
|
173
173
|
---
|
|
@@ -488,7 +488,7 @@ siluzan-tso account email-deauth -c <mediaCustomerId> --invitation-id <id> --res
|
|
|
488
488
|
|
|
489
489
|
| 功能 | 媒体 | 网页路径 |
|
|
490
490
|
| --------------------------------------- | ------ | ------------------------------------------------- |
|
|
491
|
-
| **账户激活**(邀请他人激活 / 充值激活) | Google | `https://www
|
|
491
|
+
| **账户激活**(邀请他人激活 / 充值激活) | Google | `https://www.siluzan.com/v3/foreign_trade/tso/manageAccounts` |
|
|
492
492
|
|
|
493
493
|
**Agent 建议话术**:
|
|
494
494
|
|
|
@@ -497,5 +497,5 @@ siluzan-tso account email-deauth -c <mediaCustomerId> --invitation-id <id> --res
|
|
|
497
497
|
siluzan-tso config show # 查看 webUrl 字段
|
|
498
498
|
|
|
499
499
|
# 账户激活(Google)→ 引导至账户管理页
|
|
500
|
-
# https://www
|
|
500
|
+
# https://www.siluzan.com/v3/foreign_trade/tso/manageAccounts
|
|
501
501
|
```
|
|
@@ -126,7 +126,7 @@ siluzan-tso config show
|
|
|
126
126
|
|
|
127
127
|
### 充值页链接(按媒体 × 类型)
|
|
128
128
|
|
|
129
|
-
链接模式:`https://www
|
|
129
|
+
链接模式:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/<page>?mediaType=<mediaType>`;丝路赞钱包:`https://www.siluzan.com/v3/foreign_trade/tso/recharge/siluzanWallet`(无媒体参数)。
|
|
130
130
|
|
|
131
131
|
| 充值类型 | `<page>` | 支持媒体(`mediaType` 参数) |
|
|
132
132
|
| ------------------------- | --------------------- | ------------------------------------------- |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 各媒体开户
|
|
2
2
|
|
|
3
|
-
> 网页链接:`https://www
|
|
3
|
+
> 网页链接:`https://www.siluzan.com/v3/foreign_trade/tso/accountOpeningHistory?tso=%2Fv3umijs%2Ftso%2FaccountOpeningHistory`
|
|
4
4
|
> 多命令串联见 `references/core/workflows.md` § 流程一。
|
|
5
5
|
|
|
6
6
|
## Contents
|
|
@@ -65,7 +65,7 @@ siluzan-tso open-account google-wizard
|
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
67
|
siluzan-tso account-history -m Google
|
|
68
|
-
# 审核通过后:config show → https://www
|
|
68
|
+
# 审核通过后:config show → https://www.siluzan.com/v3/foreign_trade/tso/recharge/pay?mediaType=Google
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
---
|
|
@@ -405,7 +405,7 @@ siluzan-tso yandex-analysis render \
|
|
|
405
405
|
--out ./yandex-period-report.html
|
|
406
406
|
```
|
|
407
407
|
|
|
408
|
-
`search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。无 Metrica / 写操作。报告模板:`report-templates/yandex-period-report.md`(8
|
|
408
|
+
`search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。无 Metrica / 写操作。报告模板:`report-templates/yandex-period-report.md`(8 个分析章节必写总结+建议;周趋势读落盘 `daily.weekly[]`,**禁止**自行按 `date` 聚周)。
|
|
409
409
|
|
|
410
410
|
---
|
|
411
411
|
|
|
@@ -190,9 +190,9 @@ siluzan-tso report push receive-emails -m Google [--json-out ./snap]
|
|
|
190
190
|
|
|
191
191
|
| 媒体 | 报告类型 | URL 模板 |
|
|
192
192
|
| ------ | ---------------- | ----------------------------------------------------------- |
|
|
193
|
-
| Google | 日报(Daily) | `https://www
|
|
194
|
-
| Google | 小时报(Hourly) | `https://www
|
|
195
|
-
| TikTok | 日报 | `https://www
|
|
193
|
+
| Google | 日报(Daily) | `https://www.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
|
|
194
|
+
| Google | 小时报(Hourly) | `https://www.siluzan.com/media-report/hour/{entityId}?culture=zh-CN` |
|
|
195
|
+
| TikTok | 日报 | `https://www.siluzan.com/media-report/publish/{entityId}?culture=zh-CN` |
|
|
196
196
|
|
|
197
197
|
`entityId` 来自 `siluzan-tso report list --json-out ./snap` 中每条记录的 `entityId` 字段。
|
|
198
198
|
|
|
@@ -205,8 +205,8 @@ siluzan-tso report list -m Google --json-out ./snap
|
|
|
205
205
|
|
|
206
206
|
# 第二步:查看 webUrl
|
|
207
207
|
siluzan-tso config show
|
|
208
|
-
# webUrl: https://www
|
|
208
|
+
# webUrl: https://www.siluzan.com
|
|
209
209
|
|
|
210
210
|
# 第三步:拼接链接(Google 日报)
|
|
211
|
-
# https://www
|
|
211
|
+
# https://www.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
|
|
212
212
|
```
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
## 安装 CLI
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g siluzan-tso-cli
|
|
13
|
+
npm install -g siluzan-tso-cli
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
---
|
|
@@ -64,7 +64,7 @@ siluzan-tso config set --api-key <Key> # 或 config 直接写入
|
|
|
64
64
|
siluzan-tso config set --token <Token> # 备用:设置 JWT Token
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
API Key 获取入口:`https://www
|
|
67
|
+
API Key 获取入口:`https://www.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
# 第 1 步:让用户报出手机号后,立刻发码(命令立即返回,不会等待输入)
|
|
@@ -127,9 +127,9 @@ siluzan-tso config show
|
|
|
127
127
|
|
|
128
128
|
```
|
|
129
129
|
构建环境 : production
|
|
130
|
-
apiBaseUrl : https://tso-api
|
|
131
|
-
googleApiUrl : https://googleapi
|
|
132
|
-
webUrl : https://www
|
|
130
|
+
apiBaseUrl : https://tso-api.siluzan.com
|
|
131
|
+
googleApiUrl : https://googleapi.mysiluzan.com
|
|
132
|
+
webUrl : https://www.siluzan.com
|
|
133
133
|
apiKey : abcd****1234
|
|
134
134
|
```
|
|
135
135
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
siluzan-tso config show # 取 webUrl
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
首页地址:`https://www
|
|
9
|
+
首页地址:`https://www.siluzan.com/v3/foreign_trade/tso/home`
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -25,7 +25,7 @@ siluzan-tso config show # 取 webUrl
|
|
|
25
25
|
|
|
26
26
|
## 推荐话术
|
|
27
27
|
|
|
28
|
-
1. **「和首页一样的总览」** → 打开 `https://www
|
|
28
|
+
1. **「和首页一样的总览」** → 打开 `https://www.siluzan.com/v3/foreign_trade/tso/home`。
|
|
29
29
|
2. **「某个 Google 账户昨天花了多少」** → `list-accounts -m Google` + `stats -m Google -a <id>`。
|
|
30
30
|
3. **「有待充值账户」** → 说明聚合数据在首页;CLI 可 `list-accounts` + `balance` 逐户排查,或引导充值页。
|
|
31
31
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
| --------------- | ------------------------- | ---------------------------- |
|
|
36
36
|
| 账户总览 | `overview` | 对象:`totals` / `balance` … |
|
|
37
37
|
| 日报表 | `daily` | `items[]` |
|
|
38
|
-
| 周报表 |
|
|
38
|
+
| 周报表 | `daily` 的 `weekly[]` | **读落盘 weekly,禁止自行聚周** |
|
|
39
39
|
| Search 网络 | `search` | `items[]` |
|
|
40
40
|
| 广告系列 | `campaigns` | `items[]` |
|
|
41
41
|
| 关键词 | `keywords` | `items[]` |
|
|
@@ -78,7 +78,7 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
|
|
|
78
78
|
| 地域 | `locationName` / `targetingLocationName` |
|
|
79
79
|
| 设备 | `device` |
|
|
80
80
|
| 日报 | `date` |
|
|
81
|
-
| 周报 | `weekLabel`、`startDate`、`endDate`、`days
|
|
81
|
+
| 周报 | `daily.weekly[]` 的 `weekLabel`、`startDate`、`endDate`、`days`(CLI 已算) |
|
|
82
82
|
|
|
83
83
|
**账户总览 Sheet**:KPI 区(余额、本期消耗、日均、点击、CTR、CPC、转化)+ **全部叙事**(执行摘要、各维总结/建议、≥3 条跨维优化建议)写在本 Sheet;列表 Sheet 仅数据表。
|
|
84
84
|
|
|
@@ -90,6 +90,7 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
|
|
|
90
90
|
| ------------------------------------ | ---------------------------------------------------- |
|
|
91
91
|
| 把 `ctr` 当已是百分数再 ÷100 | 落盘为 0~1;Excel 用百分比格式或 `*100` 后加 `%` |
|
|
92
92
|
| 编造学习期、搜索词分类等无接口硬事实 | 无字段且算不出则不写该列;关键词翻译走 rowInsights |
|
|
93
|
+
| 自行按 `daily.date` 聚周(易出 `NaN-NaN-NaN`) | 读 `daily.weekly[]`;`date` 已是 YYYY-MM-DD,禁止再拼 `T00:00:00Z` |
|
|
93
94
|
| `campaignId` 写成数字 | `String(id)` + 文本格式 |
|
|
94
95
|
| search-terms 拉超过 180 天 | 缩短区间或排除该维 |
|
|
95
96
|
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
> 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML。
|
|
7
7
|
> **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `yandex-analysis render`;禁止假设存在内置 excel 子命令。见 `yandex-period-report-excel.md`。
|
|
8
8
|
|
|
9
|
-
数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 `daily`
|
|
9
|
+
数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 CLI 写入 `daily.weekly`(后端无独立 weekly)。**禁止**自行按 `date` 聚周。
|
|
10
10
|
|
|
11
11
|
| CLI `--sections` | 落盘形状(CI 实测) | 报告 `tables.*` / KPI |
|
|
12
12
|
| ---------------- | -------------------------------------------------------- | ---------------------------------------------------------- |
|
|
13
13
|
| `overview` | 对象:`account` / `currency` / `balance` / `totals` … | → `meta` + `kpis`(余额、日均、续航天数) |
|
|
14
|
-
| `daily` | `{ account, startDate, endDate, items: DayRow[] }`
|
|
14
|
+
| `daily` | `{ account, startDate, endDate, items: DayRow[], weekly: WeekRow[] }` | → `tables.daily[]` + `tables.weekly[]`(勿重算) |
|
|
15
15
|
| `search` | `{ …, items: SearchRow[] }` | → `tables.search[]` |
|
|
16
16
|
| `campaigns` | `{ …, items: CampaignRow[] }`(默认 limit=100) | → `tables.campaigns[]` |
|
|
17
17
|
| `keywords` | `{ …, items: KeywordRow[] }` | → `tables.keywords[]` |
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
| 步骤 | 执行者 | 动作 |
|
|
29
29
|
| ---------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
30
|
| **1. 拉数** | Agent 调 CLI | `yandex-analysis -a <porg-xxx> --start <s> --end <e> --json-out ./snap-yandex`(全 8 维或 `--sections` 指定子集) |
|
|
31
|
-
| **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json
|
|
31
|
+
| **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`)。周趋势读 `daily-*.json` 的 `weekly[]`,**禁止**自己按 `date` 聚周。 |
|
|
32
32
|
| **3. 写 JSON** | Agent | 撰写 `yandex-period-report.json`:仅 `meta.accountId` + `narrative`(8 个分析章节)为必填;`kpis`/`tables` 可省略由 `--snapshot-dir` 合并 |
|
|
33
33
|
| **4. 渲染 HTML** | CLI | `yandex-analysis render` — **校验 narrative 8 个分析章节必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
|
|
34
34
|
|
|
@@ -49,7 +49,7 @@ siluzan-tso yandex-analysis render \
|
|
|
49
49
|
- `kpis`:`spend` / `impressions` / `clicks` / `conversions` / `ctr` / `averageCpc` / `costPerConversion` / `conversionRate` / `roi` / `balance` / `avgDailySpend` / `activeDays` / `runwayDays` / `balanceAsOf`;可省略。
|
|
50
50
|
- `tables`(可省略,由 `--snapshot-dir` 映射):
|
|
51
51
|
- `daily[]`:`{date, spend, impressions, clicks, ctr, averageCpc, conversions, …}`
|
|
52
|
-
- `weekly[]`:`{weekLabel, startDate, endDate, days, spend,
|
|
52
|
+
- `weekly[]`:`{weekLabel, isoWeek, startDate, endDate, days, spend, …}`(**只读** CLI 已算好的周;`startDate`/`endDate` 为该 ISO 周周一~周日,`days` 为有数据的天数。禁止自填)
|
|
53
53
|
- `search[]`:`{network, spend, …}`
|
|
54
54
|
- `campaigns[]`:`{campaignId, campaignName, status, state, campaignType, budget, strategy, …metrics}`
|
|
55
55
|
- `keywords[]`:`{keyword, campaignName, criteriaType, network, …metrics}`
|
|
@@ -60,15 +60,14 @@ siluzan-tso yandex-analysis render \
|
|
|
60
60
|
- `executiveSummary[]`:≥1 段执行摘要
|
|
61
61
|
- `sections.{overview,search,campaigns,keywords,searchTerms,geo,devices,daily}`:每个维度各 `{analysis[](≥1,含数字依据), suggestions[](≥1)}`
|
|
62
62
|
- `recommendations[]`:≥3 条跨维度优化建议(可用 `{priority,text,anchor}`,anchor 对齐样例锚点如 `#keyword` / `#query`)
|
|
63
|
-
- `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML
|
|
63
|
+
- `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML。见下方「逐行洞察」。Campaign / 关键词 / 搜索词 / 地域 / 设备列表**不展示**「问题」「建议」列,行级判断写入各维 `sections.*.analysis/suggestions` 即可。
|
|
64
64
|
|
|
65
65
|
#### 逐行洞察 `narrative.rowInsights`
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
表格主观列目前只有关键词「中文翻译」,不是原始接口字段,由 Agent 基于已拉取原文翻译:
|
|
68
68
|
|
|
69
69
|
- **必填(`render` 校验)**:`tables.keywords` 有行时,须为 HTML 将展示的每一行(按消耗降序,最多 40 行)提供匹配的 `rowInsights`,且有非空 `translation`。缺一项即拒绝生成 HTML。
|
|
70
|
-
-
|
|
71
|
-
- 搜索词「分类」Yandex 不提供,也无法由现有指标算出,报告不展示该列。
|
|
70
|
+
- Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。
|
|
72
71
|
|
|
73
72
|
示例:
|
|
74
73
|
|
|
@@ -77,25 +76,13 @@ siluzan-tso yandex-analysis render \
|
|
|
77
76
|
"rowInsights": {
|
|
78
77
|
// 按 keyword 原文匹配(如同词跨系列,可加 campaignName 消歧)
|
|
79
78
|
"keywords": [
|
|
80
|
-
{ "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备"
|
|
81
|
-
]
|
|
82
|
-
// 按 searchQuery 原文匹配
|
|
83
|
-
"searchTerms": [
|
|
84
|
-
{ "match": { "searchQuery": "прокатный стан своими руками" }, "issue": "DIY 意图消耗", "suggestion": "添加否定短语 своими руками" }
|
|
85
|
-
],
|
|
86
|
-
// 按 campaignName 匹配
|
|
87
|
-
"campaigns": [
|
|
88
|
-
{ "match": { "campaignName": "Search - 自动定向" }, "issue": "自动定向 CTR 弱", "suggestion": "评估降价或暂停" }
|
|
89
|
-
],
|
|
90
|
-
// 按 locationName(城市名)匹配
|
|
91
|
-
"geo": [{ "match": { "locationName": "Moscow" }, "issue": "", "suggestion": "" }],
|
|
92
|
-
// 按 device 匹配
|
|
93
|
-
"devices": [{ "match": { "device": "tablet" }, "issue": "样本不足", "suggestion": "暂不调整设备系数" }]
|
|
79
|
+
{ "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备" }
|
|
80
|
+
]
|
|
94
81
|
}
|
|
95
82
|
}
|
|
96
83
|
```
|
|
97
84
|
|
|
98
|
-
- `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。
|
|
85
|
+
- `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。
|
|
99
86
|
- 每条洞察按原文匹配一行;HTML 将展示的关键词行若缺中文翻译,`render` 会拒绝出 HTML。
|
|
100
87
|
|
|
101
88
|
### 快照合并范围(`--snapshot-dir`)
|
|
@@ -104,9 +91,17 @@ siluzan-tso yandex-analysis render \
|
|
|
104
91
|
|
|
105
92
|
1. `meta`(仅补空字段)
|
|
106
93
|
2. **`kpis` / `tables.*` 一律以 CLI 快照覆盖**(有对应 section 时)
|
|
107
|
-
3. `tables.weekly` 由 `daily.items`
|
|
94
|
+
3. `tables.weekly` 由 CLI 从 `daily.items` 按 ISO 周重算并覆盖(与落盘 `daily.weekly` 同一套函数)
|
|
108
95
|
|
|
109
|
-
Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables`
|
|
96
|
+
Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables` 数值,**禁止**自行聚周。
|
|
97
|
+
|
|
98
|
+
### 周趋势(禁止再出现 NaN)
|
|
99
|
+
|
|
100
|
+
网关 `daily.items[].date` 可能是 ISO 时间戳(`2026-07-01T00:00:00+00:00`)。CLI 落盘时已截成 `YYYY-MM-DD`,并写好 `weekly[]`。
|
|
101
|
+
|
|
102
|
+
- 写「周趋势」叙事时:**脚本读 `weekly[]`**(`weekLabel` / `startDate` / `endDate` / `spend` / `conversions` / `days`)。
|
|
103
|
+
- **禁止** `new Date(date + 'T00:00:00Z')`:日期已是完整时间戳时再拼接会变成 Invalid Date,年份/月份变成 `NaN`,报告出现 `NaN-NaN-NaN`。
|
|
104
|
+
- **禁止**自己算 ISO 周、自己拆周。周末无投放时 `days` 会小于 7,属正常。
|
|
110
105
|
|
|
111
106
|
---
|
|
112
107
|
|
|
@@ -130,7 +125,8 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
|
|
|
130
125
|
|
|
131
126
|
- 写脚本前先读各 `yandex-*-<id>.outline.txt`(或 stdout 摘要里的 outline),再脚本读 `.json`。
|
|
132
127
|
- `search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。
|
|
133
|
-
-
|
|
128
|
+
- 周趋势读 `daily-*.json` 的 `weekly[]`,禁止自行聚周、禁止给 `date` 再拼 `T00:00:00Z`。
|
|
129
|
+
- Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。关键词中文翻译可由 Agent 通过 `narrative.rowInsights` 提供(见上方「逐行洞察」)。
|
|
134
130
|
|
|
135
131
|
---
|
|
136
132
|
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
| --------------- | ------------------------- | ---------------------------- |
|
|
36
36
|
| 账户总览 | `overview` | 对象:`totals` / `balance` … |
|
|
37
37
|
| 日报表 | `daily` | `items[]` |
|
|
38
|
-
| 周报表 |
|
|
38
|
+
| 周报表 | `daily` 的 `weekly[]` | **读落盘 weekly,禁止自行聚周** |
|
|
39
39
|
| Search 网络 | `search` | `items[]` |
|
|
40
40
|
| 广告系列 | `campaigns` | `items[]` |
|
|
41
41
|
| 关键词 | `keywords` | `items[]` |
|
|
@@ -78,7 +78,7 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
|
|
|
78
78
|
| 地域 | `locationName` / `targetingLocationName` |
|
|
79
79
|
| 设备 | `device` |
|
|
80
80
|
| 日报 | `date` |
|
|
81
|
-
| 周报 | `weekLabel`、`startDate`、`endDate`、`days
|
|
81
|
+
| 周报 | `daily.weekly[]` 的 `weekLabel`、`startDate`、`endDate`、`days`(CLI 已算) |
|
|
82
82
|
|
|
83
83
|
**账户总览 Sheet**:KPI 区(余额、本期消耗、日均、点击、CTR、CPC、转化)+ **全部叙事**(执行摘要、各维总结/建议、≥3 条跨维优化建议)写在本 Sheet;列表 Sheet 仅数据表。
|
|
84
84
|
|
|
@@ -90,6 +90,7 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
|
|
|
90
90
|
| ------------------------------------ | ---------------------------------------------------- |
|
|
91
91
|
| 把 `ctr` 当已是百分数再 ÷100 | 落盘为 0~1;Excel 用百分比格式或 `*100` 后加 `%` |
|
|
92
92
|
| 编造学习期、搜索词分类等无接口硬事实 | 无字段且算不出则不写该列;关键词翻译走 rowInsights |
|
|
93
|
+
| 自行按 `daily.date` 聚周(易出 `NaN-NaN-NaN`) | 读 `daily.weekly[]`;`date` 已是 YYYY-MM-DD,禁止再拼 `T00:00:00Z` |
|
|
93
94
|
| `campaignId` 写成数字 | `String(id)` + 文本格式 |
|
|
94
95
|
| search-terms 拉超过 180 天 | 缩短区间或排除该维 |
|
|
95
96
|
|
|
@@ -380,9 +380,26 @@
|
|
|
380
380
|
return String(v ?? "").trim().toLowerCase();
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
+
/** 周区间文案:含 NaN 时回退,避免脚本误聚周把 Invalid Date 写进表格。 */
|
|
384
|
+
function weekRangeText(r) {
|
|
385
|
+
const start = String((r && r.startDate) || "").trim();
|
|
386
|
+
const end = String((r && r.endDate) || "").trim();
|
|
387
|
+
const joined = start && end ? start + " ~ " + end : start || end;
|
|
388
|
+
if (joined && !/nan/i.test(joined)) return joined;
|
|
389
|
+
const label = String((r && r.weekLabel) || "").trim();
|
|
390
|
+
if (label && !/nan/i.test(label)) return label;
|
|
391
|
+
return "—";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function weekLabelText(r) {
|
|
395
|
+
const label = String((r && (r.isoWeek || r.weekLabel)) || "").trim();
|
|
396
|
+
if (label && !/nan/i.test(label)) return label;
|
|
397
|
+
return "—";
|
|
398
|
+
}
|
|
399
|
+
|
|
383
400
|
/**
|
|
384
|
-
* 从 Agent
|
|
385
|
-
*
|
|
401
|
+
* 从 Agent 提供的 `narrative.rowInsights.<dim>[]` 按主字段匹配行洞察(目前仅关键词中文翻译)。
|
|
402
|
+
* 找不到返回 null——渲染层显示「—」,不编造内容。
|
|
386
403
|
* 每条洞察支持 `{match:{...}, ...}` 或扁平写法 `{keyword, campaignName, ...}`。
|
|
387
404
|
*/
|
|
388
405
|
function findInsight(list, primaryField, primaryValue, row) {
|
|
@@ -541,9 +558,9 @@
|
|
|
541
558
|
.map(function (r) {
|
|
542
559
|
return (
|
|
543
560
|
"<tr><td>" +
|
|
544
|
-
escapeHtml(r
|
|
561
|
+
escapeHtml(weekLabelText(r)) +
|
|
545
562
|
"</td><td>" +
|
|
546
|
-
escapeHtml((r
|
|
563
|
+
escapeHtml(weekRangeText(r)) +
|
|
547
564
|
'</td><td class="num">' +
|
|
548
565
|
int(r.days) +
|
|
549
566
|
'</td><td class="num">' +
|
|
@@ -852,9 +869,9 @@
|
|
|
852
869
|
.map(function (r) {
|
|
853
870
|
return (
|
|
854
871
|
"<tr><td>" +
|
|
855
|
-
escapeHtml(r
|
|
872
|
+
escapeHtml(weekLabelText(r)) +
|
|
856
873
|
"</td><td>" +
|
|
857
|
-
escapeHtml((r
|
|
874
|
+
escapeHtml(weekRangeText(r)) +
|
|
858
875
|
'</td><td class="num">' +
|
|
859
876
|
int(r.days) +
|
|
860
877
|
'</td><td class="num">' +
|
|
@@ -883,7 +900,7 @@
|
|
|
883
900
|
"</div>" +
|
|
884
901
|
"</div></section>" +
|
|
885
902
|
'<section id="weekly"><div class="wrap">' +
|
|
886
|
-
sectionHead("周报表", "
|
|
903
|
+
sectionHead("周报表", "由 CLI 从 daily 按 ISO 周聚合(禁止脚本重算)") +
|
|
887
904
|
'<div class="panel">' +
|
|
888
905
|
weeklyBody +
|
|
889
906
|
"</div>" +
|
|
@@ -963,16 +980,8 @@
|
|
|
963
980
|
{ label: "Conv", num: true },
|
|
964
981
|
{ label: "CPA", num: true },
|
|
965
982
|
{ label: "状态" },
|
|
966
|
-
{ label: "问题" },
|
|
967
|
-
{ label: "建议" },
|
|
968
983
|
],
|
|
969
|
-
rowCells: function (r, currency
|
|
970
|
-
const insight = findInsight(
|
|
971
|
-
rowInsightsOf(data, "campaigns"),
|
|
972
|
-
"campaignName",
|
|
973
|
-
r.campaignName,
|
|
974
|
-
r
|
|
975
|
-
);
|
|
984
|
+
rowCells: function (r, currency) {
|
|
976
985
|
return (
|
|
977
986
|
"<td>" +
|
|
978
987
|
escapeHtml(r.campaignName || r.campaignId || "—") +
|
|
@@ -996,10 +1005,6 @@
|
|
|
996
1005
|
money(r.costPerConversion, currency) +
|
|
997
1006
|
"</td><td>" +
|
|
998
1007
|
campaignStatusBadge(r) +
|
|
999
|
-
"</td><td>" +
|
|
1000
|
-
escapeHtml((insight && insight.issue) || "—") +
|
|
1001
|
-
"</td><td>" +
|
|
1002
|
-
escapeHtml((insight && insight.suggestion) || "—") +
|
|
1003
1008
|
"</td>"
|
|
1004
1009
|
);
|
|
1005
1010
|
},
|
|
@@ -1015,7 +1020,7 @@
|
|
|
1015
1020
|
title: "关键词",
|
|
1016
1021
|
desc: "按消耗排序的关键词明细",
|
|
1017
1022
|
footnote:
|
|
1018
|
-
"
|
|
1023
|
+
"「中文翻译」为 Agent 撰写报告时提供(narrative.rowInsights.keywords),未提供的行显示「—」,不做机械占位。",
|
|
1019
1024
|
headers: [
|
|
1020
1025
|
{ label: "Campaign" },
|
|
1021
1026
|
{ label: "Group" },
|
|
@@ -1029,8 +1034,6 @@
|
|
|
1029
1034
|
{ label: "CPA", num: true },
|
|
1030
1035
|
{ label: "CTR", num: true },
|
|
1031
1036
|
{ label: "CPC", num: true },
|
|
1032
|
-
{ label: "问题" },
|
|
1033
|
-
{ label: "建议" },
|
|
1034
1037
|
],
|
|
1035
1038
|
rowCells: function (r, currency, data) {
|
|
1036
1039
|
const insight = findInsight(rowInsightsOf(data, "keywords"), "keyword", r.keyword, r);
|
|
@@ -1059,10 +1062,6 @@
|
|
|
1059
1062
|
pct(derivedCtr(r)) +
|
|
1060
1063
|
'</td><td class="num">' +
|
|
1061
1064
|
money(derivedCpc(r), currency) +
|
|
1062
|
-
"</td><td>" +
|
|
1063
|
-
escapeHtml((insight && insight.issue) || "—") +
|
|
1064
|
-
"</td><td>" +
|
|
1065
|
-
escapeHtml((insight && insight.suggestion) || "—") +
|
|
1066
1065
|
"</td>"
|
|
1067
1066
|
);
|
|
1068
1067
|
},
|
|
@@ -1077,8 +1076,6 @@
|
|
|
1077
1076
|
tableKey: "searchTerms",
|
|
1078
1077
|
title: "搜索词",
|
|
1079
1078
|
desc: "按消耗排序的搜索词明细(仅近 180 天数据)",
|
|
1080
|
-
footnote:
|
|
1081
|
-
"「问题/建议」为 Agent 撰写报告时提供(narrative.rowInsights.searchTerms),未提供的行显示「—」,不做机械占位。",
|
|
1082
1079
|
headers: [
|
|
1083
1080
|
{ label: "Search Query" },
|
|
1084
1081
|
{ label: "匹配关键词" },
|
|
@@ -1090,16 +1087,8 @@
|
|
|
1090
1087
|
{ label: "Conv", num: true },
|
|
1091
1088
|
{ label: "cr%", num: true },
|
|
1092
1089
|
{ label: "CPA", num: true },
|
|
1093
|
-
{ label: "问题" },
|
|
1094
|
-
{ label: "建议" },
|
|
1095
1090
|
],
|
|
1096
|
-
rowCells: function (r, currency
|
|
1097
|
-
const insight = findInsight(
|
|
1098
|
-
rowInsightsOf(data, "searchTerms"),
|
|
1099
|
-
"searchQuery",
|
|
1100
|
-
r.searchQuery,
|
|
1101
|
-
r
|
|
1102
|
-
);
|
|
1091
|
+
rowCells: function (r, currency) {
|
|
1103
1092
|
return (
|
|
1104
1093
|
"<td>" +
|
|
1105
1094
|
escapeHtml(r.searchQuery || "—") +
|
|
@@ -1121,10 +1110,6 @@
|
|
|
1121
1110
|
pct(derivedCr(r)) +
|
|
1122
1111
|
'</td><td class="num">' +
|
|
1123
1112
|
money(derivedCpa(r), currency) +
|
|
1124
|
-
"</td><td>" +
|
|
1125
|
-
escapeHtml((insight && insight.issue) || "—") +
|
|
1126
|
-
"</td><td>" +
|
|
1127
|
-
escapeHtml((insight && insight.suggestion) || "—") +
|
|
1128
1113
|
"</td>"
|
|
1129
1114
|
);
|
|
1130
1115
|
},
|
|
@@ -1151,8 +1136,6 @@
|
|
|
1151
1136
|
{ label: "Conv", num: true },
|
|
1152
1137
|
{ label: "cr%", num: true },
|
|
1153
1138
|
{ label: "CPA", num: true },
|
|
1154
|
-
{ label: "问题" },
|
|
1155
|
-
{ label: "建议" },
|
|
1156
1139
|
],
|
|
1157
1140
|
rowCells: function (r, currency, data) {
|
|
1158
1141
|
const cityName =
|
|
@@ -1161,7 +1144,6 @@
|
|
|
1161
1144
|
r.location ||
|
|
1162
1145
|
r.name ||
|
|
1163
1146
|
(r.locationId != null ? "地域 " + r.locationId : "—");
|
|
1164
|
-
const insight = findInsight(rowInsightsOf(data, "geo"), "locationName", cityName, r);
|
|
1165
1147
|
return (
|
|
1166
1148
|
"<td>" +
|
|
1167
1149
|
escapeHtml((data.meta && data.meta.region) || "—") +
|
|
@@ -1183,10 +1165,6 @@
|
|
|
1183
1165
|
pct(derivedCr(r)) +
|
|
1184
1166
|
'</td><td class="num">' +
|
|
1185
1167
|
money(derivedCpa(r), currency) +
|
|
1186
|
-
"</td><td>" +
|
|
1187
|
-
escapeHtml((insight && insight.issue) || "—") +
|
|
1188
|
-
"</td><td>" +
|
|
1189
|
-
escapeHtml((insight && insight.suggestion) || "—") +
|
|
1190
1168
|
"</td>"
|
|
1191
1169
|
);
|
|
1192
1170
|
},
|
|
@@ -1211,11 +1189,8 @@
|
|
|
1211
1189
|
{ label: "Conv", num: true },
|
|
1212
1190
|
{ label: "cr%", num: true },
|
|
1213
1191
|
{ label: "CPA", num: true },
|
|
1214
|
-
{ label: "问题" },
|
|
1215
|
-
{ label: "建议" },
|
|
1216
1192
|
],
|
|
1217
|
-
rowCells: function (r, currency
|
|
1218
|
-
const insight = findInsight(rowInsightsOf(data, "devices"), "device", r.device, r);
|
|
1193
|
+
rowCells: function (r, currency) {
|
|
1219
1194
|
return (
|
|
1220
1195
|
"<td>" +
|
|
1221
1196
|
escapeHtml(r.device || "—") +
|
|
@@ -1235,10 +1210,6 @@
|
|
|
1235
1210
|
pct(derivedCr(r)) +
|
|
1236
1211
|
'</td><td class="num">' +
|
|
1237
1212
|
money(derivedCpa(r), currency) +
|
|
1238
|
-
"</td><td>" +
|
|
1239
|
-
escapeHtml((insight && insight.issue) || "—") +
|
|
1240
|
-
"</td><td>" +
|
|
1241
|
-
escapeHtml((insight && insight.suggestion) || "—") +
|
|
1242
1213
|
"</td>"
|
|
1243
1214
|
);
|
|
1244
1215
|
},
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
> 用户**未指定格式**时一律走下方 **标准四步流程**;**禁止** Agent 手写/拼接 HTML。
|
|
7
7
|
> **用户明确要求 Excel / xlsx** 时:拉数步骤不变,改为 Agent 脚本写 `.xlsx`;**不要**调用 `yandex-analysis render`;禁止假设存在内置 excel 子命令。见 `yandex-period-report-excel.md`。
|
|
8
8
|
|
|
9
|
-
数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 `daily`
|
|
9
|
+
数据块(YandexAPI `/yandex-analysis` 8 维 + 本地周聚合):总览、按日、Search 网络、系列、关键词、搜索词、地域、设备;周报由 CLI 写入 `daily.weekly`(后端无独立 weekly)。**禁止**自行按 `date` 聚周。
|
|
10
10
|
|
|
11
11
|
| CLI `--sections` | 落盘形状(CI 实测) | 报告 `tables.*` / KPI |
|
|
12
12
|
| ---------------- | -------------------------------------------------------- | ---------------------------------------------------------- |
|
|
13
13
|
| `overview` | 对象:`account` / `currency` / `balance` / `totals` … | → `meta` + `kpis`(余额、日均、续航天数) |
|
|
14
|
-
| `daily` | `{ account, startDate, endDate, items: DayRow[] }`
|
|
14
|
+
| `daily` | `{ account, startDate, endDate, items: DayRow[], weekly: WeekRow[] }` | → `tables.daily[]` + `tables.weekly[]`(勿重算) |
|
|
15
15
|
| `search` | `{ …, items: SearchRow[] }` | → `tables.search[]` |
|
|
16
16
|
| `campaigns` | `{ …, items: CampaignRow[] }`(默认 limit=100) | → `tables.campaigns[]` |
|
|
17
17
|
| `keywords` | `{ …, items: KeywordRow[] }` | → `tables.keywords[]` |
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
| 步骤 | 执行者 | 动作 |
|
|
29
29
|
| ---------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
30
|
| **1. 拉数** | Agent 调 CLI | `yandex-analysis -a <porg-xxx> --start <s> --end <e> --json-out ./snap-yandex`(全 8 维或 `--sections` 指定子集) |
|
|
31
|
-
| **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json
|
|
31
|
+
| **2. 分析** | Agent | 用 **node/python 脚本**读落盘 JSON(勿用 Read 打开业务 `*.json`)。周趋势读 `daily-*.json` 的 `weekly[]`,**禁止**自己按 `date` 聚周。 |
|
|
32
32
|
| **3. 写 JSON** | Agent | 撰写 `yandex-period-report.json`:仅 `meta.accountId` + `narrative`(8 个分析章节)为必填;`kpis`/`tables` 可省略由 `--snapshot-dir` 合并 |
|
|
33
33
|
| **4. 渲染 HTML** | CLI | `yandex-analysis render` — **校验 narrative 8 个分析章节必含字段**,缺项报错不生成 HTML;**禁止** Agent 手写/拼接 HTML |
|
|
34
34
|
|
|
@@ -49,7 +49,7 @@ siluzan-tso yandex-analysis render \
|
|
|
49
49
|
- `kpis`:`spend` / `impressions` / `clicks` / `conversions` / `ctr` / `averageCpc` / `costPerConversion` / `conversionRate` / `roi` / `balance` / `avgDailySpend` / `activeDays` / `runwayDays` / `balanceAsOf`;可省略。
|
|
50
50
|
- `tables`(可省略,由 `--snapshot-dir` 映射):
|
|
51
51
|
- `daily[]`:`{date, spend, impressions, clicks, ctr, averageCpc, conversions, …}`
|
|
52
|
-
- `weekly[]`:`{weekLabel, startDate, endDate, days, spend,
|
|
52
|
+
- `weekly[]`:`{weekLabel, isoWeek, startDate, endDate, days, spend, …}`(**只读** CLI 已算好的周;`startDate`/`endDate` 为该 ISO 周周一~周日,`days` 为有数据的天数。禁止自填)
|
|
53
53
|
- `search[]`:`{network, spend, …}`
|
|
54
54
|
- `campaigns[]`:`{campaignId, campaignName, status, state, campaignType, budget, strategy, …metrics}`
|
|
55
55
|
- `keywords[]`:`{keyword, campaignName, criteriaType, network, …metrics}`
|
|
@@ -60,15 +60,14 @@ siluzan-tso yandex-analysis render \
|
|
|
60
60
|
- `executiveSummary[]`:≥1 段执行摘要
|
|
61
61
|
- `sections.{overview,search,campaigns,keywords,searchTerms,geo,devices,daily}`:每个维度各 `{analysis[](≥1,含数字依据), suggestions[](≥1)}`
|
|
62
62
|
- `recommendations[]`:≥3 条跨维度优化建议(可用 `{priority,text,anchor}`,anchor 对齐样例锚点如 `#keyword` / `#query`)
|
|
63
|
-
- `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML
|
|
63
|
+
- `rowInsights`:有关键词表格行时,**HTML 将展示的 Top N 行必须有中文翻译**;`render` 缺项会拒绝出 HTML。见下方「逐行洞察」。Campaign / 关键词 / 搜索词 / 地域 / 设备列表**不展示**「问题」「建议」列,行级判断写入各维 `sections.*.analysis/suggestions` 即可。
|
|
64
64
|
|
|
65
65
|
#### 逐行洞察 `narrative.rowInsights`
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
表格主观列目前只有关键词「中文翻译」,不是原始接口字段,由 Agent 基于已拉取原文翻译:
|
|
68
68
|
|
|
69
69
|
- **必填(`render` 校验)**:`tables.keywords` 有行时,须为 HTML 将展示的每一行(按消耗降序,最多 40 行)提供匹配的 `rowInsights`,且有非空 `translation`。缺一项即拒绝生成 HTML。
|
|
70
|
-
-
|
|
71
|
-
- 搜索词「分类」Yandex 不提供,也无法由现有指标算出,报告不展示该列。
|
|
70
|
+
- Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。
|
|
72
71
|
|
|
73
72
|
示例:
|
|
74
73
|
|
|
@@ -77,25 +76,13 @@ siluzan-tso yandex-analysis render \
|
|
|
77
76
|
"rowInsights": {
|
|
78
77
|
// 按 keyword 原文匹配(如同词跨系列,可加 campaignName 消歧)
|
|
79
78
|
"keywords": [
|
|
80
|
-
{ "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备"
|
|
81
|
-
]
|
|
82
|
-
// 按 searchQuery 原文匹配
|
|
83
|
-
"searchTerms": [
|
|
84
|
-
{ "match": { "searchQuery": "прокатный стан своими руками" }, "issue": "DIY 意图消耗", "suggestion": "添加否定短语 своими руками" }
|
|
85
|
-
],
|
|
86
|
-
// 按 campaignName 匹配
|
|
87
|
-
"campaigns": [
|
|
88
|
-
{ "match": { "campaignName": "Search - 自动定向" }, "issue": "自动定向 CTR 弱", "suggestion": "评估降价或暂停" }
|
|
89
|
-
],
|
|
90
|
-
// 按 locationName(城市名)匹配
|
|
91
|
-
"geo": [{ "match": { "locationName": "Moscow" }, "issue": "", "suggestion": "" }],
|
|
92
|
-
// 按 device 匹配
|
|
93
|
-
"devices": [{ "match": { "device": "tablet" }, "issue": "样本不足", "suggestion": "暂不调整设备系数" }]
|
|
79
|
+
{ "match": { "keyword": "металлопрокатное оборудование" }, "translation": "金属轧制设备" }
|
|
80
|
+
]
|
|
94
81
|
}
|
|
95
82
|
}
|
|
96
83
|
```
|
|
97
84
|
|
|
98
|
-
- `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。
|
|
85
|
+
- `translation` 是 Agent **对已知原文的翻译**,允许生成;不得编造原始接口没有、也无法由现有指标算出的硬事实。
|
|
99
86
|
- 每条洞察按原文匹配一行;HTML 将展示的关键词行若缺中文翻译,`render` 会拒绝出 HTML。
|
|
100
87
|
|
|
101
88
|
### 快照合并范围(`--snapshot-dir`)
|
|
@@ -104,9 +91,17 @@ siluzan-tso yandex-analysis render \
|
|
|
104
91
|
|
|
105
92
|
1. `meta`(仅补空字段)
|
|
106
93
|
2. **`kpis` / `tables.*` 一律以 CLI 快照覆盖**(有对应 section 时)
|
|
107
|
-
3. `tables.weekly` 由 `daily.items`
|
|
94
|
+
3. `tables.weekly` 由 CLI 从 `daily.items` 按 ISO 周重算并覆盖(与落盘 `daily.weekly` 同一套函数)
|
|
108
95
|
|
|
109
|
-
Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables`
|
|
96
|
+
Agent 只需撰写 `narrative`;**禁止手写 HTML**,也**禁止**自填 `kpis`/`tables` 数值,**禁止**自行聚周。
|
|
97
|
+
|
|
98
|
+
### 周趋势(禁止再出现 NaN)
|
|
99
|
+
|
|
100
|
+
网关 `daily.items[].date` 可能是 ISO 时间戳(`2026-07-01T00:00:00+00:00`)。CLI 落盘时已截成 `YYYY-MM-DD`,并写好 `weekly[]`。
|
|
101
|
+
|
|
102
|
+
- 写「周趋势」叙事时:**脚本读 `weekly[]`**(`weekLabel` / `startDate` / `endDate` / `spend` / `conversions` / `days`)。
|
|
103
|
+
- **禁止** `new Date(date + 'T00:00:00Z')`:日期已是完整时间戳时再拼接会变成 Invalid Date,年份/月份变成 `NaN`,报告出现 `NaN-NaN-NaN`。
|
|
104
|
+
- **禁止**自己算 ISO 周、自己拆周。周末无投放时 `days` 会小于 7,属正常。
|
|
110
105
|
|
|
111
106
|
---
|
|
112
107
|
|
|
@@ -130,7 +125,8 @@ siluzan-tso yandex-analysis -a <porg-xxx> --start <S> --end <E> --json-out ./sna
|
|
|
130
125
|
|
|
131
126
|
- 写脚本前先读各 `yandex-*-<id>.outline.txt`(或 stdout 摘要里的 outline),再脚本读 `.json`。
|
|
132
127
|
- `search` / `campaigns` / `keywords` / `geo` / `devices` 在 Direct 侧过滤 `AdNetworkType=SEARCH`。
|
|
133
|
-
-
|
|
128
|
+
- 周趋势读 `daily-*.json` 的 `weekly[]`,禁止自行聚周、禁止给 `date` 再拼 `T00:00:00Z`。
|
|
129
|
+
- Campaign「学习期」、搜索词「分类」、各维行级「问题/建议」均不展示。关键词中文翻译可由 Agent 通过 `narrative.rowInsights` 提供(见上方「逐行洞察」)。
|
|
134
130
|
|
|
135
131
|
---
|
|
136
132
|
|
|
@@ -9,11 +9,11 @@ $ErrorActionPreference = 'Stop'
|
|
|
9
9
|
# -- Package info (injected at build time) ------------------------------------
|
|
10
10
|
$PKG_NAME = 'siluzan-tso-cli'
|
|
11
11
|
# PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
|
|
12
|
-
$PKG_VERSION = '1.1.45
|
|
12
|
+
$PKG_VERSION = '1.1.45'
|
|
13
13
|
$CLI_BIN = 'siluzan-tso'
|
|
14
14
|
$SKILL_LABEL = 'Siluzan TSO'
|
|
15
|
-
$INSTALL_CMD = 'npm install -g siluzan-tso-cli
|
|
16
|
-
$WEB_BASE = 'https://www
|
|
15
|
+
$INSTALL_CMD = 'npm install -g siluzan-tso-cli'
|
|
16
|
+
$WEB_BASE = 'https://www.siluzan.com'
|
|
17
17
|
|
|
18
18
|
# -- Constants ----------------------------------------------------------------
|
|
19
19
|
$NODE_MAJOR_MIN = 18
|
|
@@ -9,11 +9,11 @@ set -euo pipefail
|
|
|
9
9
|
# -- Package info (injected at build time) ------------------------------------
|
|
10
10
|
readonly PKG_NAME="siluzan-tso-cli"
|
|
11
11
|
# PKG_VERSION 锁定到与本脚本同批构建产物一致的版本,避免与 dist/skill 错位
|
|
12
|
-
readonly PKG_VERSION="1.1.45
|
|
12
|
+
readonly PKG_VERSION="1.1.45"
|
|
13
13
|
readonly CLI_BIN="siluzan-tso"
|
|
14
14
|
readonly SKILL_LABEL="Siluzan TSO"
|
|
15
|
-
readonly INSTALL_CMD="npm install -g siluzan-tso-cli
|
|
16
|
-
readonly WEB_BASE="https://www
|
|
15
|
+
readonly INSTALL_CMD="npm install -g siluzan-tso-cli"
|
|
16
|
+
readonly WEB_BASE="https://www.siluzan.com"
|
|
17
17
|
|
|
18
18
|
# -- Constants ----------------------------------------------------------------
|
|
19
19
|
readonly NODE_MAJOR_MIN=18
|