siluzan-tso-cli 1.1.18-beta.5 → 1.1.18-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,7 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
51
51
  siluzan-tso init --force # 强制覆盖已存在文件
52
52
  ```
53
53
 
54
- > **注意**:当前为测试版(1.1.18-beta.5),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.18-beta.7),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
@@ -69,7 +69,18 @@ siluzan-tso init --force # 强制覆盖已存在文件
69
69
 
70
70
  **若用户已安装 `siluzan-cso` 并完成登录,无需重复操作,直接跳到第 4 步。**
71
71
 
72
- ### 方式 A:交互式登录(推荐)
72
+ **推荐顺序**:① **手机号 + 验证码**(首选)完整参数与排错见 `references/setup.md`(随 Skill 安装到本地)。
73
+
74
+ ### 方式 A:手机号 + 验证码(**推荐**)
75
+
76
+ ```bash
77
+ siluzan-tso send-login-code --phone <手机号>
78
+ siluzan-tso login --phone <手机号> --code <6位验证码>
79
+ ```
80
+
81
+ 不向终端索要交互输入;Agent 先发码、用户回填验证码后再执行第二步。手机号须已在丝路赞注册。
82
+
83
+ ### 方式 B:交互式登录(需真人 TTY)
73
84
 
74
85
  ```bash
75
86
  siluzan-tso login
@@ -81,13 +92,13 @@ siluzan-tso login
81
92
  siluzan-tso login --api-key <YOUR_API_KEY>
82
93
  ```
83
94
 
84
- ### 方式 B:直接设置(适合自动化场景)
95
+ ### 方式 C:直接设置(适合自动化场景)
85
96
 
86
97
  ```bash
87
98
  siluzan-tso config set --api-key <你的ApiKey>
88
99
  ```
89
100
 
90
- ### 方式 C:Token 登录(已有 siluzan-cso 账号)
101
+ ### 方式 D:Token 登录(已有 siluzan-cso 账号)
91
102
 
92
103
  ```bash
93
104
  npm install -g siluzan-cso-cli
@@ -137,4 +148,4 @@ siluzan-tso init --ai xxx --force
137
148
 
138
149
  ## 6. Token 续期
139
150
 
140
- Token 过期后,重新运行 `siluzan-cso login` `siluzan-tso config set --api-key <新Key>` 即可,`siluzan-tso` 自动读取新凭据,无需额外操作。
151
+ Token 过期后:**优先**再走一遍**方式 A**(手机验证码)签发新 API Key;或 `siluzan-cso login` / `siluzan-tso config set --api-key <新Key>`,`siluzan-tso` 自动读取新凭据。
package/dist/index.js CHANGED
@@ -1974,6 +1974,7 @@ import { performance } from "perf_hooks";
1974
1974
  import * as fs2 from "fs";
1975
1975
  import * as path2 from "path";
1976
1976
  import { fileURLToPath } from "url";
1977
+ import { spawn } from "child_process";
1977
1978
  import Table from "cli-table3";
1978
1979
  import * as path3 from "path";
1979
1980
  import { chmod, mkdir, open, readdir, stat, unlink } from "fs/promises";
@@ -2375,6 +2376,29 @@ async function fetchNpmVersion(pkgName, tag, timeoutMs = 4e3) {
2375
2376
  return null;
2376
2377
  }
2377
2378
  }
2379
+ async function defaultRunMinRequiredGlobalInstall(ctx) {
2380
+ if (process.env.SILUZAN_SKIP_AUTO_GLOBAL_INSTALL === "1") {
2381
+ return { ok: false, stderr: "SILUZAN_SKIP_AUTO_GLOBAL_INSTALL=1" };
2382
+ }
2383
+ const spec = `${ctx.pkgName}@${ctx.tag}`;
2384
+ return await new Promise((resolve32) => {
2385
+ const child = spawn("npm", ["install", "-g", spec, "--no-fund", "--no-audit"], {
2386
+ stdio: "inherit",
2387
+ shell: true
2388
+ });
2389
+ child.on("error", (err) => {
2390
+ resolve32({ ok: false, stderr: err instanceof Error ? err.message : String(err) });
2391
+ });
2392
+ child.on("close", (code, signal) => {
2393
+ if (code === 0) {
2394
+ resolve32({ ok: true });
2395
+ } else {
2396
+ const sig = signal ? ` signal=${signal}` : "";
2397
+ resolve32({ ok: false, stderr: `exit code ${code}${sig}` });
2398
+ }
2399
+ });
2400
+ });
2401
+ }
2378
2402
  function createVersionNotifier(opts) {
2379
2403
  const {
2380
2404
  pkgName,
@@ -2383,27 +2407,33 @@ function createVersionNotifier(opts) {
2383
2407
  resolveTag,
2384
2408
  forceUpdateExtra = "",
2385
2409
  updateAvailableExtra = "",
2410
+ runMinRequiredGlobalInstall = defaultRunMinRequiredGlobalInstall,
2386
2411
  getCurrentVersion: getCurrentVersion22,
2387
2412
  mergeWriteConfig,
2388
2413
  readConfigRaw
2389
2414
  } = opts;
2390
- const KEY_LAST_CHECK = `${cachePrefix}LastVersionCheck`;
2391
2415
  const KEY_LATEST_STABLE = `${cachePrefix}LatestStable`;
2392
2416
  const KEY_LATEST_BETA = `${cachePrefix}LatestBeta`;
2393
2417
  const KEY_MIN_STABLE = `${cachePrefix}MinRequiredStable`;
2394
2418
  const KEY_MIN_BETA = `${cachePrefix}MinRequiredBeta`;
2395
2419
  const KEY_LAST_NOTIFIED = `${cachePrefix}LastNotified`;
2420
+ const KEY_FETCH_AT_MAIN = `${cachePrefix}VersionFetchAtMain`;
2421
+ const KEY_FETCH_AT_MIN = `${cachePrefix}VersionFetchAtMin`;
2396
2422
  const HOURS_24 = 24 * 60 * 60 * 1e3;
2397
- async function fetchVersionByTag(tag, cacheKey, cfg) {
2398
- const lastCheck = cfg[KEY_LAST_CHECK];
2399
- if (typeof lastCheck === "string" && cacheKey in cfg) {
2400
- const lastMs = new Date(lastCheck).getTime();
2401
- if (Date.now() - lastMs < HOURS_24) {
2423
+ const TTL_MAIN_TAG_MS = 60 * 60 * 1e3;
2424
+ const TTL_MIN_REQUIRED_MS = HOURS_24;
2425
+ async function fetchVersionByTag(tag, cacheKey, fetchAtKey, cfg, maxAgeMs) {
2426
+ const lastAt = cfg[fetchAtKey];
2427
+ if (typeof lastAt === "string" && cacheKey in cfg) {
2428
+ const lastMs = new Date(lastAt).getTime();
2429
+ if (Date.now() - lastMs < maxAgeMs) {
2402
2430
  const v = cfg[cacheKey];
2403
- return typeof v === "string" && v ? v : null;
2431
+ const sv = typeof v === "string" && v ? v : null;
2432
+ return { version: sv, hitNetwork: false };
2404
2433
  }
2405
2434
  }
2406
- return fetchNpmVersion(pkgName, tag);
2435
+ const version = await fetchNpmVersion(pkgName, tag);
2436
+ return { version, hitNetwork: true };
2407
2437
  }
2408
2438
  async function notifyIfOutdated2() {
2409
2439
  try {
@@ -2414,15 +2444,20 @@ function createVersionNotifier(opts) {
2414
2444
  const minCacheKey = isBeta ? KEY_MIN_BETA : KEY_MIN_STABLE;
2415
2445
  const minTag = npmMinRequiredTagForBuildEnv(isBeta ? "test" : "production");
2416
2446
  const cfg = readConfigRaw();
2417
- const [latest, minRequired] = await Promise.all([
2418
- fetchVersionByTag(tag, latestCacheKey, cfg),
2419
- fetchVersionByTag(minTag, minCacheKey, cfg)
2447
+ const [mainRes, minRes] = await Promise.all([
2448
+ fetchVersionByTag(tag, latestCacheKey, KEY_FETCH_AT_MAIN, cfg, TTL_MAIN_TAG_MS),
2449
+ fetchVersionByTag(minTag, minCacheKey, KEY_FETCH_AT_MIN, cfg, TTL_MIN_REQUIRED_MS)
2420
2450
  ]);
2421
- await mergeWriteConfig({
2422
- [KEY_LAST_CHECK]: (/* @__PURE__ */ new Date()).toISOString(),
2451
+ const latest = mainRes.version;
2452
+ const minRequired = minRes.version;
2453
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2454
+ const cacheUpdates = {
2423
2455
  [latestCacheKey]: latest ?? "",
2424
2456
  [minCacheKey]: minRequired ?? ""
2425
- });
2457
+ };
2458
+ if (mainRes.hitNetwork) cacheUpdates[KEY_FETCH_AT_MAIN] = nowIso;
2459
+ if (minRes.hitNetwork) cacheUpdates[KEY_FETCH_AT_MIN] = nowIso;
2460
+ await mergeWriteConfig(cacheUpdates);
2426
2461
  const lastNotified = typeof cfg[KEY_LAST_NOTIFIED] === "string" ? new Date(cfg[KEY_LAST_NOTIFIED]).getTime() : 0;
2427
2462
  if (Date.now() - lastNotified < HOURS_24) return;
2428
2463
  const tagLabel = isBeta ? "\uFF08\u6D4B\u8BD5\u7248\uFF09" : "\uFF08\u6B63\u5F0F\u7248\uFF09";
@@ -2430,11 +2465,26 @@ function createVersionNotifier(opts) {
2430
2465
  await mergeWriteConfig({ [KEY_LAST_NOTIFIED]: (/* @__PURE__ */ new Date()).toISOString() });
2431
2466
  console.log(
2432
2467
  `
2433
- [SILUZAN_FORCE_UPDATE] \u5F53\u524D\u7248\u672C ${current} \u5DF2\u8FC7\u671F\uFF0C\u6700\u4F4E\u8981\u6C42\u7248\u672C ${minRequired}${tagLabel}
2434
- \u26D4 \u5FC5\u987B\u5148\u5347\u7EA7\u624D\u80FD\u7EE7\u7EED\u4F7F\u7528\uFF0CAgent \u8BF7\u7ACB\u5373\u6267\u884C\uFF1A${binName} update
2435
- \u6216\u624B\u52A8\uFF1Anpm install -g ${pkgName}@${tag}
2468
+ [SILUZAN_FORCE_UPDATE] \u5F53\u524D\u7248\u672C ${current} \u5DF2\u4F4E\u4E8E\u6700\u4F4E\u8981\u6C42 ${minRequired}${tagLabel}
2469
+ \u26D4 \u6B63\u5728\u81EA\u52A8\u6267\u884C\uFF1Anpm install -g ${pkgName}@${tag}
2436
2470
  ` + (forceUpdateExtra ? forceUpdateExtra + "\n" : "")
2437
2471
  );
2472
+ const installResult = await runMinRequiredGlobalInstall({ pkgName, tag });
2473
+ if (installResult.ok) {
2474
+ console.log(
2475
+ `
2476
+ [SILUZAN_AUTO_GLOBAL_INSTALL_OK] \u5168\u5C40\u5B89\u88C5\u5DF2\u5B8C\u6210\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C\u672C\u547D\u4EE4\u4EE5\u52A0\u8F7D\u65B0\u7248\u672C ${pkgName}@${tag}\u3002
2477
+ `
2478
+ );
2479
+ } else {
2480
+ console.log(
2481
+ `
2482
+ [SILUZAN_AUTO_GLOBAL_INSTALL_FAILED] \u81EA\u52A8\u5168\u5C40\u5B89\u88C5\u5931\u8D25\uFF1A${installResult.stderr ?? "unknown"}
2483
+ \u8BF7\u624B\u52A8\u6267\u884C\uFF1Anpm install -g ${pkgName}@${tag}
2484
+ \u6216\uFF1A${binName} update
2485
+ ` + (forceUpdateExtra ? forceUpdateExtra + "\n" : "")
2486
+ );
2487
+ }
2438
2488
  return;
2439
2489
  }
2440
2490
  if (latest && isNewer(current, latest)) {
@@ -5925,6 +5975,11 @@ async function fetchJson(config, pathWithQuery, verbose) {
5925
5975
  function assertNever(x, ctx) {
5926
5976
  throw new Error(`${ctx}\uFF1A\u672A\u5904\u7406\u7684\u5206\u652F ${String(x)}`);
5927
5977
  }
5978
+ function rowsFromAccountDailyReportsEnvelope(raw, mediaCustomerId) {
5979
+ const block = raw.accounts?.[mediaCustomerId];
5980
+ const list = block?.data;
5981
+ return Array.isArray(list) ? list : [];
5982
+ }
5928
5983
  async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name) {
5929
5984
  switch (name) {
5930
5985
  case "overview":
@@ -5955,8 +6010,6 @@ async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name) {
5955
6010
  return fetchJson(config, fullPath, verbose);
5956
6011
  case "conversion-actions":
5957
6012
  return fetchJson(config, fullPath, verbose);
5958
- case "daily-metrics":
5959
- return fetchJson(config, fullPath, verbose);
5960
6013
  case "gold-account":
5961
6014
  return fetchJson(config, fullPath, verbose);
5962
6015
  case "ads-index":
@@ -6009,6 +6062,18 @@ async function fetchSectionPayload(def, opts, config, id) {
6009
6062
  const merged = { images, videos };
6010
6063
  return stripLegacyGoogleFieldsIfV2Present(merged);
6011
6064
  }
6065
+ if (def.name === "daily-metrics") {
6066
+ const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
6067
+ const params = new URLSearchParams({
6068
+ mediaCustomerIds: id,
6069
+ startDate: `${startDate}T00:00:00+08:00`,
6070
+ endDate: `${endDate}T23:59:59+08:00`
6071
+ });
6072
+ const url = `${config.apiBaseUrl}/report/media-account/google/account-daily-reports?${params.toString()}`;
6073
+ const raw = await apiFetch2(url, config, {}, !!opts.verbose);
6074
+ const rows = rowsFromAccountDailyReportsEnvelope(raw, id);
6075
+ return stripLegacyGoogleFieldsIfV2Present(rows);
6076
+ }
6012
6077
  const sectionPath = def.path(id);
6013
6078
  const query = buildSearchParams(def, opts.start, opts.end, extras);
6014
6079
  const data = await fetchGoogleAnalysisSectionJson(
@@ -6417,9 +6482,10 @@ var init_google_analysis2 = __esm({
6417
6482
  },
6418
6483
  {
6419
6484
  name: "daily-metrics",
6420
- description: "\u6309\u65E5\u6307\u6807\u66F2\u7EBF reports\uFF08\u542B\u8F6C\u5316\u6210\u672C\u7B49\uFF09",
6485
+ description: "\u6309\u65E5\u6307\u6807\uFF08\u4E3B\u5E73\u53F0 /report/media-account/google/account-daily-reports\uFF0C\u542B\u641C\u7D22\u4EFD\u989D\u7B49\uFF09",
6421
6486
  dateMode: "range",
6422
- path: (id) => `/reporting/media-account/${id}/reports`
6487
+ /** 仅用于 manifest endpointHint;实际请求走 fetchSectionPayload 专用分支(apiBaseUrl + 东八区起止时刻) */
6488
+ path: () => "/report/media-account/google/account-daily-reports"
6423
6489
  },
6424
6490
  {
6425
6491
  name: "gold-account",
@@ -14328,6 +14394,42 @@ init_auth();
14328
14394
  init_cli_json_snapshot();
14329
14395
  init_strip_legacy_google_fields();
14330
14396
  init_cli_table();
14397
+ function unwrapKeywordDisplayTextForEdit(raw) {
14398
+ const t = raw.trim();
14399
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) {
14400
+ return t.slice(1, -1);
14401
+ }
14402
+ if (t.length >= 2 && t.startsWith("[") && t.endsWith("]")) {
14403
+ return t.slice(1, -1);
14404
+ }
14405
+ return t;
14406
+ }
14407
+ function formatKeywordTextForMatchType(rawCoreOrDisplay, matchType) {
14408
+ const core = unwrapKeywordDisplayTextForEdit(rawCoreOrDisplay);
14409
+ switch (matchType) {
14410
+ case "Broad":
14411
+ return core;
14412
+ case "Phrase":
14413
+ return `"${core}"`;
14414
+ case "Exact":
14415
+ return `[${core}]`;
14416
+ default: {
14417
+ const _x = matchType;
14418
+ return _x;
14419
+ }
14420
+ }
14421
+ }
14422
+ function firstKeywordTextFromRecord(k) {
14423
+ const kt = k["keywordText"];
14424
+ if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
14425
+ return kt[0];
14426
+ }
14427
+ const t = k["text"];
14428
+ if (typeof t === "string") {
14429
+ return t;
14430
+ }
14431
+ return "";
14432
+ }
14331
14433
  async function runAdKeywords(opts) {
14332
14434
  const config = loadConfig(opts.token);
14333
14435
  const googleApiUrl = requireGoogleApi(config);
@@ -14519,8 +14621,17 @@ async function runAdKeywordEdit(opts) {
14519
14621
  process.exit(1);
14520
14622
  }
14521
14623
  const body = { ...keyword };
14522
- if (opts.text !== void 0) body["keywordText"] = [opts.text];
14523
- if (opts.matchType !== void 0) body["matchTypeV2"] = opts.matchType;
14624
+ if (opts.matchType !== void 0) {
14625
+ const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
14626
+ if (!String(base).trim()) {
14627
+ console.error("\n\u274C \u65E0\u6CD5\u89E3\u6790\u5F53\u524D\u5173\u952E\u8BCD\u6587\u6848\uFF0C\u8BF7\u540C\u65F6\u4F20 --text <\u8BCD\u5E72>\n");
14628
+ process.exit(1);
14629
+ }
14630
+ body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
14631
+ body["matchTypeV2"] = opts.matchType;
14632
+ } else if (opts.text !== void 0) {
14633
+ body["keywordText"] = [opts.text];
14634
+ }
14524
14635
  if (opts.maxCpc !== void 0) body["maxCPC"] = opts.maxCpc;
14525
14636
  if (opts.finalUrl !== void 0) body["finalURL"] = opts.finalUrl;
14526
14637
  const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
@@ -14567,8 +14678,17 @@ async function runAdNegativeKeywordEdit(opts) {
14567
14678
  process.exit(1);
14568
14679
  }
14569
14680
  const body = { ...keyword };
14570
- if (opts.text !== void 0) body["keywordText"] = [opts.text];
14571
- if (opts.matchType !== void 0) body["matchTypeV2"] = opts.matchType;
14681
+ if (opts.matchType !== void 0) {
14682
+ const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
14683
+ if (!String(base).trim()) {
14684
+ console.error("\n\u274C \u65E0\u6CD5\u89E3\u6790\u5F53\u524D\u5426\u5B9A\u5173\u952E\u8BCD\u6587\u6848\uFF0C\u8BF7\u540C\u65F6\u4F20 --text <\u8BCD\u5E72>\n");
14685
+ process.exit(1);
14686
+ }
14687
+ body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
14688
+ body["matchTypeV2"] = opts.matchType;
14689
+ } else if (opts.text !== void 0) {
14690
+ body["keywordText"] = [opts.text];
14691
+ }
14572
14692
  const url = `${googleApiUrl}/negativekeywordmanagement/negativekeyword/${opts.account}/${opts.id}`;
14573
14693
  try {
14574
14694
  await apiFetch2(url, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
@@ -15870,7 +15990,10 @@ function register20(program2) {
15870
15990
  });
15871
15991
  }
15872
15992
  );
15873
- adCmd.command("keyword-edit").description("\u7F16\u8F91\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u6570\u7EC4 body\uFF0C\u5148 list \u518D\u5408\u5E76\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C\uFF08\u5199\u5165 keywordText \u6570\u7EC4\uFF09").option("--match-type <type>", "\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199\u5165 matchTypeV2\uFF09").option(
15993
+ adCmd.command("keyword-edit").description("\u7F16\u8F91\u641C\u7D22\u5173\u952E\u8BCD\uFF08\u6570\u7EC4 body\uFF0C\u5148 list \u518D\u5408\u5E76\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C\uFF08\u5199\u5165 keywordText \u6570\u7EC4\uFF09").option(
15994
+ "--match-type <type>",
15995
+ '\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199 matchTypeV2\uFF0C\u5E76\u9ED8\u8BA4\u540C\u6B65\u6539\u5199 keywordText \u4E3A\u8BCD\u5E72/"\u8BCD"/[\u8BCD] \u4EE5\u7B26\u5408\u7F51\u5173\u63A8\u65AD\uFF09'
15996
+ ).option(
15874
15997
  "--max-cpc <n>",
15875
15998
  "\u6700\u9AD8\u6BCF\u6B21\u70B9\u51FB\u8D39\u7528 maxCPC\uFF0C\u4E3B\u5E01\u79CD\u91D1\u989D\uFF08\u5982 5 \u8868\u793A \xA55\uFF1B\u26A0\uFE0F \u8FD9\u4E2A\u5B57\u6BB5\u540E\u7AEF\u5355\u4F4D\u5C31\u662F\u300C\u4E3B\u5E01\u79CD\u5143\u300D\uFF0CCLI \u76F4\u63A5\u900F\u4F20\u4E0D\u505A \xD7100\uFF0C\u4E0E budget / \u7EC4 maxCPCAmount \u4E0D\u540C\uFF1B0 \u8868\u793A\u6309\u5E73\u53F0/\u8BA1\u5212\u9ED8\u8BA4\uFF09"
15876
15999
  ).option("--final-url <url>", "\u5173\u952E\u8BCD\u7EA7\u6700\u7EC8\u5230\u8FBE\u7F51\u5740 finalURL").option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
@@ -15901,7 +16024,10 @@ function register20(program2) {
15901
16024
  });
15902
16025
  }
15903
16026
  );
15904
- adCmd.command("keyword-negative-edit").description("\u7F16\u8F91\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u6587\u672C\u6216\u5339\u914D\u7C7B\u578B\uFF0C\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5426\u5B9A\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --negative --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C").option("--match-type <type>", "\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact").option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
16027
+ adCmd.command("keyword-negative-edit").description("\u7F16\u8F91\u5426\u5B9A\u5173\u952E\u8BCD\uFF08\u6587\u672C\u6216\u5339\u914D\u7C7B\u578B\uFF0C\u81F3\u5C11\u4F20\u4E00\u4E2A\u4FEE\u6539\u9879\uFF09").requiredOption("-a, --account <id>", "Google \u8D26\u6237 mediaCustomerId").requiredOption("--id <keywordId>", "\u5426\u5B9A\u5173\u952E\u8BCD ID\uFF08\u6765\u81EA ad keywords --negative --json \u2192 id\uFF09").option("--text <text>", "\u65B0\u5173\u952E\u8BCD\u6587\u672C").option(
16028
+ "--match-type <type>",
16029
+ '\u65B0\u5339\u914D\u7C7B\u578B\uFF1ABroad | Phrase | Exact\uFF08\u5199 matchTypeV2\uFF0C\u5E76\u9ED8\u8BA4\u540C\u6B65\u6539\u5199 keywordText \u4E3A\u8BCD\u5E72/"\u8BCD"/[\u8BCD]\uFF09'
16030
+ ).option("--start <date>", "\u5217\u8868\u67E5\u8BE2\u8D77\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u5217\u8868\u67E5\u8BE2\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-t, --token <token>", "Auth Token").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(
15905
16031
  async (opts) => {
15906
16032
  if (opts.matchType && !["Broad", "Phrase", "Exact"].includes(opts.matchType)) {
15907
16033
  console.error("\n\u274C --match-type \u53EA\u63A5\u53D7 Broad | Phrase | Exact\n");
@@ -16403,7 +16529,7 @@ async function runAccountShareDetail(opts) {
16403
16529
 
16404
16530
  // src/commands/account-manage/oauth.ts
16405
16531
  init_auth();
16406
- import { spawn } from "child_process";
16532
+ import { spawn as spawn2 } from "child_process";
16407
16533
  function toApiMediaType(media) {
16408
16534
  return media === "Meta" ? "FacebookAds" : media;
16409
16535
  }
@@ -16414,17 +16540,17 @@ function tryOpenBrowser(url) {
16414
16540
  return false;
16415
16541
  }
16416
16542
  if (process.platform === "win32") {
16417
- const child = spawn("rundll32", ["url.dll,FileProtocolHandler", parsed.toString()], {
16543
+ const child = spawn2("rundll32", ["url.dll,FileProtocolHandler", parsed.toString()], {
16418
16544
  detached: true,
16419
16545
  stdio: "ignore",
16420
16546
  windowsHide: true
16421
16547
  });
16422
16548
  child.unref();
16423
16549
  } else if (process.platform === "darwin") {
16424
- const child = spawn("open", [parsed.toString()], { detached: true, stdio: "ignore" });
16550
+ const child = spawn2("open", [parsed.toString()], { detached: true, stdio: "ignore" });
16425
16551
  child.unref();
16426
16552
  } else {
16427
- const child = spawn("xdg-open", [parsed.toString()], { detached: true, stdio: "ignore" });
16553
+ const child = spawn2("xdg-open", [parsed.toString()], { detached: true, stdio: "ignore" });
16428
16554
  child.unref();
16429
16555
  }
16430
16556
  return true;
@@ -26,7 +26,7 @@ allowed-tools: Bash(siluzan-tso:*) Read Write
26
26
 
27
27
  Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令时存在兼容性问题。若上述命令异常失败,请先安装 [Git for Windows](https://git-scm.com/download/win),然后在 Git Bash 中执行 macOS / Linux / WSL 的 Bash 安装命令。
28
28
 
29
- 参照 `references/setup.md` 完成安装与配置。
29
+ 参照 `references/setup.md` 完成安装与配置。**需登录或我方 401 换凭据时,优先引导「手机号 + 验证码」**(`send-login-code` → `login --phone --code`),详见 `references/setup.md` 登录优先级说明。
30
30
 
31
31
  ---
32
32
 
@@ -34,7 +34,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
34
34
 
35
35
  | 文档 | 功能 |
36
36
  |------|------|
37
- | `references/setup.md` | 安装、登录、配置、环境切换、更新 |
37
+ | `references/setup.md` | 安装、**登录(手机验证码优先)**、配置、环境切换、更新 |
38
38
  | `references/workflows.md` | 多步骤业务流程、跨命令串联 |
39
39
  | `references/tips.md` | **Agent 拉数一律 `--json-out`**(目录或 `*.json` 文件)+ 读 `cli-manifest[-<查询id>].json` / 各 `<section>[-<查询id>].json` + **`*.outline.txt`**(TS 式类型,**几百字节,描述完整字段结构**——写聚合脚本前先读它而不是 `Read` 整个 JSON,省 2~3 个数量级 context);stdout 一行摘要含 `manifestFile` / `writtenFiles` / `outlineFile` 等 |
40
40
  | `references/accounts.md` | 账户列表、余额、消耗、开户记录、授权/解绑/分享/MCC/BC/BM/邮箱授权 |
@@ -147,7 +147,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
147
147
  2. 中断后**必须**用 `resume --run-id <id>` 续跑,**禁止**重新 `run`。
148
148
  3. stdout 始终是单行 JSON(`kind=siluzan-tso-batch-summary`);进度读 `progress.json`、轨迹读 `state/tasks.jsonl`。
149
149
  4. 退出码:`0` 全成功 / `2` 部分成功 / `3` 全失败或 Token 失效 / `4` 用法错误。
150
- 5. 401 响应 → 整批终止 + `tokenInvalidated:true`,提示用户重新登录`references/setup.md` 后再 resume
150
+ 5. 401 响应 → 整批终止 + `tokenInvalidated:true`,提示用户按 `references/setup.md` **优先手机验证码**重新登录后再 `resume`。
151
151
 
152
152
  若无批量命令(如 117 个 Bing 账户剩余天数计算):先 `list-accounts --json-out <dir>` 一次性拿全量 → `node -e` 本地计算 → 只对命中账户做后续操作。
153
153
 
@@ -244,7 +244,7 @@ siluzan-tso accounts-digest -m Google -a id1,id2,... --start <S> --end <D> --jso
244
244
  ### 常见 HTTP 状态码
245
245
 
246
246
  - **400**:参数错误,查看对应 reference 或用 `-h` 了解命令用法
247
- - **401**:平台方返回则需用户重新授权;我方返回则让用户执行 `siluzan-tso login`
247
+ - **401**:平台方返回则需用户重新授权;**我方凭据失效**则优先 **`send-login-code` + `login --phone --code`**(或 TTY 下 `siluzan-tso login` / `config set …`),见 `references/setup.md`
248
248
  - **500**:服务可能正在部署/升级,建议提交给 Siluzan 相关人员
249
249
 
250
250
  ### 报告模板外部资源
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.18-beta.5",
4
- "publishedAt": 1778322175468
3
+ "version": "1.1.18-beta.7",
4
+ "publishedAt": 1778465751958
5
5
  }
@@ -33,10 +33,10 @@
33
33
  2. 确定报告维度(默认含:执行摘要、每日趋势、月度汇总、系列表现、设备分布、地域分布、关键词表现、优化建议),详见 `report-templates/README.md`。
34
34
  3. **拉数**:使用 `google-analysis … --json-out <dir>`(Google)或对应 `report <media>-*` 命令落盘。
35
35
  4. **编写并执行代码**从磁盘读取 `manifest-<accountId>.json` 与各 `<section>-<accountId>.json` 来完成筛选、聚合、排序等计算;**禁止**用 `Read` 看 JSON 后在对话里心算或手填报告数字。
36
- - **写脚本前先读 `<section>-<accountId>.outline.txt`**(与 JSON 同 stem 的纯文本,最后一行是 TS 式类型字面量)了解字段结构;它体积只有几百字节、不含数据,比直接 `Read` 整个 `*.json`(动辄几 MB / 几万行)省**两到三个数量级**的上下文。**注意是 `.outline.txt` 不是 `.outline.json`**,不要 `require()`,用 `fs.readFileSync(outlineFile,'utf8')` 读最后一行即可。
37
- - 真实数据始终从 `<section>-<accountId>.json` 由脚本读,**不要**把 outline 当作业务数据贴给用户。
38
- 5. **由代码写出最终文件**(HTML/Excel/PDF/PPT/Markdown 等)。**禁止**在报告脚本中以源码字面量写死应从 JSON 读取的业务数据(消耗金额、系列名、日期区间等)。允许的常量仅限:快照目录路径、JSON 字段键名、版式/结构占位。
39
- 6. **报告首行**须标注:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`,由脚本从 JSON 拼接写入,不得手写常量冒充。
36
+ - **写脚本前先读 `<section>-<accountId>.outline.txt`**(与 JSON 同 stem 的纯文本,最后一行是 TS 式类型字面量)了解字段结构;它体积只有几百字节、不含数据,比直接 `Read` 整个 `*.json`(动辄几 MB / 几万行)省**两到三个数量级**的上下文,不要 `require()`,用 `fs.readFileSync(outlineFile,'utf8')` 读最后一行即可。
37
+ - 真实数据始终从 `<section>-<accountId>.json` 通过代码获取,**不要**把 outline 当作业务数据贴给用户。
38
+ 5. **由代码写出最终文件**(HTML/Excel/PDF/PPT/Markdown/word 等)。**禁止**在报告脚本中以源码字面量写死应从 JSON 读取的业务数据(消耗金额、系列名、日期区间等)。允许的常量仅限:快照目录路径、JSON 字段键名、版式/结构占位。
39
+ 6. **报告首行**须标注:`统计区间:YYYY-MM-DD ~ YYYY-MM-DD(货币:XXX)`
40
40
  7. 交付后帮用户打开报告文件。
41
41
 
42
42
  ---
@@ -139,7 +139,7 @@ siluzan-tso google-analysis -a <id> --exclude materials,gold-account --json-out
139
139
  | `materials` | 合并图片+视频 `{ images, videos }` |
140
140
  | `resource-counts` | 结构统计 |
141
141
  | `conversion-actions` | 转化动作 |
142
- | `daily-metrics` | 按日报表 |
142
+ | `daily-metrics` | 按日指标(主平台 `GET …/report/media-account/google/account-daily-reports`,`--json` 根为按日数组) |
143
143
  | `gold-account` | 黄金账户 |
144
144
  | `ads-index` | 质量指标 |
145
145
  | `final-urls` | 最终到达网址(不传 `--start`/`--end`) |
@@ -1,7 +1,5 @@
1
1
  # 账户管理命令详解
2
2
 
3
- > 所属 skill:`siluzan-tso`。通用选项 `--token <token>` 可覆盖配置文件中的 Token(通常无需传,直接使用 `siluzan-tso login` 保存的配置)。
4
-
5
3
  ---
6
4
 
7
5
  ## list-accounts — 查询广告账户列表
@@ -980,6 +980,8 @@ siluzan-tso ad keyword-delete -a 6326027735 --id 2464982882313 --adgroup-id 1955
980
980
 
981
981
  **约束:** `--text`、`--match-type`、`--max-cpc`、`--final-url` 至少传一项。
982
982
 
983
+ **匹配类型与文案:** Google 网关 V2 根据 `keywordText` 上的 `"` / `[` `]` 推断实际 MatchType(会覆盖仅传的 `matchTypeV2`)。因此只要传 `--match-type`,CLI **默认**把 `keywordText` 规范为词干 / `"词干"` / `[词干]` 并写入 `matchTypeV2`,无需额外开关;仅改匹配时可不传 `--text`(用列表里的当前文案去外层括号后再包一层)。
984
+
983
985
  ```bash
984
986
  siluzan-tso ad keyword-edit \
985
987
  -a <accountId> \
@@ -1009,6 +1011,8 @@ siluzan-tso ad keyword-edit -a 6326027735 --id 2081924039951 \
1009
1011
 
1010
1012
  ## ad keyword-negative-edit — 否词编辑
1011
1013
 
1014
+ 与搜索词相同:传 `--match-type` 时 CLI 会默认同步改写 `keywordText` 外层括号/引号。
1015
+
1012
1016
  ```bash
1013
1017
  siluzan-tso ad keyword-negative-edit \
1014
1018
  -a <accountId> \
@@ -40,8 +40,11 @@ siluzan-tso init -d /path/to-your/skills # 写入自定义目录
40
40
 
41
41
  `siluzan-tso` 与 `siluzan-cso` **共用同一份凭据**,存储在 `~/.siluzan/config.json`,配置一次两个 CLI 均可使用。
42
42
 
43
+ > **登录方式优先级**
44
+ > 1. **首选**:**手机号 + 短信验证码**两段式(`send-login-code` → `login --phone --code`)——无 TTY 不卡死、不依赖浏览器里复制 API Key,**对话式 AI / OpenClaw / CI 日志旁路**均适用。
43
45
 
44
- ### 通过手机号 + 验证码登录(对话式 AI 推荐)
46
+
47
+ ### 通过手机号 + 验证码登录(**首选**;对话式 AI / 无 TTY 与各 Agent 环境)
45
48
 
46
49
  **两段式调用**,专为 AI Agent 设计——任何一步都不会进入交互等待,绝不会卡住 stdout。
47
50
  拆分后单一职责:第 1 步只发码;第 2 步只用 code 换 API Key。这样 Agent 不会因为"看到 stdout 卡住就重试"而触发短信轰炸。
@@ -51,13 +54,12 @@ siluzan-tso init -d /path/to-your/skills # 写入自定义目录
51
54
  | 1 | `siluzan-tso send-login-code --phone <手机号>` | 仅向手机发送 6 位验证码 |
52
55
  | 2 | `siluzan-tso login --phone <手机号> --code <验证码>` | 用 code 完成登录并自动签发 API Key 写入 `~/.siluzan/config.json` |
53
56
 
54
- ## 其他方式登录
57
+ ## 其它登录方式(TTY 交互 / 已有 API Key / JWT)
58
+
55
59
  ```bash
56
- siluzan-tso login # 交互式登录,按提示创建 API Key 后粘贴
60
+ siluzan-tso login # 交互式登录(需 TTY),按提示创建 API Key 后粘贴
57
61
  siluzan-tso login --api-key <YOUR_API_KEY> # 直接设置 API Key(跳过交互)
58
- siluzan-tso send-login-code --phone 138xxxx # 两段式登录第 1 步:发送短信验证码
59
- siluzan-tso login --phone 138xxxx --code 123456 # 两段式登录第 2 步:用验证码完成登录
60
- siluzan-tso config set --api-key <Key> # 或通过 config set 直接写入
62
+ siluzan-tso config set --api-key <Key> # config 直接写入
61
63
  siluzan-tso config set --token <Token> # 备用:设置 JWT Token
62
64
  ```
63
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.18-beta.5",
3
+ "version": "1.1.18-beta.7",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",