siluzan-tso-cli 1.1.18-beta.6 → 1.1.18-beta.8

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.6),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
54
+ > **注意**:当前为测试版(1.1.18-beta.8),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
55
55
 
56
56
  | 助手 | 建议 `--ai` |
57
57
  | ----------------------- | ------------------------------------ |
package/dist/index.js CHANGED
@@ -2412,23 +2412,28 @@ function createVersionNotifier(opts) {
2412
2412
  mergeWriteConfig,
2413
2413
  readConfigRaw
2414
2414
  } = opts;
2415
- const KEY_LAST_CHECK = `${cachePrefix}LastVersionCheck`;
2416
2415
  const KEY_LATEST_STABLE = `${cachePrefix}LatestStable`;
2417
2416
  const KEY_LATEST_BETA = `${cachePrefix}LatestBeta`;
2418
2417
  const KEY_MIN_STABLE = `${cachePrefix}MinRequiredStable`;
2419
2418
  const KEY_MIN_BETA = `${cachePrefix}MinRequiredBeta`;
2420
2419
  const KEY_LAST_NOTIFIED = `${cachePrefix}LastNotified`;
2420
+ const KEY_FETCH_AT_MAIN = `${cachePrefix}VersionFetchAtMain`;
2421
+ const KEY_FETCH_AT_MIN = `${cachePrefix}VersionFetchAtMin`;
2421
2422
  const HOURS_24 = 24 * 60 * 60 * 1e3;
2422
- async function fetchVersionByTag(tag, cacheKey, cfg) {
2423
- const lastCheck = cfg[KEY_LAST_CHECK];
2424
- if (typeof lastCheck === "string" && cacheKey in cfg) {
2425
- const lastMs = new Date(lastCheck).getTime();
2426
- 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) {
2427
2430
  const v = cfg[cacheKey];
2428
- return typeof v === "string" && v ? v : null;
2431
+ const sv = typeof v === "string" && v ? v : null;
2432
+ return { version: sv, hitNetwork: false };
2429
2433
  }
2430
2434
  }
2431
- return fetchNpmVersion(pkgName, tag);
2435
+ const version = await fetchNpmVersion(pkgName, tag);
2436
+ return { version, hitNetwork: true };
2432
2437
  }
2433
2438
  async function notifyIfOutdated2() {
2434
2439
  try {
@@ -2439,15 +2444,20 @@ function createVersionNotifier(opts) {
2439
2444
  const minCacheKey = isBeta ? KEY_MIN_BETA : KEY_MIN_STABLE;
2440
2445
  const minTag = npmMinRequiredTagForBuildEnv(isBeta ? "test" : "production");
2441
2446
  const cfg = readConfigRaw();
2442
- const [latest, minRequired] = await Promise.all([
2443
- fetchVersionByTag(tag, latestCacheKey, cfg),
2444
- 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)
2445
2450
  ]);
2446
- await mergeWriteConfig({
2447
- [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 = {
2448
2455
  [latestCacheKey]: latest ?? "",
2449
2456
  [minCacheKey]: minRequired ?? ""
2450
- });
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);
2451
2461
  const lastNotified = typeof cfg[KEY_LAST_NOTIFIED] === "string" ? new Date(cfg[KEY_LAST_NOTIFIED]).getTime() : 0;
2452
2462
  if (Date.now() - lastNotified < HOURS_24) return;
2453
2463
  const tagLabel = isBeta ? "\uFF08\u6D4B\u8BD5\u7248\uFF09" : "\uFF08\u6B63\u5F0F\u7248\uFF09";
@@ -2549,16 +2559,11 @@ function printAuthMissingHelp(binName) {
2549
2559
  `
2550
2560
  \u274C \u672A\u627E\u5230\u8BA4\u8BC1\u51ED\u636E\u3002\u8BF7\u9009\u62E9\u4EE5\u4E0B\u4EFB\u610F\u4E00\u79CD\u65B9\u5F0F\uFF1A
2551
2561
 
2552
- \u65B9\u5F0F\u4E00\uFF08\u63A8\u8350\uFF09\uFF1AAPI Key
2553
- \u5728\u4E1D\u8DEF\u8D5E\u300C\u8BBE\u7F6E \u2192 API Key \u7BA1\u7406\u300D\u521B\u5EFA API Key\uFF0C\u7136\u540E\u8FD0\u884C\uFF1A
2554
- ${binName} login --api-key <YOUR_API_KEY>
2555
-
2556
- \u65B9\u5F0F\u4E8C\uFF1A\u73AF\u5883\u53D8\u91CF\uFF08CI/CD \u63A8\u8350\uFF09
2557
- export SILUZAN_API_KEY=<YOUR_API_KEY>
2558
- # \u6216 export SILUZAN_AUTH_TOKEN=<YOUR_TOKEN>
2559
-
2560
- \u65B9\u5F0F\u4E09\uFF1AJWT Token
2561
- ${binName} login
2562
+ \u8BF7\u4F7F\u7528\u624B\u673A\u53F7\u91CD\u65B0\u767B\u5F55
2563
+ ${binName} send-login-code --phone <YOUR_PHONE>
2564
+ `,
2565
+ `\u7136\u540E\u4F7F\u7528\u6536\u5230\u7684\u9A8C\u8BC1\u7801\u5B8C\u6210\u767B\u5F55
2566
+ ${binName} login --phone <YOUR_PHONE> --code <YOUR_CODE>
2562
2567
  `
2563
2568
  );
2564
2569
  process.exit(1);
@@ -3050,6 +3055,35 @@ function normalizeChinaPhone(input) {
3050
3055
  function isValidChinaPhone(input) {
3051
3056
  return /^\+861\d{10}$/.test(normalizeChinaPhone(input));
3052
3057
  }
3058
+ async function sendPhoneLoginCode(opts) {
3059
+ const phone = normalizeChinaPhone(opts.phone);
3060
+ const url = `${opts.ssoBaseUrl}/Account/SendVaildCode?Phone=${encodeURIComponent(
3061
+ phone
3062
+ )}&RandStr=&Iicket=`;
3063
+ if (opts.verbose) {
3064
+ process.stderr.write(`[phone-login] GET ${url}
3065
+ `);
3066
+ }
3067
+ const res = await rawRequest(url, {
3068
+ method: "GET",
3069
+ headers: {
3070
+ Accept: "application/json",
3071
+ "Accept-Language": "zh-CN"
3072
+ }
3073
+ });
3074
+ if (res.status < 200 || res.status >= 300) {
3075
+ return { ok: false, message: `HTTP ${res.status}` };
3076
+ }
3077
+ let body;
3078
+ try {
3079
+ body = JSON.parse(res.text);
3080
+ } catch {
3081
+ return { ok: false, message: `\u54CD\u5E94\u975E JSON\uFF1A${res.text.slice(0, 120)}` };
3082
+ }
3083
+ const state = (body.State ?? body.state ?? "").toLowerCase();
3084
+ const message = body.Message ?? body.message ?? "";
3085
+ return { ok: state === "ok", message };
3086
+ }
3053
3087
  async function loginByPhoneCode(opts) {
3054
3088
  const phone = normalizeChinaPhone(opts.phone);
3055
3089
  const url = `${opts.ssoBaseUrl}/Account/LoginByMiniCode?phone=${encodeURIComponent(
@@ -5965,6 +5999,11 @@ async function fetchJson(config, pathWithQuery, verbose) {
5965
5999
  function assertNever(x, ctx) {
5966
6000
  throw new Error(`${ctx}\uFF1A\u672A\u5904\u7406\u7684\u5206\u652F ${String(x)}`);
5967
6001
  }
6002
+ function rowsFromAccountDailyReportsEnvelope(raw, mediaCustomerId) {
6003
+ const block = raw.accounts?.[mediaCustomerId];
6004
+ const list = block?.data;
6005
+ return Array.isArray(list) ? list : [];
6006
+ }
5968
6007
  async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name) {
5969
6008
  switch (name) {
5970
6009
  case "overview":
@@ -5995,8 +6034,6 @@ async function fetchGoogleAnalysisSectionJson(config, fullPath, verbose, name) {
5995
6034
  return fetchJson(config, fullPath, verbose);
5996
6035
  case "conversion-actions":
5997
6036
  return fetchJson(config, fullPath, verbose);
5998
- case "daily-metrics":
5999
- return fetchJson(config, fullPath, verbose);
6000
6037
  case "gold-account":
6001
6038
  return fetchJson(config, fullPath, verbose);
6002
6039
  case "ads-index":
@@ -6049,6 +6086,18 @@ async function fetchSectionPayload(def, opts, config, id) {
6049
6086
  const merged = { images, videos };
6050
6087
  return stripLegacyGoogleFieldsIfV2Present(merged);
6051
6088
  }
6089
+ if (def.name === "daily-metrics") {
6090
+ const { startDate, endDate } = resolveDateRange2(opts.start, opts.end);
6091
+ const params = new URLSearchParams({
6092
+ mediaCustomerIds: id,
6093
+ startDate: `${startDate}T00:00:00+08:00`,
6094
+ endDate: `${endDate}T23:59:59+08:00`
6095
+ });
6096
+ const url = `${config.apiBaseUrl}/report/media-account/google/account-daily-reports?${params.toString()}`;
6097
+ const raw = await apiFetch2(url, config, {}, !!opts.verbose);
6098
+ const rows = rowsFromAccountDailyReportsEnvelope(raw, id);
6099
+ return stripLegacyGoogleFieldsIfV2Present(rows);
6100
+ }
6052
6101
  const sectionPath = def.path(id);
6053
6102
  const query = buildSearchParams(def, opts.start, opts.end, extras);
6054
6103
  const data = await fetchGoogleAnalysisSectionJson(
@@ -6457,9 +6506,10 @@ var init_google_analysis2 = __esm({
6457
6506
  },
6458
6507
  {
6459
6508
  name: "daily-metrics",
6460
- description: "\u6309\u65E5\u6307\u6807\u66F2\u7EBF reports\uFF08\u542B\u8F6C\u5316\u6210\u672C\u7B49\uFF09",
6509
+ description: "\u6309\u65E5\u6307\u6807\uFF08\u4E3B\u5E73\u53F0 /report/media-account/google/account-daily-reports\uFF0C\u542B\u641C\u7D22\u4EFD\u989D\u7B49\uFF09",
6461
6510
  dateMode: "range",
6462
- path: (id) => `/reporting/media-account/${id}/reports`
6511
+ /** 仅用于 manifest endpointHint;实际请求走 fetchSectionPayload 专用分支(apiBaseUrl + 东八区起止时刻) */
6512
+ path: () => "/report/media-account/google/account-daily-reports"
6463
6513
  },
6464
6514
  {
6465
6515
  name: "gold-account",
@@ -6594,17 +6644,38 @@ function validateAndNormalizePhone(rawInput) {
6594
6644
  }
6595
6645
  return normalizeChinaPhone(rawPhone);
6596
6646
  }
6597
- async function runPhoneLogin(opts) {
6647
+ async function runSendLoginCode(opts) {
6598
6648
  const phone = validateAndNormalizePhone(opts.phone);
6599
- const code = opts.code?.trim() ?? "";
6600
- if (!code) {
6601
- console.error("\n\u274C \u7F3A\u5C11 --code \u53C2\u6570\u3002\u624B\u673A\u53F7\u767B\u5F55\u662F\u4E24\u6BB5\u5F0F\u8C03\u7528\uFF0C\u8BF7\u6309\u987A\u5E8F\u6267\u884C\uFF1A\n");
6602
- console.error(` 1) siluzan-tso send-login-code --phone ${phone}`);
6603
- console.error(` 2) siluzan-tso login --phone ${phone} --code <\u6536\u5230\u76846\u4F4D\u9A8C\u8BC1\u7801>
6649
+ const tsoApiBase = process.env.SILUZAN_TSO_API_BASE ?? DEFAULT_API_BASE;
6650
+ const ssoBaseUrl = deriveSsoBaseUrl(tsoApiBase);
6651
+ console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6652
+ console.log(" Siluzan \u53D1\u9001\u767B\u5F55\u77ED\u4FE1\u9A8C\u8BC1\u7801");
6653
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n");
6654
+ console.log(` \u624B\u673A\u53F7 : ${phone}`);
6655
+ console.log("\u2192 \u6B63\u5728\u5411\u624B\u673A\u53D1\u9001\u9A8C\u8BC1\u7801...");
6656
+ const r = await sendPhoneLoginCode({ ssoBaseUrl, phone, verbose: opts.verbose });
6657
+ if (!r.ok) {
6658
+ console.error(`
6659
+ \u274C \u77ED\u4FE1\u9A8C\u8BC1\u7801\u53D1\u9001\u5931\u8D25\uFF1A${r.message || "(\u540E\u7AEF\u672A\u8FD4\u56DE\u539F\u56E0)"}
6604
6660
  `);
6605
- console.error("\uFF08\u62C6\u6210\u4E24\u6B65\u662F\u4E3A\u4E86\u907F\u514D AI Agent \u5361\u5728\u4EA4\u4E92\u8F93\u5165\u65F6\u53CD\u590D\u91CD\u8BD5\u5BFC\u81F4\u77ED\u4FE1\u8F70\u70B8\uFF09\n");
6606
6661
  process.exit(1);
6607
6662
  }
6663
+ console.log(`\u2713 \u9A8C\u8BC1\u7801\u5DF2\u53D1\u9001\uFF0810 \u5206\u949F\u5185\u6709\u6548\uFF09\u3002
6664
+ `);
6665
+ console.log("\u4E0B\u4E00\u6B65\uFF1A\u62FF\u5230 6 \u4F4D\u9A8C\u8BC1\u7801\u540E\uFF0C\u8FD0\u884C\u4E0B\u9762\u7684\u547D\u4EE4\u5B8C\u6210\u767B\u5F55\uFF1A\n");
6666
+ console.log(` siluzan-tso login --phone ${phone} --code <6\u4F4D\u9A8C\u8BC1\u7801>
6667
+ `);
6668
+ console.log("\u53EF\u9009\u53C2\u6570\uFF1A--name / --valid-days / --expires-at / --services");
6669
+ console.log("\uFF08\u9ED8\u8BA4\u521B\u5EFA 90 \u5929\u6709\u6548\u3001\u52FE\u9009 TSO + CUT \u670D\u52A1\u7684 API Key\uFF09\n");
6670
+ }
6671
+ function normalizeBearerTokenInput(raw) {
6672
+ const t = raw.trim();
6673
+ if (/^bearer\s+/i.test(t)) {
6674
+ return t.replace(/^bearer\s+/i, "").trim();
6675
+ }
6676
+ return t;
6677
+ }
6678
+ async function executePhoneLoginWithVerifiedCode(phone, code, opts) {
6608
6679
  let allowedServices;
6609
6680
  try {
6610
6681
  allowedServices = parseAllowedServices(opts.services);
@@ -6676,9 +6747,10 @@ async function runPhoneLogin(opts) {
6676
6747
  `
6677
6748
  \u8BE5\u624B\u673A\u53F7\u5C1A\u672A\u6CE8\u518C\u4E1D\u8DEF\u8D5E\u8D26\u53F7\u3002\u8BF7\u5148\u5728\u7F51\u9875\u7AEF\u6CE8\u518C\uFF1A
6678
6749
  ${WEB_BASE_URL}
6679
- \u6CE8\u518C\u6210\u529F\u540E\u518D\u56DE\u5230 CLI \u91CD\u8BD5\u4E24\u6BB5\u5F0F\u767B\u5F55\uFF1A
6750
+ \u6CE8\u518C\u6210\u529F\u540E\u518D\u56DE\u5230 CLI \u91CD\u8BD5\uFF1A
6680
6751
  siluzan-tso send-login-code --phone ${phone}
6681
6752
  siluzan-tso login --phone ${phone} --code <6\u4F4D\u9A8C\u8BC1\u7801>
6753
+ \u6216\u5728\u7EC8\u7AEF\u6267\u884C\u65E0\u53C2 siluzan-tso login \u540E\u9009\u62E9\u300C\u624B\u673A\u53F7\u767B\u5F55\u300D\u3002
6682
6754
  `
6683
6755
  );
6684
6756
  } else if (/验证码错误|验证码/.test(msg)) {
@@ -6692,34 +6764,72 @@ async function runPhoneLogin(opts) {
6692
6764
  process.exit(1);
6693
6765
  }
6694
6766
  }
6695
- async function runLogin(opts = {}) {
6696
- if (opts.apiKey !== void 0) {
6697
- const key = opts.apiKey.trim();
6698
- if (!key) {
6699
- console.error("\n\u274C API Key \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
6767
+ async function runPhoneLogin(opts) {
6768
+ const phone = validateAndNormalizePhone(opts.phone);
6769
+ const code = opts.code?.trim() ?? "";
6770
+ if (!code) {
6771
+ console.error("\n\u274C \u7F3A\u5C11 --code \u53C2\u6570\u3002\u624B\u673A\u53F7\u767B\u5F55\u662F\u4E24\u6BB5\u5F0F\u8C03\u7528\uFF0C\u8BF7\u6309\u987A\u5E8F\u6267\u884C\uFF1A\n");
6772
+ console.error(` 1) siluzan-tso send-login-code --phone ${phone}`);
6773
+ console.error(` 2) siluzan-tso login --phone ${phone} --code <\u6536\u5230\u76846\u4F4D\u9A8C\u8BC1\u7801>
6774
+ `);
6775
+ console.error("\uFF08\u62C6\u6210\u4E24\u6B65\u662F\u4E3A\u4E86\u907F\u514D AI Agent \u5361\u5728\u4EA4\u4E92\u8F93\u5165\u65F6\u53CD\u590D\u91CD\u8BD5\u5BFC\u81F4\u77ED\u4FE1\u8F70\u70B8\uFF09\n");
6776
+ console.error("\u6216\u5728\u7EC8\u7AEF\u6267\u884C\uFF1Asiluzan-tso login\uFF08\u65E0\u53C2\u6570\uFF09\uFF0C\u9009\u62E9\u300C\u624B\u673A\u53F7 + \u77ED\u4FE1\u9A8C\u8BC1\u7801\u300D\u7531\u672C CLI \u53D1\u7801\u3002\n");
6777
+ process.exit(1);
6778
+ }
6779
+ await executePhoneLoginWithVerifiedCode(phone, code, opts);
6780
+ }
6781
+ async function runInteractiveJwtLogin() {
6782
+ const shared = readSharedConfig();
6783
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6784
+ const prompt = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
6785
+ try {
6786
+ if (shared.apiKey) {
6787
+ console.log(`
6788
+ \u5DF2\u4FDD\u5B58 API Key\uFF08${maskSecret(shared.apiKey)}\uFF09\u3002\u6539\u7528 JWT \u5C06\u6E05\u9664 API Key \u5E76\u4F18\u5148\u4F7F\u7528 Bearer Token\u3002`);
6789
+ const ans = await prompt("\u662F\u5426\u7EE7\u7EED\uFF1F(y/N) ");
6790
+ if (ans.toLowerCase() !== "y") {
6791
+ console.log("\n\u5DF2\u53D6\u6D88\u3002\n");
6792
+ return;
6793
+ }
6794
+ }
6795
+ console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6796
+ console.log(" Siluzan \u767B\u5F55\uFF08JWT Token\uFF09");
6797
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6798
+ console.log("\n\u8BF7\u7C98\u8D34 access token\uFF08\u53EF\u5E26\u6216\u4E0D\u5E26 `Bearer ` \u524D\u7F00\uFF09\uFF1A\n");
6799
+ let token = "";
6800
+ for (let i = 0; i < 3; i++) {
6801
+ const input = await prompt("Token\uFF1A");
6802
+ const normalized = normalizeBearerTokenInput(input);
6803
+ if (normalized) {
6804
+ token = normalized;
6805
+ break;
6806
+ }
6807
+ console.log("\u274C Token \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u91CD\u8BD5");
6808
+ }
6809
+ if (!token) {
6810
+ console.error("\n\u274C \u591A\u6B21\u8F93\u5165\u65E0\u6548\u3002\n");
6700
6811
  process.exit(1);
6701
6812
  }
6702
- writeSharedConfig({ apiKey: key });
6813
+ writeSharedConfig({ authToken: token, apiKey: "" });
6703
6814
  console.log(`
6704
- \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(key)}\uFF09`);
6815
+ \u2705 JWT \u5DF2\u4FDD\u5B58\uFF08${maskSecret(token)}\uFF09`);
6705
6816
  console.log(` \u914D\u7F6E\u6587\u4EF6\uFF1A${CONFIG_FILE}`);
6706
6817
  printPostLoginReminderBanner();
6707
6818
  console.log("\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C siluzan-tso -h \u547D\u4EE4\u67E5\u770B\u5E2E\u52A9\u4E86\n");
6708
- return;
6709
- }
6710
- if (opts.phone !== void 0) {
6711
- await runPhoneLogin(opts);
6712
- return;
6819
+ } finally {
6820
+ rl.close();
6713
6821
  }
6822
+ }
6823
+ async function runInteractiveApiKeyLogin() {
6714
6824
  const shared = readSharedConfig();
6715
6825
  const currentKey = shared.apiKey ?? "";
6716
6826
  if (currentKey) {
6717
6827
  console.log(`
6718
6828
  \u5DF2\u68C0\u6D4B\u5230\u5DF2\u4FDD\u5B58\u7684 API Key\uFF08${maskSecret(currentKey)}\uFF09\u3002`);
6719
- const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
6829
+ const rl0 = readline.createInterface({ input: process.stdin, output: process.stdout });
6720
6830
  const answer = await new Promise(
6721
- (res) => rl2.question("\u662F\u5426\u8986\u76D6\uFF1F(y/N) ", (a) => {
6722
- rl2.close();
6831
+ (res) => rl0.question("\u662F\u5426\u8986\u76D6\uFF1F(y/N) ", (a) => {
6832
+ rl0.close();
6723
6833
  res(a.trim());
6724
6834
  })
6725
6835
  );
@@ -6730,46 +6840,164 @@ async function runLogin(opts = {}) {
6730
6840
  }
6731
6841
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6732
6842
  const prompt = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
6733
- console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6734
- console.log(" Siluzan \u767B\u5F55\uFF08API Key\uFF09");
6735
- console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6736
- console.log("\n\u8BF7\u6309\u4EE5\u4E0B\u6B65\u9AA4\u83B7\u53D6 API Key\uFF1A");
6737
- console.log("\n 1. \u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u4EE5\u4E0B\u5730\u5740\uFF08\u9700\u5DF2\u767B\u5F55\u4E1D\u8DEF\u8D5E\u8D26\u53F7\uFF09\uFF1A");
6738
- console.log(`
6843
+ try {
6844
+ console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6845
+ console.log(" Siluzan \u767B\u5F55\uFF08API Key\uFF09");
6846
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6847
+ console.log("\n\u8BF7\u6309\u4EE5\u4E0B\u6B65\u9AA4\u83B7\u53D6 API Key\uFF1A");
6848
+ console.log("\n 1. \u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u4EE5\u4E0B\u5730\u5740\uFF08\u9700\u5DF2\u767B\u5F55\u4E1D\u8DEF\u8D5E\u8D26\u53F7\uFF09\uFF1A");
6849
+ console.log(`
6739
6850
  ${API_KEY_MANAGEMENT_URL}
6740
6851
  `);
6741
- console.log(
6742
- " 2. \u70B9\u51FB\u300C\u521B\u5EFA API Key\u300D\u6309\u94AE\u751F\u6210\u4E00\u4E2A\u65B0\u7684 Key, \u52FE\u9009TSO (\u5E7F\u544A\u6295\u653E\u670D\u52A1)\u4E0ESUCAI (\u7D20\u6750\u4E2D\u5FC3\u670D\u52A1)"
6743
- );
6744
- console.log(" 3. \u590D\u5236\u751F\u6210\u7684 API Key");
6745
- console.log(" 4. \u7C98\u8D34\u5230\u4E0B\u65B9\u5E76\u6309\u56DE\u8F66\n");
6746
- let apiKey = "";
6747
- for (let i = 0; i < 3; i++) {
6748
- const input = await prompt("\u7C98\u8D34 API Key\uFF1A");
6749
- if (input) {
6750
- apiKey = input;
6852
+ console.log(
6853
+ " 2. \u70B9\u51FB\u300C\u521B\u5EFA API Key\u300D\u6309\u94AE\u751F\u6210\u4E00\u4E2A\u65B0\u7684 Key, \u52FE\u9009TSO (\u5E7F\u544A\u6295\u653E\u670D\u52A1)\u4E0ESUCAI (\u7D20\u6750\u4E2D\u5FC3\u670D\u52A1)"
6854
+ );
6855
+ console.log(" 3. \u590D\u5236\u751F\u6210\u7684 API Key");
6856
+ console.log(" 4. \u7C98\u8D34\u5230\u4E0B\u65B9\u5E76\u6309\u56DE\u8F66\n");
6857
+ let apiKey = "";
6858
+ for (let i = 0; i < 3; i++) {
6859
+ const input = await prompt("\u7C98\u8D34 API Key\uFF1A");
6860
+ if (input) {
6861
+ apiKey = input;
6862
+ break;
6863
+ }
6864
+ console.log("\u274C API Key \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u91CD\u8BD5");
6865
+ }
6866
+ if (!apiKey) {
6867
+ console.error("\n\u274C \u591A\u6B21\u8F93\u5165\u65E0\u6548\uFF0C\u8BF7\u91CD\u8BD5\u3002\n");
6868
+ process.exit(1);
6869
+ }
6870
+ writeSharedConfig({ apiKey });
6871
+ console.log(`
6872
+ \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(apiKey)}\uFF09`);
6873
+ printPostLoginReminderBanner();
6874
+ console.log("\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
6875
+ console.log(" siluzan-tso list-accounts \u67E5\u770B\u5E7F\u544A\u8D26\u6237\u5217\u8868");
6876
+ console.log(" siluzan-tso balance -m Google \u67E5\u770B\u8D26\u6237\u4F59\u989D\n");
6877
+ } finally {
6878
+ rl.close();
6879
+ }
6880
+ }
6881
+ async function runInteractivePhoneLogin(menuOpts) {
6882
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6883
+ const prompt = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
6884
+ try {
6885
+ const phoneRaw = await prompt("\n\u8BF7\u8F93\u5165\u624B\u673A\u53F7\uFF08\u5927\u9646\u53F7\u7801\uFF0C\u53EF\u5E26 +86\uFF09\uFF1A");
6886
+ const phone = validateAndNormalizePhone(phoneRaw);
6887
+ const tsoApiBase = process.env.SILUZAN_TSO_API_BASE ?? DEFAULT_API_BASE;
6888
+ const ssoBaseUrl = deriveSsoBaseUrl(tsoApiBase);
6889
+ console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6890
+ console.log(" \u53D1\u9001\u767B\u5F55\u77ED\u4FE1\u9A8C\u8BC1\u7801");
6891
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6892
+ console.log(` \u624B\u673A\u53F7 : ${phone}`);
6893
+ console.log("\u2192 \u6B63\u5728\u5411\u624B\u673A\u53D1\u9001\u9A8C\u8BC1\u7801...");
6894
+ const sendRes = await sendPhoneLoginCode({ ssoBaseUrl, phone, verbose: menuOpts.verbose });
6895
+ if (!sendRes.ok) {
6896
+ console.error(`
6897
+ \u274C \u77ED\u4FE1\u9A8C\u8BC1\u7801\u53D1\u9001\u5931\u8D25\uFF1A${sendRes.message || "(\u540E\u7AEF\u672A\u8FD4\u56DE\u539F\u56E0)"}
6898
+ `);
6899
+ process.exit(1);
6900
+ }
6901
+ console.log("\u2713 \u9A8C\u8BC1\u7801\u5DF2\u53D1\u9001\uFF0810 \u5206\u949F\u5185\u6709\u6548\uFF09\u3002\n");
6902
+ const code = await prompt("\u8BF7\u8F93\u5165 6 \u4F4D\u9A8C\u8BC1\u7801\uFF1A");
6903
+ if (!code.trim()) {
6904
+ console.error("\n\u274C \u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
6905
+ process.exit(1);
6906
+ }
6907
+ await executePhoneLoginWithVerifiedCode(phone, code.trim(), menuOpts);
6908
+ } finally {
6909
+ rl.close();
6910
+ }
6911
+ }
6912
+ async function runLoginMethodMenu(menuOpts) {
6913
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
6914
+ const prompt = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
6915
+ console.log("\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6916
+ console.log(" Siluzan \u767B\u5F55");
6917
+ console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
6918
+ console.log("\n\u8BF7\u9009\u62E9\u767B\u5F55\u65B9\u5F0F\uFF1A");
6919
+ console.log(" 1) JWT Token\uFF08Bearer\uFF0C\u7C98\u8D34 access token\uFF09");
6920
+ console.log(" 2) API Key\uFF08\u7F51\u9875\u300C\u8BBE\u7F6E \u2192 API Key \u7BA1\u7406\u300D\u521B\u5EFA\u540E\u7C98\u8D34\uFF09");
6921
+ console.log(" 3) \u624B\u673A\u53F7 + \u77ED\u4FE1\u9A8C\u8BC1\u7801\uFF08\u5C06\u5411\u624B\u673A\u53D1\u9001\u9A8C\u8BC1\u7801\uFF0C\u9A8C\u8BC1\u540E\u81EA\u52A8\u521B\u5EFA API Key\uFF09");
6922
+ console.log(" 0) \u9000\u51FA\n");
6923
+ let choice = "";
6924
+ for (let attempt = 0; attempt < 5; attempt++) {
6925
+ choice = await prompt("\u8BF7\u8F93\u5165\u9009\u9879 [0-3]\uFF1A");
6926
+ if (choice === "0" || choice === "1" || choice === "2" || choice === "3") {
6751
6927
  break;
6752
6928
  }
6753
- console.log("\u274C API Key \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u91CD\u8BD5");
6929
+ console.log("\u65E0\u6548\u9009\u62E9\uFF0C\u8BF7\u8F93\u5165 0\u30011\u30012 \u6216 3\u3002");
6754
6930
  }
6755
6931
  rl.close();
6756
- if (!apiKey) {
6757
- console.error("\n\u274C \u591A\u6B21\u8F93\u5165\u65E0\u6548\uFF0C\u8BF7\u91CD\u8BD5\u3002\n");
6758
- process.exit(1);
6932
+ if (choice === "0" || choice === "") {
6933
+ console.log("\n\u5DF2\u53D6\u6D88\u3002\n");
6934
+ return;
6759
6935
  }
6760
- writeSharedConfig({ apiKey });
6761
- console.log(`
6762
- \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(apiKey)}\uFF09`);
6763
- console.log(` \u914D\u7F6E\u6587\u4EF6\uFF1A${CONFIG_FILE}`);
6764
- printPostLoginReminderBanner();
6765
- console.log("\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
6766
- console.log(" siluzan-tso list-accounts \u67E5\u770B\u5E7F\u544A\u8D26\u6237\u5217\u8868");
6767
- console.log(" siluzan-tso balance -m Google \u67E5\u770B\u8D26\u6237\u4F59\u989D\n");
6936
+ if (choice === "1") {
6937
+ await runInteractiveJwtLogin();
6938
+ return;
6939
+ }
6940
+ if (choice === "2") {
6941
+ await runInteractiveApiKeyLogin();
6942
+ return;
6943
+ }
6944
+ await runInteractivePhoneLogin(menuOpts);
6945
+ }
6946
+ async function runLogin(opts = {}) {
6947
+ if (opts.apiKey !== void 0) {
6948
+ const key = opts.apiKey.trim();
6949
+ if (!key) {
6950
+ console.error("\n\u274C API Key \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
6951
+ process.exit(1);
6952
+ }
6953
+ writeSharedConfig({ apiKey: key });
6954
+ console.log(`
6955
+ \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(key)}\uFF09`);
6956
+ console.log(` \u914D\u7F6E\u6587\u4EF6\uFF1A${CONFIG_FILE}`);
6957
+ printPostLoginReminderBanner();
6958
+ console.log("\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C siluzan-tso -h \u547D\u4EE4\u67E5\u770B\u5E2E\u52A9\u4E86\n");
6959
+ return;
6960
+ }
6961
+ if (opts.phone !== void 0) {
6962
+ await runPhoneLogin(opts);
6963
+ return;
6964
+ }
6965
+ if (opts.manual) {
6966
+ await runInteractiveJwtLogin();
6967
+ return;
6968
+ }
6969
+ await runLoginMethodMenu(opts);
6768
6970
  }
6769
6971
  function register(program2) {
6770
- program2.command("login").description("\u4FDD\u5B58\u8BA4\u8BC1\u51ED\u636E\u5230 ~/.siluzan/config.json\uFF08Token \u6216 API Key \u4E8C\u9009\u4E00\uFF09").option("--api-key <key>", "\u76F4\u63A5\u4FDD\u5B58 API Key\uFF08\u5728\u4E1D\u8DEF\u8D5E\u300C\u8BBE\u7F6E \u2192 API Key \u7BA1\u7406\u300D\u521B\u5EFA\uFF09\uFF0C\u63A8\u8350\u65B9\u5F0F").option("--manual", "\u4EA4\u4E92\u5F0F\u7C98\u8D34 JWT Token\uFF08\u65E0 API Key \u65F6\u4F7F\u7528\uFF09").action(async (opts) => {
6771
- await runLogin({ apiKey: opts.apiKey });
6972
+ program2.command("send-login-code").description("\u5411\u624B\u673A\u53F7\u53D1\u9001\u4E1D\u8DEF\u8D5E\u767B\u5F55\u77ED\u4FE1\u9A8C\u8BC1\u7801\uFF08\u65E0\u4EA4\u4E92\uFF0C\u9002\u5408 Agent\uFF1A\u5148\u53D1\u7801\u518D login --phone --code\uFF09").requiredOption("--phone <phone>", "\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7\uFF08\u53EF\u5E26 +86\uFF09").option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
6973
+ await runSendLoginCode({ phone: opts.phone, verbose: opts.verbose });
6772
6974
  });
6975
+ program2.command("login").description(
6976
+ "\u4FDD\u5B58\u8BA4\u8BC1\u51ED\u636E\u5230 ~/.siluzan/config.json\uFF1B\u65E0\u53C2\u6570\u65F6\u5C55\u5F00 JWT / API Key / \u624B\u673A\u53F7\u4E09\u79CD\u4EA4\u4E92\u767B\u5F55"
6977
+ ).option("--api-key <key>", "\u76F4\u63A5\u4FDD\u5B58 API Key\uFF08\u7F51\u9875\u300C\u8BBE\u7F6E \u2192 API Key \u7BA1\u7406\u300D\u521B\u5EFA\uFF09").option("--phone <phone>", "\u624B\u673A\u53F7\uFF08\u987B\u4E0E --code \u540C\u4F20\uFF1B\u6216\u7EC8\u7AEF\u65E0\u53C2 login \u9009 3 \u7531 CLI \u53D1\u7801\uFF09").option("--code <code>", "\u77ED\u4FE1\u9A8C\u8BC1\u7801\uFF08\u987B\u4E0E --phone \u540C\u4F20\uFF09").option("--name <text>", "\u624B\u673A\u53F7\u767B\u5F55\u65F6\u81EA\u52A8\u521B\u5EFA\u7684 API Key \u540D\u79F0").option("--valid-days <n>", "API Key \u6709\u6548\u5929\u6570\uFF0C\u9ED8\u8BA4 90").option("--expires-at <iso>", "API Key \u7EDD\u5BF9\u8FC7\u671F\u65F6\u95F4 ISO8601\uFF08\u4E0E --valid-days \u4E8C\u9009\u4E00\uFF09").option("--services <csv>", "API Key \u670D\u52A1\uFF1ACSO,TSO,CUT \u9017\u53F7\u5206\u9694\uFF0C\u9ED8\u8BA4 TSO,CUT").option("--manual", "\u8DF3\u8FC7\u83DC\u5355\uFF0C\u76F4\u63A5\u7C98\u8D34 JWT\uFF08\u4E0E\u65E0\u53C2\u83DC\u5355\u9009\u9879 1 \u7B49\u4EF7\uFF09").option("--verbose", "\u8BE6\u7EC6 HTTP", false).action(
6978
+ async (opts) => {
6979
+ let validDays;
6980
+ if (opts.validDays !== void 0 && String(opts.validDays).trim() !== "") {
6981
+ const n = parseInt(String(opts.validDays), 10);
6982
+ if (Number.isNaN(n)) {
6983
+ console.error("\n\u274C --valid-days \u987B\u4E3A\u6574\u6570\n");
6984
+ process.exit(1);
6985
+ }
6986
+ validDays = n;
6987
+ }
6988
+ await runLogin({
6989
+ apiKey: opts.apiKey,
6990
+ phone: opts.phone,
6991
+ code: opts.code,
6992
+ name: opts.name,
6993
+ validDays,
6994
+ expiresAt: opts.expiresAt,
6995
+ services: opts.services,
6996
+ verbose: opts.verbose,
6997
+ manual: opts.manual
6998
+ });
6999
+ }
7000
+ );
6773
7001
  }
6774
7002
 
6775
7003
  // src/commands/config.ts
@@ -14368,6 +14596,42 @@ init_auth();
14368
14596
  init_cli_json_snapshot();
14369
14597
  init_strip_legacy_google_fields();
14370
14598
  init_cli_table();
14599
+ function unwrapKeywordDisplayTextForEdit(raw) {
14600
+ const t = raw.trim();
14601
+ if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) {
14602
+ return t.slice(1, -1);
14603
+ }
14604
+ if (t.length >= 2 && t.startsWith("[") && t.endsWith("]")) {
14605
+ return t.slice(1, -1);
14606
+ }
14607
+ return t;
14608
+ }
14609
+ function formatKeywordTextForMatchType(rawCoreOrDisplay, matchType) {
14610
+ const core = unwrapKeywordDisplayTextForEdit(rawCoreOrDisplay);
14611
+ switch (matchType) {
14612
+ case "Broad":
14613
+ return core;
14614
+ case "Phrase":
14615
+ return `"${core}"`;
14616
+ case "Exact":
14617
+ return `[${core}]`;
14618
+ default: {
14619
+ const _x = matchType;
14620
+ return _x;
14621
+ }
14622
+ }
14623
+ }
14624
+ function firstKeywordTextFromRecord(k) {
14625
+ const kt = k["keywordText"];
14626
+ if (Array.isArray(kt) && kt.length > 0 && typeof kt[0] === "string") {
14627
+ return kt[0];
14628
+ }
14629
+ const t = k["text"];
14630
+ if (typeof t === "string") {
14631
+ return t;
14632
+ }
14633
+ return "";
14634
+ }
14371
14635
  async function runAdKeywords(opts) {
14372
14636
  const config = loadConfig(opts.token);
14373
14637
  const googleApiUrl = requireGoogleApi(config);
@@ -14559,8 +14823,17 @@ async function runAdKeywordEdit(opts) {
14559
14823
  process.exit(1);
14560
14824
  }
14561
14825
  const body = { ...keyword };
14562
- if (opts.text !== void 0) body["keywordText"] = [opts.text];
14563
- if (opts.matchType !== void 0) body["matchTypeV2"] = opts.matchType;
14826
+ if (opts.matchType !== void 0) {
14827
+ const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
14828
+ if (!String(base).trim()) {
14829
+ console.error("\n\u274C \u65E0\u6CD5\u89E3\u6790\u5F53\u524D\u5173\u952E\u8BCD\u6587\u6848\uFF0C\u8BF7\u540C\u65F6\u4F20 --text <\u8BCD\u5E72>\n");
14830
+ process.exit(1);
14831
+ }
14832
+ body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
14833
+ body["matchTypeV2"] = opts.matchType;
14834
+ } else if (opts.text !== void 0) {
14835
+ body["keywordText"] = [opts.text];
14836
+ }
14564
14837
  if (opts.maxCpc !== void 0) body["maxCPC"] = opts.maxCpc;
14565
14838
  if (opts.finalUrl !== void 0) body["finalURL"] = opts.finalUrl;
14566
14839
  const url = `${googleApiUrl}/keywordmanagement/Keyword/${opts.account}/batch`;
@@ -14607,8 +14880,17 @@ async function runAdNegativeKeywordEdit(opts) {
14607
14880
  process.exit(1);
14608
14881
  }
14609
14882
  const body = { ...keyword };
14610
- if (opts.text !== void 0) body["keywordText"] = [opts.text];
14611
- if (opts.matchType !== void 0) body["matchTypeV2"] = opts.matchType;
14883
+ if (opts.matchType !== void 0) {
14884
+ const base = opts.text !== void 0 ? opts.text : firstKeywordTextFromRecord(keyword);
14885
+ if (!String(base).trim()) {
14886
+ 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");
14887
+ process.exit(1);
14888
+ }
14889
+ body["keywordText"] = [formatKeywordTextForMatchType(base, opts.matchType)];
14890
+ body["matchTypeV2"] = opts.matchType;
14891
+ } else if (opts.text !== void 0) {
14892
+ body["keywordText"] = [opts.text];
14893
+ }
14612
14894
  const url = `${googleApiUrl}/negativekeywordmanagement/negativekeyword/${opts.account}/${opts.id}`;
14613
14895
  try {
14614
14896
  await apiFetch2(url, config, { method: "PUT", body: JSON.stringify(body) }, opts.verbose);
@@ -15910,7 +16192,10 @@ function register20(program2) {
15910
16192
  });
15911
16193
  }
15912
16194
  );
15913
- 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(
16195
+ 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(
16196
+ "--match-type <type>",
16197
+ '\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'
16198
+ ).option(
15914
16199
  "--max-cpc <n>",
15915
16200
  "\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"
15916
16201
  ).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(
@@ -15941,7 +16226,10 @@ function register20(program2) {
15941
16226
  });
15942
16227
  }
15943
16228
  );
15944
- 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(
16229
+ 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(
16230
+ "--match-type <type>",
16231
+ '\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'
16232
+ ).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(
15945
16233
  async (opts) => {
15946
16234
  if (opts.matchType && !["Broad", "Phrase", "Exact"].includes(opts.matchType)) {
15947
16235
  console.error("\n\u274C --match-type \u53EA\u63A5\u53D7 Broad | Phrase | Exact\n");
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.18-beta.6",
4
- "publishedAt": 1778324246028
3
+ "version": "1.1.18-beta.8",
4
+ "publishedAt": 1778478403907
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`) |
@@ -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> \
@@ -57,7 +57,8 @@ siluzan-tso init -d /path/to-your/skills # 写入自定义目录
57
57
  ## 其它登录方式(TTY 交互 / 已有 API Key / JWT)
58
58
 
59
59
  ```bash
60
- siluzan-tso login # 交互式登录(需 TTY),按提示创建 API Key 后粘贴
60
+ siluzan-tso login # 无参:TTY 下展开菜单 → 1 JWT / 2 API Key / 3 手机号(发码+输码)
61
+ siluzan-tso login --manual # 跳过菜单,直接粘贴 JWT(会清除已存 API Key 以启用 Bearer)
61
62
  siluzan-tso login --api-key <YOUR_API_KEY> # 直接设置 API Key(跳过交互)
62
63
  siluzan-tso config set --api-key <Key> # 或 config 直接写入
63
64
  siluzan-tso config set --token <Token> # 备用:设置 JWT Token
@@ -94,7 +95,7 @@ siluzan-tso login --phone 13800138000 --code 123456 \
94
95
  >
95
96
  > **验证码错误/过期**:返回明确提示,让用户重新跑 `send-login-code` 拿新验证码。
96
97
  >
97
- > **AI 助手用法**:先调 `send-login-code` 发码、立即返回继续对话;等用户报出收到的验证码后,再调 `login --phone xxx --code xxx` 完成登录。**永远不要在没拿到 `--code` 的情况下调用 `login --phone xxx`,那会直接报错。**
98
+ > **AI 助手用法**:先调 `send-login-code` 发码、立即返回继续对话;等用户报出收到的验证码后,再调 `login --phone xxx --code xxx` 完成登录。**永远不要在没拿到 `--code` 的情况下调用 `login --phone xxx`,那会直接报错。**(人类在本地终端可直接运行无参 `siluzan-tso login` 选 3,由 CLI 发码并读验证码,无需先记 `send-login-code`。)
98
99
 
99
100
  ### 通过环境变量传入凭据(CI/CD 推荐)
100
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.18-beta.6",
3
+ "version": "1.1.18-beta.8",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "keywords": [
6
6
  "ad-account",