siluzan-tso-cli 1.1.8-beta.6 → 1.1.9-beta.2

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
@@ -2,6 +2,35 @@
2
2
 
3
3
  当用户发来本页面的 **URL** 时,请按顺序协助用户完成安装。
4
4
 
5
+ ## 0. 安全与透明性说明
6
+
7
+ 本 Skill 是 **instruction-only**(纯指令型),不包含可执行代码文件。它通过指导 AI 助手调用 `siluzan-tso` CLI 来操作丝路赞广告管理平台。
8
+
9
+ ### 运行时依赖
10
+ | 项目 | 说明 |
11
+ |------|------|
12
+ | **Node.js 18+** | CLI 运行时 |
13
+ | **npm 8+** 或 **pnpm** | 安装 CLI 全局包 |
14
+ | **siluzan-tso** CLI | 由 `npm install -g siluzan-tso-cli` 安装 |
15
+
16
+ ### 凭据与网络
17
+ | 项目 | 说明 |
18
+ |------|------|
19
+ | API Key / Token | 存储在 `~/.siluzan/config.json`,用于所有 API 调用鉴权 |
20
+ | TSO API | `tso-api.siluzan.com`(生产)/ `tso-api-ci.siluzan.com`(测试) |
21
+ | Google API 网关 | `googleapi.mysiluzan.com` / `googleapi-ci.mysiluzan.com` |
22
+ | 前端页面 | `www.siluzan.com`(充值、激活等浏览器操作) |
23
+ | 错误追踪 | `o605862.ingest.us.sentry.io`(匿名崩溃日志) |
24
+ | 版本检查 | `registry.npmjs.org` |
25
+
26
+ ### 可执行的操作范围
27
+ - **只读**:查询账户列表、余额、消耗数据、报告、预警记录等
28
+ - **写入**(需用户确认):创建/修改/发布广告、开户申请、创建预警规则、报告推送配置
29
+ - **破坏性**(必须用户确认):账户解绑/关闭、MCC/BC 解绑、删除预警/报告/广告、发票申请
30
+
31
+ ### 报告模板外部资源
32
+ HTML 报告模板引用以下 CDN:`cdn.tailwindcss.com`、`cdnjs.cloudflare.com`、`cdn.jsdelivr.net`、`fonts.googleapis.com`。渲染报告时浏览器会发出对应网络请求。
33
+
5
34
  ## 1. 环境
6
35
 
7
36
  - Node.js **18+**
@@ -20,7 +49,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
20
49
  siluzan-tso init --force # 强制覆盖已存在文件
21
50
  ```
22
51
 
23
- > **注意**:当前为测试版(1.1.8-beta.6),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
52
+ > **注意**:当前为测试版(1.1.9-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
24
53
 
25
54
  | 助手 | 建议 `--ai` |
26
55
  |------|-------------|
package/dist/index.js CHANGED
@@ -362,8 +362,8 @@ var require_semver = __commonJS({
362
362
  }
363
363
  // preminor will bump the version up to the next minor release, and immediately
364
364
  // down to pre-release. premajor and prepatch work the same way.
365
- inc(release2, identifier, identifierBase) {
366
- if (release2.startsWith("pre")) {
365
+ inc(release, identifier, identifierBase) {
366
+ if (release.startsWith("pre")) {
367
367
  if (!identifier && identifierBase === false) {
368
368
  throw new Error("invalid increment argument: identifier is empty");
369
369
  }
@@ -374,7 +374,7 @@ var require_semver = __commonJS({
374
374
  }
375
375
  }
376
376
  }
377
- switch (release2) {
377
+ switch (release) {
378
378
  case "premajor":
379
379
  this.prerelease.length = 0;
380
380
  this.patch = 0;
@@ -465,7 +465,7 @@ var require_semver = __commonJS({
465
465
  break;
466
466
  }
467
467
  default:
468
- throw new Error(`invalid increment argument: ${release2}`);
468
+ throw new Error(`invalid increment argument: ${release}`);
469
469
  }
470
470
  this.raw = this.format();
471
471
  if (this.build.length) {
@@ -531,7 +531,7 @@ var require_inc = __commonJS({
531
531
  "../node_modules/.pnpm/semver@7.7.4/node_modules/semver/functions/inc.js"(exports, module) {
532
532
  "use strict";
533
533
  var SemVer = require_semver();
534
- var inc = (version, release2, options, identifier, identifierBase) => {
534
+ var inc = (version, release, options, identifier, identifierBase) => {
535
535
  if (typeof options === "string") {
536
536
  identifierBase = identifier;
537
537
  identifier = options;
@@ -541,7 +541,7 @@ var require_inc = __commonJS({
541
541
  return new SemVer(
542
542
  version instanceof SemVer ? version.version : version,
543
543
  options
544
- ).inc(release2, identifier, identifierBase).version;
544
+ ).inc(release, identifier, identifierBase).version;
545
545
  } catch (er) {
546
546
  return null;
547
547
  }
@@ -1974,7 +1974,6 @@ var import_semver = __toESM(require_semver2(), 1);
1974
1974
  import * as fs2 from "fs";
1975
1975
  import * as path2 from "path";
1976
1976
  import { fileURLToPath } from "url";
1977
- import * as os2 from "os";
1978
1977
  var SILUZAN_DIR = path.join(os.homedir(), ".siluzan");
1979
1978
  var CONFIG_FILE = path.join(SILUZAN_DIR, "config.json");
1980
1979
  function readStr(raw, key) {
@@ -2055,21 +2054,33 @@ function validateBaseUrl(raw) {
2055
2054
  }
2056
2055
  return null;
2057
2056
  }
2057
+ var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
2058
+ var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
2058
2059
  function rawRequest(url, options) {
2059
2060
  return new Promise((resolve2, reject) => {
2060
2061
  const parsed = new URL(url);
2061
2062
  const transport = parsed.protocol === "https:" ? https : http;
2063
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2064
+ const maxBytes = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
2062
2065
  const reqOpts = {
2063
2066
  hostname: parsed.hostname,
2064
2067
  port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
2065
2068
  path: parsed.pathname + parsed.search,
2066
2069
  method: options.method ?? "GET",
2067
- headers: options.headers
2070
+ headers: options.headers,
2071
+ timeout: timeoutMs || void 0
2068
2072
  };
2069
2073
  const req = transport.request(reqOpts, (res) => {
2070
2074
  let data = "";
2075
+ let byteLen = 0;
2071
2076
  res.setEncoding("utf8");
2072
2077
  res.on("data", (chunk) => {
2078
+ byteLen += Buffer.byteLength(chunk, "utf8");
2079
+ if (maxBytes && byteLen > maxBytes) {
2080
+ res.destroy();
2081
+ reject(new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u4E0A\u9650\uFF08${(maxBytes / 1024 / 1024).toFixed(0)} MB\uFF09\uFF0C\u5DF2\u4E2D\u65AD\u8FDE\u63A5`));
2082
+ return;
2083
+ }
2073
2084
  data += chunk;
2074
2085
  });
2075
2086
  res.on("end", () => {
@@ -2082,6 +2093,10 @@ function rawRequest(url, options) {
2082
2093
  resolve2({ status: res.statusCode ?? 0, text: data, headers });
2083
2094
  });
2084
2095
  });
2096
+ req.on("timeout", () => {
2097
+ req.destroy();
2098
+ reject(new Error(`\u8BF7\u6C42\u8D85\u65F6\uFF08${(timeoutMs / 1e3).toFixed(0)} \u79D2\uFF09\uFF1A${options.method ?? "GET"} ${url}`));
2099
+ });
2085
2100
  req.on("error", reject);
2086
2101
  if (options.body) req.write(options.body);
2087
2102
  req.end();
@@ -2191,308 +2206,6 @@ async function fetchNpmVersion(pkgName, tag, timeoutMs = 4e3) {
2191
2206
  return null;
2192
2207
  }
2193
2208
  }
2194
- var DEFAULT_SENTRY_DSN = "https://bafcf42aab6fe7b485310619ae041b5e@o4510436169285632.ingest.us.sentry.io/4511103054708736";
2195
- function isSentryDisabled() {
2196
- return process.env.SILUZAN_SENTRY_DISABLED === "1" || process.env.SILUZAN_SENTRY_DISABLED === "true";
2197
- }
2198
- function getDsn() {
2199
- return process.env.SILUZAN_SENTRY_DSN?.trim() || DEFAULT_SENTRY_DSN;
2200
- }
2201
- var cliMeta = { name: "siluzan-cli", version: "unknown" };
2202
- var cliInvocation = "";
2203
- function setSiluzanCliInvocation(redactedCommandLine) {
2204
- cliInvocation = redactedCommandLine;
2205
- }
2206
- function redactCliArgvForSentry(argv) {
2207
- const args = argv.slice(2);
2208
- const redactNext = /* @__PURE__ */ new Set(["-t", "--token", "--api-key", "--password"]);
2209
- const out = [];
2210
- for (let i = 0; i < args.length; i++) {
2211
- const a = args[i];
2212
- if (redactNext.has(a)) {
2213
- out.push(a, "***");
2214
- i++;
2215
- continue;
2216
- }
2217
- const eq = a.indexOf("=");
2218
- if (eq > 0) {
2219
- const key = a.slice(0, eq).toLowerCase();
2220
- if (key === "--token" || key === "--api-key" || key === "--password") {
2221
- out.push(`${a.slice(0, eq)}=***`);
2222
- continue;
2223
- }
2224
- }
2225
- if (/^-t[^-]/.test(a) && a.length > 3) {
2226
- out.push("-t***");
2227
- continue;
2228
- }
2229
- out.push(a);
2230
- }
2231
- return out.join(" ");
2232
- }
2233
- function setSiluzanCliMeta(name, version) {
2234
- cliMeta = { name, version };
2235
- if (sentryReady) {
2236
- void import("@sentry/node").then((Sentry) => {
2237
- Sentry.setTag("cli", name);
2238
- Sentry.setTag("cli_version", version);
2239
- }).catch(() => {
2240
- });
2241
- }
2242
- }
2243
- var sentryReady = false;
2244
- var sentryInitPromise = null;
2245
- var flushHookRegistered = false;
2246
- function buildRuntimeContext() {
2247
- const platform = process.platform;
2248
- const osName = platform === "win32" ? "Windows" : platform === "darwin" ? "macOS" : "Linux";
2249
- return {
2250
- os: {
2251
- name: osName,
2252
- version: os2.release(),
2253
- // 内核版本,如 10.0.26100(Win11)、24.3.0(macOS)
2254
- arch: process.arch
2255
- // x64 / arm64
2256
- },
2257
- runtime: {
2258
- node_version: process.version,
2259
- // v18.x.x / v20.x.x
2260
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
2261
- }
2262
- };
2263
- }
2264
- async function ensureSentryInitialized(requestUrl) {
2265
- const dsn = getDsn();
2266
- if (!dsn || isSentryDisabled()) return false;
2267
- if (sentryReady) return true;
2268
- if (!sentryInitPromise) {
2269
- sentryInitPromise = (async () => {
2270
- const Sentry = await import("@sentry/node");
2271
- const envOverride = process.env.SILUZAN_SENTRY_ENV?.trim();
2272
- const environment = envOverride || (inferSiluzanRuntimeEnvironment(requestUrl) === "test" ? "test" : "production");
2273
- Sentry.init({
2274
- dsn,
2275
- environment,
2276
- sendDefaultPii: true,
2277
- tracesSampleRate: 0
2278
- });
2279
- const ctx = buildRuntimeContext();
2280
- Sentry.setContext("os", ctx.os);
2281
- Sentry.setContext("runtime", ctx.runtime);
2282
- Sentry.setTag("cli", cliMeta.name);
2283
- Sentry.setTag("cli_version", cliMeta.version);
2284
- if (!flushHookRegistered) {
2285
- flushHookRegistered = true;
2286
- process.once("beforeExit", () => {
2287
- void Sentry.flush(2e3).catch(() => {
2288
- });
2289
- });
2290
- }
2291
- sentryReady = true;
2292
- })();
2293
- }
2294
- await sentryInitPromise;
2295
- return sentryReady;
2296
- }
2297
- function deriveMainApiOriginFromRequestUrl(requestUrl) {
2298
- try {
2299
- const u = new URL(requestUrl);
2300
- const host = u.hostname.toLowerCase();
2301
- if (!host.endsWith("siluzan.com")) return null;
2302
- if (host.startsWith("tso-api")) {
2303
- return `${u.protocol}//${host.replace(/^tso-api/, "api")}`;
2304
- }
2305
- if (host === "api.siluzan.com" || host === "api-ci.siluzan.com") {
2306
- return `${u.protocol}//${host}`;
2307
- }
2308
- return null;
2309
- } catch {
2310
- return null;
2311
- }
2312
- }
2313
- function inferSiluzanRuntimeEnvironment(requestUrl) {
2314
- try {
2315
- const host = new URL(requestUrl).hostname.toLowerCase();
2316
- return host.includes("-ci") ? "test" : "production";
2317
- } catch {
2318
- return "production";
2319
- }
2320
- }
2321
- function cacheKeyForUser(mainOrigin, config) {
2322
- const cred = config.apiKey ? `k:${config.apiKey}` : `t:${config.authToken}`;
2323
- return `${mainOrigin}\0${cred}`;
2324
- }
2325
- var userContextDone = /* @__PURE__ */ new Set();
2326
- var userContextInflight = /* @__PURE__ */ new Map();
2327
- function pickCompanyIdFromMe(data) {
2328
- const direct = pickStr(data, "companyId") ?? pickStr(data, "companyID") ?? pickStr(data, "CompanyId");
2329
- if (direct) return direct;
2330
- const ci = data["companyInfo"];
2331
- if (ci && typeof ci === "object") {
2332
- const o = ci;
2333
- return pickStr(o, "id") ?? pickStr(o, "companyId") ?? pickStr(o, "companyID");
2334
- }
2335
- return void 0;
2336
- }
2337
- function parseMeResponse(text) {
2338
- try {
2339
- const json = JSON.parse(text);
2340
- const data = json?.data && typeof json.data === "object" ? json.data : json;
2341
- const id = pickStr(data, "entityId") ?? pickStr(data, "id") ?? pickStr(data, "userId") ?? pickStr(data, "accountId");
2342
- const email = pickStr(data, "email");
2343
- const username = pickStr(data, "userName") ?? pickStr(data, "username") ?? pickStr(data, "name") ?? pickStr(data, "phone");
2344
- const companyId = pickCompanyIdFromMe(data);
2345
- if (!id && !email && !username && !companyId) return null;
2346
- return { id, email, username, companyId };
2347
- } catch {
2348
- return null;
2349
- }
2350
- }
2351
- function pickStr(obj, key) {
2352
- const v = obj[key];
2353
- return typeof v === "string" && v.trim() ? v.trim() : void 0;
2354
- }
2355
- async function fetchAndSetUser(mainOrigin, config) {
2356
- const meUrl = `${mainOrigin.replace(/\/$/, "")}/query/account/me`;
2357
- const authHeaders = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
2358
- const res = await rawRequest(meUrl, {
2359
- method: "GET",
2360
- headers: {
2361
- "Content-Type": "application/json",
2362
- "Accept-Language": "zh-CN",
2363
- ...authHeaders
2364
- }
2365
- });
2366
- if (res.status < 200 || res.status >= 300) return;
2367
- const parsed = parseMeResponse(res.text);
2368
- if (!parsed) return;
2369
- const Sentry = await import("@sentry/node");
2370
- const user = {};
2371
- if (parsed.id) user.id = parsed.id;
2372
- if (parsed.email) user.email = parsed.email;
2373
- if (parsed.username) user.username = parsed.username;
2374
- Sentry.setUser(user);
2375
- }
2376
- function scheduleUserContext(mainOrigin, config) {
2377
- const key = cacheKeyForUser(mainOrigin, config);
2378
- if (userContextDone.has(key)) return;
2379
- let p = userContextInflight.get(key);
2380
- if (!p) {
2381
- p = (async () => {
2382
- try {
2383
- await fetchAndSetUser(mainOrigin, config);
2384
- } catch {
2385
- } finally {
2386
- userContextInflight.delete(key);
2387
- userContextDone.add(key);
2388
- }
2389
- })();
2390
- userContextInflight.set(key, p);
2391
- }
2392
- }
2393
- function refreshSiluzanUser(apiBase, config) {
2394
- if (isSentryDisabled()) return;
2395
- void (async () => {
2396
- try {
2397
- const ok = await ensureSentryInitialized(apiBase);
2398
- if (!ok) return;
2399
- const mainOrigin = deriveMainApiOriginFromRequestUrl(apiBase) ?? (apiBase.includes("-ci") ? "https://api-ci.siluzan.com" : "https://api.siluzan.com");
2400
- const key = cacheKeyForUser(mainOrigin, config);
2401
- userContextDone.delete(key);
2402
- userContextInflight.delete(key);
2403
- scheduleUserContext(mainOrigin, config);
2404
- } catch {
2405
- }
2406
- })();
2407
- }
2408
-
2409
- // src/utils/version.ts
2410
- import * as fs3 from "fs";
2411
- import * as path3 from "path";
2412
- import * as os3 from "os";
2413
- var PKG_NAME = "siluzan-tso-cli";
2414
- var CONFIG_FILE2 = path3.join(os3.homedir(), ".siluzan", "config.json");
2415
- function getCurrentVersion2() {
2416
- return getCurrentVersion(import.meta.url);
2417
- }
2418
- function readConfigRaw() {
2419
- try {
2420
- return JSON.parse(fs3.readFileSync(CONFIG_FILE2, "utf8"));
2421
- } catch {
2422
- return {};
2423
- }
2424
- }
2425
- function writeConfigRaw(data) {
2426
- try {
2427
- fs3.mkdirSync(path3.dirname(CONFIG_FILE2), { recursive: true });
2428
- fs3.writeFileSync(CONFIG_FILE2, JSON.stringify(data, null, 2), "utf8");
2429
- if (process.platform !== "win32") {
2430
- fs3.chmodSync(CONFIG_FILE2, 384);
2431
- }
2432
- } catch {
2433
- }
2434
- }
2435
- async function fetchVersionByTag(tag, cacheKey, cfg) {
2436
- const hours24 = 24 * 60 * 60 * 1e3;
2437
- if (cfg._tsoLastVersionCheck && cfg[cacheKey]) {
2438
- const lastCheck = new Date(cfg._tsoLastVersionCheck).getTime();
2439
- if (Date.now() - lastCheck < hours24) {
2440
- return cfg[cacheKey];
2441
- }
2442
- }
2443
- return fetchNpmVersion(PKG_NAME, tag);
2444
- }
2445
- async function notifyIfOutdated() {
2446
- try {
2447
- const current = getCurrentVersion2();
2448
- const tag = npmDistTagForCurrentVersion(current);
2449
- const isBeta = tag === "beta";
2450
- const latestCacheKey = isBeta ? "_tsoLatestBeta" : "_tsoLatestStable";
2451
- const minCacheKey = isBeta ? "_tsoMinRequiredBeta" : "_tsoMinRequiredStable";
2452
- const minTag = npmMinRequiredTagForBuildEnv(isBeta ? "test" : "production");
2453
- const cfg = readConfigRaw();
2454
- const [latest, minRequired] = await Promise.all([
2455
- fetchVersionByTag(tag, latestCacheKey, cfg),
2456
- fetchVersionByTag(minTag, minCacheKey, cfg)
2457
- ]);
2458
- writeConfigRaw({
2459
- ...cfg,
2460
- _tsoLastVersionCheck: (/* @__PURE__ */ new Date()).toISOString(),
2461
- ...latest ? { [latestCacheKey]: latest } : {},
2462
- ...minRequired ? { [minCacheKey]: minRequired } : {}
2463
- });
2464
- const lastNotified = cfg._tsoLastNotified ? new Date(cfg._tsoLastNotified).getTime() : 0;
2465
- if (Date.now() - lastNotified < 24 * 60 * 60 * 1e3) return;
2466
- const tagLabel = isBeta ? "\uFF08\u6D4B\u8BD5\u7248\uFF09" : "\uFF08\u6B63\u5F0F\u7248\uFF09";
2467
- if (minRequired && isNewer(current, minRequired)) {
2468
- writeConfigRaw({ ...cfg, _tsoLastNotified: (/* @__PURE__ */ new Date()).toISOString() });
2469
- console.log(
2470
- `
2471
- [SILUZAN_FORCE_UPDATE] \u5F53\u524D\u7248\u672C ${current} \u5DF2\u8FC7\u671F\uFF0C\u6700\u4F4E\u8981\u6C42\u7248\u672C ${minRequired}${tagLabel}
2472
- \u26D4 \u5FC5\u987B\u5148\u5347\u7EA7\u624D\u80FD\u7EE7\u7EED\u4F7F\u7528\uFF0CAgent \u8BF7\u7ACB\u5373\u6267\u884C\uFF1Asiluzan-tso update
2473
- \u6216\u624B\u52A8\uFF1Anpm install -g ${PKG_NAME}@${tag}
2474
- `
2475
- );
2476
- return;
2477
- }
2478
- if (latest && isNewer(current, latest)) {
2479
- writeConfigRaw({ ...cfg, _tsoLastNotified: (/* @__PURE__ */ new Date()).toISOString() });
2480
- console.log(
2481
- `
2482
- [SILUZAN_UPDATE_AVAILABLE] \u53D1\u73B0\u65B0\u7248\u672C ${latest}${tagLabel}\uFF08\u5F53\u524D ${current}\uFF09
2483
- \u{1F4A1} Agent \u8BF7\u7ACB\u5373\u6267\u884C\uFF1Asiluzan-tso update
2484
- \u6216\u624B\u52A8\uFF1Anpm install -g ${PKG_NAME}@${tag}
2485
- `
2486
- );
2487
- }
2488
- } catch {
2489
- }
2490
- }
2491
- async function fetchLatestVersion() {
2492
- const current = getCurrentVersion2();
2493
- const tag = npmDistTagForCurrentVersion(current);
2494
- return fetchNpmVersion(PKG_NAME, tag);
2495
- }
2496
2209
 
2497
2210
  // src/utils/auth.ts
2498
2211
  function deriveMainApiUrl(tsoApiBaseUrl) {
@@ -2579,7 +2292,6 @@ function loadConfig(tokenArg) {
2579
2292
  \u274C googleApiUrl \u4E0D\u5408\u6CD5\uFF1A${googleApiErr}`);
2580
2293
  process.exit(1);
2581
2294
  }
2582
- setSiluzanCliMeta("siluzan-tso", getCurrentVersion2());
2583
2295
  return {
2584
2296
  apiBaseUrl,
2585
2297
  authToken,
@@ -2696,26 +2408,26 @@ function cmdConfigClear() {
2696
2408
  }
2697
2409
 
2698
2410
  // src/commands/init.ts
2699
- import * as fs5 from "fs/promises";
2411
+ import * as fs4 from "fs/promises";
2700
2412
  import * as fsSync from "fs";
2701
- import * as os4 from "os";
2702
- import * as path5 from "path";
2413
+ import * as os2 from "os";
2414
+ import * as path4 from "path";
2703
2415
  import { fileURLToPath as fileURLToPath2 } from "url";
2704
2416
 
2705
2417
  // src/templates/load-templates.ts
2706
- import * as fs4 from "fs/promises";
2707
- import * as path4 from "path";
2418
+ import * as fs3 from "fs/promises";
2419
+ import * as path3 from "path";
2708
2420
  async function getSkillFiles(skillDir) {
2709
2421
  const out = {};
2710
2422
  async function walk(dir, prefix) {
2711
- const entries = await fs4.readdir(dir, { withFileTypes: true });
2423
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
2712
2424
  for (const ent of entries) {
2713
2425
  const rel = prefix ? `${prefix}/${ent.name}` : ent.name;
2714
- const full = path4.join(dir, ent.name);
2426
+ const full = path3.join(dir, ent.name);
2715
2427
  if (ent.isDirectory()) {
2716
2428
  await walk(full, rel);
2717
2429
  } else {
2718
- out[rel] = await fs4.readFile(full, "utf8");
2430
+ out[rel] = await fs3.readFile(full, "utf8");
2719
2431
  }
2720
2432
  }
2721
2433
  }
@@ -2724,14 +2436,14 @@ async function getSkillFiles(skillDir) {
2724
2436
  }
2725
2437
 
2726
2438
  // src/commands/init.ts
2727
- var __dirname = path5.dirname(fileURLToPath2(import.meta.url));
2439
+ var __dirname = path4.dirname(fileURLToPath2(import.meta.url));
2728
2440
  var TARGET_DIRS = {
2729
- cursor: (cwd) => path5.join(cwd, ".cursor", "skills", "siluzan-tso"),
2730
- claude: (cwd) => path5.join(cwd, ".claude", "skills", "siluzan-tso"),
2731
- "openclaw-workspace": (cwd) => path5.join(cwd, "skills", "siluzan-tso"),
2732
- "openclaw-global": (_cwd, home) => path5.join(home, ".openclaw", "skills", "siluzan-tso"),
2733
- "workbuddy-workspace": (cwd) => path5.join(cwd, ".workbuddy", "skills", "siluzan-tso"),
2734
- "workbuddy-global": (_cwd, home) => path5.join(home, ".workbuddy", "skills", "siluzan-tso")
2441
+ cursor: (cwd) => path4.join(cwd, ".cursor", "skills", "siluzan-tso"),
2442
+ claude: (cwd) => path4.join(cwd, ".claude", "skills", "siluzan-tso"),
2443
+ "openclaw-workspace": (cwd) => path4.join(cwd, "skills", "siluzan-tso"),
2444
+ "openclaw-global": (_cwd, home) => path4.join(home, ".openclaw", "skills", "siluzan-tso"),
2445
+ "workbuddy-workspace": (cwd) => path4.join(cwd, ".workbuddy", "skills", "siluzan-tso"),
2446
+ "workbuddy-global": (_cwd, home) => path4.join(home, ".workbuddy", "skills", "siluzan-tso")
2735
2447
  };
2736
2448
  function parseTargets(raw) {
2737
2449
  const normalized = raw.trim().toLowerCase();
@@ -2772,32 +2484,32 @@ function parseTargets(raw) {
2772
2484
  return [...new Set(result)];
2773
2485
  }
2774
2486
  function skillRoot() {
2775
- return path5.join(__dirname, "skill");
2487
+ return path4.join(__dirname, "skill");
2776
2488
  }
2777
2489
  async function writeSkillFilesToDir(destDir, skillFiles, force) {
2778
- await fs5.mkdir(destDir, { recursive: true });
2490
+ await fs4.mkdir(destDir, { recursive: true });
2779
2491
  let anyWritten = false;
2780
2492
  for (const [relativePath, content] of Object.entries(skillFiles)) {
2781
- const fullPath = path5.join(destDir, relativePath);
2782
- await fs5.mkdir(path5.dirname(fullPath), { recursive: true });
2493
+ const fullPath = path4.join(destDir, relativePath);
2494
+ await fs4.mkdir(path4.dirname(fullPath), { recursive: true });
2783
2495
  try {
2784
- await fs5.access(fullPath);
2496
+ await fs4.access(fullPath);
2785
2497
  if (!force) {
2786
2498
  console.warn(`\u8DF3\u8FC7\uFF08\u5DF2\u5B58\u5728\uFF0C\u4F7F\u7528 --force \u8986\u76D6\uFF09: ${fullPath}`);
2787
2499
  continue;
2788
2500
  }
2789
2501
  } catch {
2790
2502
  }
2791
- await fs5.writeFile(fullPath, content, "utf8");
2503
+ await fs4.writeFile(fullPath, content, "utf8");
2792
2504
  console.log(`\u5DF2\u5199\u5165: ${fullPath}`);
2793
2505
  anyWritten = true;
2794
2506
  }
2795
2507
  return anyWritten;
2796
2508
  }
2797
2509
  function saveInstalledTargets(entries) {
2798
- const CONFIG_FILE3 = path5.join(os4.homedir(), ".siluzan", "config.json");
2510
+ const CONFIG_FILE3 = path4.join(os2.homedir(), ".siluzan", "config.json");
2799
2511
  try {
2800
- fsSync.mkdirSync(path5.dirname(CONFIG_FILE3), { recursive: true });
2512
+ fsSync.mkdirSync(path4.dirname(CONFIG_FILE3), { recursive: true });
2801
2513
  let existing = {};
2802
2514
  if (fsSync.existsSync(CONFIG_FILE3)) {
2803
2515
  existing = JSON.parse(fsSync.readFileSync(CONFIG_FILE3, "utf8"));
@@ -2819,11 +2531,11 @@ function saveInstalledTargets(entries) {
2819
2531
  }
2820
2532
  }
2821
2533
  async function runInit(options) {
2822
- const home = os4.homedir();
2534
+ const home = os2.homedir();
2823
2535
  const skillFiles = await getSkillFiles(skillRoot());
2824
2536
  const installedEntries = [];
2825
2537
  if (options.dir) {
2826
- const destDir = path5.resolve(options.cwd, options.dir);
2538
+ const destDir = path4.resolve(options.cwd, options.dir);
2827
2539
  console.log(`\u5B89\u88C5\u76EE\u6807\u76EE\u5F55\uFF1A${destDir}`);
2828
2540
  const anyWritten = await writeSkillFilesToDir(destDir, skillFiles, options.force);
2829
2541
  if (anyWritten) installedEntries.push({ target: "custom", cwd: "", dir: destDir });
@@ -2854,6 +2566,96 @@ async function runInit(options) {
2854
2566
  import * as path6 from "path";
2855
2567
  import { fileURLToPath as fileURLToPath3 } from "url";
2856
2568
  import { spawnSync } from "child_process";
2569
+
2570
+ // src/utils/version.ts
2571
+ import * as fs5 from "fs";
2572
+ import * as path5 from "path";
2573
+ import * as os3 from "os";
2574
+ var PKG_NAME = "siluzan-tso-cli";
2575
+ var CONFIG_FILE2 = path5.join(os3.homedir(), ".siluzan", "config.json");
2576
+ function getCurrentVersion2() {
2577
+ return getCurrentVersion(import.meta.url);
2578
+ }
2579
+ function readConfigRaw() {
2580
+ try {
2581
+ return JSON.parse(fs5.readFileSync(CONFIG_FILE2, "utf8"));
2582
+ } catch {
2583
+ return {};
2584
+ }
2585
+ }
2586
+ function writeConfigRaw(data) {
2587
+ try {
2588
+ fs5.mkdirSync(path5.dirname(CONFIG_FILE2), { recursive: true });
2589
+ fs5.writeFileSync(CONFIG_FILE2, JSON.stringify(data, null, 2), "utf8");
2590
+ if (process.platform !== "win32") {
2591
+ fs5.chmodSync(CONFIG_FILE2, 384);
2592
+ }
2593
+ } catch {
2594
+ }
2595
+ }
2596
+ async function fetchVersionByTag(tag, cacheKey, cfg) {
2597
+ const hours24 = 24 * 60 * 60 * 1e3;
2598
+ if (cfg._tsoLastVersionCheck && cfg[cacheKey]) {
2599
+ const lastCheck = new Date(cfg._tsoLastVersionCheck).getTime();
2600
+ if (Date.now() - lastCheck < hours24) {
2601
+ return cfg[cacheKey];
2602
+ }
2603
+ }
2604
+ return fetchNpmVersion(PKG_NAME, tag);
2605
+ }
2606
+ async function notifyIfOutdated() {
2607
+ try {
2608
+ const current = getCurrentVersion2();
2609
+ const tag = npmDistTagForCurrentVersion(current);
2610
+ const isBeta = tag === "beta";
2611
+ const latestCacheKey = isBeta ? "_tsoLatestBeta" : "_tsoLatestStable";
2612
+ const minCacheKey = isBeta ? "_tsoMinRequiredBeta" : "_tsoMinRequiredStable";
2613
+ const minTag = npmMinRequiredTagForBuildEnv(isBeta ? "test" : "production");
2614
+ const cfg = readConfigRaw();
2615
+ const [latest, minRequired] = await Promise.all([
2616
+ fetchVersionByTag(tag, latestCacheKey, cfg),
2617
+ fetchVersionByTag(minTag, minCacheKey, cfg)
2618
+ ]);
2619
+ writeConfigRaw({
2620
+ ...cfg,
2621
+ _tsoLastVersionCheck: (/* @__PURE__ */ new Date()).toISOString(),
2622
+ ...latest ? { [latestCacheKey]: latest } : {},
2623
+ ...minRequired ? { [minCacheKey]: minRequired } : {}
2624
+ });
2625
+ const lastNotified = cfg._tsoLastNotified ? new Date(cfg._tsoLastNotified).getTime() : 0;
2626
+ if (Date.now() - lastNotified < 24 * 60 * 60 * 1e3) return;
2627
+ const tagLabel = isBeta ? "\uFF08\u6D4B\u8BD5\u7248\uFF09" : "\uFF08\u6B63\u5F0F\u7248\uFF09";
2628
+ if (minRequired && isNewer(current, minRequired)) {
2629
+ writeConfigRaw({ ...cfg, _tsoLastNotified: (/* @__PURE__ */ new Date()).toISOString() });
2630
+ console.log(
2631
+ `
2632
+ [SILUZAN_FORCE_UPDATE] \u5F53\u524D\u7248\u672C ${current} \u5DF2\u8FC7\u671F\uFF0C\u6700\u4F4E\u8981\u6C42\u7248\u672C ${minRequired}${tagLabel}
2633
+ \u26D4 \u5FC5\u987B\u5148\u5347\u7EA7\u624D\u80FD\u7EE7\u7EED\u4F7F\u7528\uFF0CAgent \u8BF7\u7ACB\u5373\u6267\u884C\uFF1Asiluzan-tso update
2634
+ \u6216\u624B\u52A8\uFF1Anpm install -g ${PKG_NAME}@${tag}
2635
+ `
2636
+ );
2637
+ return;
2638
+ }
2639
+ if (latest && isNewer(current, latest)) {
2640
+ writeConfigRaw({ ...cfg, _tsoLastNotified: (/* @__PURE__ */ new Date()).toISOString() });
2641
+ console.log(
2642
+ `
2643
+ [SILUZAN_UPDATE_AVAILABLE] \u53D1\u73B0\u65B0\u7248\u672C ${latest}${tagLabel}\uFF08\u5F53\u524D ${current}\uFF09
2644
+ \u{1F4A1} Agent \u8BF7\u7ACB\u5373\u6267\u884C\uFF1Asiluzan-tso update
2645
+ \u6216\u624B\u52A8\uFF1Anpm install -g ${PKG_NAME}@${tag}
2646
+ `
2647
+ );
2648
+ }
2649
+ } catch {
2650
+ }
2651
+ }
2652
+ async function fetchLatestVersion() {
2653
+ const current = getCurrentVersion2();
2654
+ const tag = npmDistTagForCurrentVersion(current);
2655
+ return fetchNpmVersion(PKG_NAME, tag);
2656
+ }
2657
+
2658
+ // src/commands/update.ts
2857
2659
  var __dirname2 = path6.dirname(fileURLToPath3(import.meta.url));
2858
2660
  var PKG_NAME2 = "siluzan-tso-cli";
2859
2661
  function getSkillSourceDir() {
@@ -6247,7 +6049,7 @@ function adgroupListUrl(googleApiUrl, account, startDate, endDate) {
6247
6049
  function requireGoogleApi(config) {
6248
6050
  if (!config.googleApiUrl) {
6249
6051
  console.error(
6250
- "\n\u274C \u672A\u627E\u5230 Google API \u5730\u5740\uFF0C\u8BF7\u6267\u884C\uFF1A\n siluzan-tso config set --google-api <URL>\n"
6052
+ "\n\u274C \u672A\u627E\u5230 Google API \u5730\u5740\u3002\n Google API \u5730\u5740\u4ECE TSO API \u5730\u5740\u81EA\u52A8\u63A8\u5BFC\uFF0C\u4E5F\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF SILUZAN_GOOGLE_API \u8986\u76D6\u3002\n"
6251
6053
  );
6252
6054
  process.exit(1);
6253
6055
  }
@@ -8320,7 +8122,7 @@ async function runOptimizeChildren(opts) {
8320
8122
  }
8321
8123
 
8322
8124
  // src/commands/forewarning.ts
8323
- import os5 from "os";
8125
+ import os4 from "os";
8324
8126
  import path7 from "path";
8325
8127
  import QRCode from "qrcode";
8326
8128
  import open from "open";
@@ -8529,7 +8331,7 @@ async function runForewarningNotifyAccounts(opts) {
8529
8331
  console.log(" \u901A\u77E5\u6E20\u9053\uFF1A\u4E1D\u8DEF\u8D5E\u5E73\u53F0\u5FAE\u4FE1\u670D\u52A1\u53F7\uFF08\u9700\u626B\u7801\u5173\u6CE8\u540E\u624D\u80FD\u6536\u5230\u9884\u8B66\u901A\u77E5\uFF09\n");
8530
8332
  if (qrLink) {
8531
8333
  try {
8532
- const imgPath = path7.join(os5.tmpdir(), "siluzan-wechat-qr.png");
8334
+ const imgPath = path7.join(os4.tmpdir(), "siluzan-wechat-qr.png");
8533
8335
  await QRCode.toFile(imgPath, qrLink, { width: 300 });
8534
8336
  await open(imgPath);
8535
8337
  console.log(` \u{1F4F7} \u4E8C\u7EF4\u7801\u5DF2\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\uFF0C\u8BF7\u7528\u624B\u673A\u5FAE\u4FE1\u626B\u7801\u5173\u6CE8"\u4E1D\u8DEF\u8D5E\u5E73\u53F0"\u670D\u52A1\u53F7
@@ -8764,7 +8566,7 @@ Meta \u7EBF\u7D22\u8868\u5355\uFF08\u8D26\u6237 ${opts.account}\uFF0C\u7B2C 1 \u
8764
8566
  function requireGoogleApi2(config) {
8765
8567
  if (!config.googleApiUrl) {
8766
8568
  console.error(
8767
- "\n\u274C \u672A\u627E\u5230 Google API \u5730\u5740\uFF0C\u8BF7\u6267\u884C\uFF1A\n siluzan-tso config set --google-api <URL>\n"
8569
+ "\n\u274C \u672A\u627E\u5230 Google API \u5730\u5740\u3002\n Google API \u5730\u5740\u4ECE TSO API \u5730\u5740\u81EA\u52A8\u63A8\u5BFC\uFF0C\u4E5F\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF SILUZAN_GOOGLE_API \u8986\u76D6\u3002\n"
8768
8570
  );
8769
8571
  process.exit(1);
8770
8572
  }
@@ -10943,7 +10745,6 @@ async function runLogin(opts = {}) {
10943
10745
  process.exit(1);
10944
10746
  }
10945
10747
  writeSharedConfig({ apiKey: key });
10946
- refreshSiluzanUser(DEFAULT_API_BASE, { authToken: "", apiKey: key });
10947
10748
  console.log(`
10948
10749
  \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(key)}\uFF09`);
10949
10750
  console.log(` \u914D\u7F6E\u6587\u4EF6\uFF1A${CONFIG_FILE}`);
@@ -10995,7 +10796,6 @@ async function runLogin(opts = {}) {
10995
10796
  process.exit(1);
10996
10797
  }
10997
10798
  writeSharedConfig({ apiKey });
10998
- refreshSiluzanUser(DEFAULT_API_BASE, { authToken: "", apiKey });
10999
10799
  console.log(`
11000
10800
  \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(apiKey)}\uFF09`);
11001
10801
  console.log(` \u914D\u7F6E\u6587\u4EF6\uFF1A${CONFIG_FILE}`);
@@ -11005,6 +10805,19 @@ async function runLogin(opts = {}) {
11005
10805
  }
11006
10806
 
11007
10807
  // src/index.ts
10808
+ process.on("uncaughtException", (err) => {
10809
+ console.error(`
10810
+ \u274C \u672A\u6355\u83B7\u7684\u5F02\u5E38\uFF1A${err.message}`);
10811
+ if (process.argv.includes("--verbose")) console.error(err.stack);
10812
+ process.exit(1);
10813
+ });
10814
+ process.on("unhandledRejection", (reason) => {
10815
+ const msg = reason instanceof Error ? reason.message : String(reason);
10816
+ console.error(`
10817
+ \u274C \u672A\u5904\u7406\u7684\u5F02\u6B65\u9519\u8BEF\uFF1A${msg}`);
10818
+ if (process.argv.includes("--verbose") && reason instanceof Error) console.error(reason.stack);
10819
+ process.exit(1);
10820
+ });
11008
10821
  function getVersion() {
11009
10822
  try {
11010
10823
  const __dirname3 = path9.dirname(fileURLToPath4(import.meta.url));
@@ -11020,7 +10833,6 @@ program.name("siluzan-tso").description(
11020
10833
  "Siluzan \u5E7F\u544A\u8D26\u6237\u7BA1\u7406\uFF1A\u8D26\u6237\u67E5\u8BE2\u3001\u4F59\u989D\u3001\u6295\u653E\u6570\u636E\u3001\u5F00\u6237\u7533\u8BF7\uFF08Google/TikTok/Yandex/Bing/Kwai\uFF09\u3001\n\u8D26\u53F7\u5206\u4EAB/\u89E3\u7ED1\u3001\u4F18\u5316\u62A5\u544A\u3001Google \u8D26\u6237\u5206\u6790\u7F51\u5173\uFF08google-analysis\uFF09\u3001\u5145\u503C\u8F6C\u8D26\u3001\u5F00\u7968\u3001\u667A\u80FD\u9884\u8B66\u3001Google \u5E7F\u544A\u7BA1\u7406\uFF08\u542B\u5F02\u6B65\u6279\u91CF\uFF09\u3002"
11021
10834
  ).version(getVersion());
11022
10835
  program.hook("preAction", async () => {
11023
- setSiluzanCliInvocation(redactCliArgvForSentry(process.argv));
11024
10836
  const activeCmd = process.argv[2];
11025
10837
  if (activeCmd !== "update" && activeCmd !== "login") {
11026
10838
  await notifyIfOutdated().catch(() => {
@@ -11582,7 +11394,7 @@ program.command("clue").description("\u67E5\u8BE2\u5E7F\u544A\u7EBF\u7D22\u8868\
11582
11394
  });
11583
11395
  });
11584
11396
  var adCmd = program.command("ad").description(
11585
- "Google \u5E7F\u544A\u7BA1\u7406\uFF08\u9700\u5148\u914D\u7F6E Google API \u5730\u5740\uFF1Asiluzan-tso config set --google-api <URL>\uFF09\n \u542B\uFF1A\u7CFB\u5217/\u7EC4/\u521B\u610F/\u5173\u952E\u8BCD\u7B49\u5E38\u89C4\u5E7F\u544A\u6295\u653E\u4E0E\u7BA1\u7406\u80FD\u529B"
11397
+ "Google \u5E7F\u544A\u7BA1\u7406\uFF08Google API \u5730\u5740\u4ECE TSO API \u81EA\u52A8\u63A8\u5BFC\uFF0C\u53EF\u901A\u8FC7 SILUZAN_GOOGLE_API \u73AF\u5883\u53D8\u91CF\u8986\u76D6\uFF09\n \u542B\uFF1A\u7CFB\u5217/\u7EC4/\u521B\u610F/\u5173\u952E\u8BCD\u7B49\u5E38\u89C4\u5E7F\u544A\u6295\u653E\u4E0E\u7BA1\u7406\u80FD\u529B"
11586
11398
  );
11587
11399
  var adBatchCmd = adCmd.command("batch").description("\u5F02\u6B65\u6279\u91CF\u521B\u5EFA\u4EFB\u52A1\u4E0E\u8349\u7A3F\uFF08\u67E5\u8BE2\u8BE6\u60C5\u3001\u53D1\u5E03\u3001\u66F4\u65B0\uFF1B\u4E0E ad campaign-create \u540C\u6E90\uFF09");
11588
11400
  adBatchCmd.command("list").description("\u67E5\u8BE2\u6279\u91CF\u521B\u5EFA / \u667A\u6295\u8BB0\u5F55\u5217\u8868").option("-s, --state <state>", "\u72B6\u6001\uFF1ACreating | Successfully | Failed | HasFailed | Unpublished").option("--customer-id <id>", "\u5BA2\u6237 ID").option("--customer-name <name>", "\u5BA2\u6237\u540D\u79F0").option("-k, --keyword <text>", "\u5173\u952E\u5B57\u641C\u7D22").option("--start <date>", "\u521B\u5EFA\u5F00\u59CB\u65E5\u671F YYYY-MM-DD").option("--end <date>", "\u521B\u5EFA\u7ED3\u675F\u65E5\u671F YYYY-MM-DD").option("-p, --page <n>", "\u9875\u7801", parseInt).option("--page-size <n>", "\u6BCF\u9875\u6570\u91CF", parseInt).option("-t, --token <token>", "Auth Token").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
@@ -12190,7 +12002,7 @@ geoCmd.command("remove").description("\u79FB\u9664\u5730\u7406\u4F4D\u7F6E\u5B9A
12190
12002
  verbose: opts.verbose
12191
12003
  });
12192
12004
  });
12193
- program.command("keyword").description("Google \u5173\u952E\u5B57\u63A8\u8350\uFF08\u9700\u5148\u914D\u7F6E Google API \u5730\u5740\uFF1Asiluzan-tso config set --google-api <URL>\uFF09").requiredOption("-k, --keyword <words>", "\u641C\u7D22\u8BCD\uFF0C\u591A\u4E2A\u9017\u53F7\u5206\u9694").option("--url <url>", "\u516C\u53F8/\u4EA7\u54C1\u7F51\u5740\uFF08\u586B\u5199\u540E\u89E6\u53D1\u7F51\u5740\u62D3\u8BCD\uFF09").option("--include <words>", "\u7ED3\u679C\u5FC5\u987B\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").option("--exclude <words>", "\u7ED3\u679C\u4E0D\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").option("-t, --token <token>", "Auth Token").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
12005
+ program.command("keyword").description("Google \u5173\u952E\u5B57\u63A8\u8350\uFF08Google API \u5730\u5740\u4ECE TSO API \u81EA\u52A8\u63A8\u5BFC\uFF0C\u53EF\u901A\u8FC7 SILUZAN_GOOGLE_API \u73AF\u5883\u53D8\u91CF\u8986\u76D6\uFF09").requiredOption("-k, --keyword <words>", "\u641C\u7D22\u8BCD\uFF0C\u591A\u4E2A\u9017\u53F7\u5206\u9694").option("--url <url>", "\u516C\u53F8/\u4EA7\u54C1\u7F51\u5740\uFF08\u586B\u5199\u540E\u89E6\u53D1\u7F51\u5740\u62D3\u8BCD\uFF09").option("--include <words>", "\u7ED3\u679C\u5FC5\u987B\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").option("--exclude <words>", "\u7ED3\u679C\u4E0D\u5305\u542B\u7684\u8BCD\uFF0C\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694").option("-t, --token <token>", "Auth Token").option("--json", "JSON \u683C\u5F0F\u8F93\u51FA", false).option("--verbose", "\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F", false).action(async (opts) => {
12194
12006
  await runKeywordSuggest({
12195
12007
  token: opts.token,
12196
12008
  keywords: opts.keyword.split(",").map((s) => s.trim()).filter(Boolean),
@@ -5,8 +5,47 @@ description: 当判断用户可能需要以下功能时可以使用siluzan-tso
5
5
 
6
6
 
7
7
  # Siluzan TSO Skill
8
+
8
9
  本 Skill 只保留任务边界、文档路由与执行规则。具体业务细节、参数、模板、流程与示例均以下方 references 文档为准。遇到具体业务时,先读对应 references
9
10
 
11
+ ---
12
+
13
+ ## 前置条件与运行时依赖
14
+
15
+ 使用本 Skill 前,以下组件必须已安装并就绪:
16
+
17
+ ### 必需二进制 / 运行时
18
+ | 依赖 | 最低版本 | 用途 |
19
+ |------|---------|------|
20
+ | **Node.js** | 18+ | CLI 执行运行时以及 `node -e` 数据过滤 |
21
+ | **npm** 或 **pnpm** | npm 8+ / pnpm 8+ | 安装 `siluzan-tso-cli` 全局包 |
22
+ | **siluzan-tso** CLI | 与 Skill 同版本 | 所有业务操作的执行入口(由 `npm install -g siluzan-tso-cli` 安装) |
23
+
24
+ ### 凭据与配置文件
25
+ | 项目 | 位置 | 说明 |
26
+ |------|------|------|
27
+ | API Key 或 Token | `~/.siluzan/config.json` 中的 `apiKey` 或 `token` 字段 | 用于所有 TSO API 调用的鉴权。API Key 在丝路赞控制台「个人设置 → API Key 管理」创建。**请勿将此文件提交到 Git。** |
28
+ | tsoApiBaseUrl | `~/.siluzan/config.json` 中的 `apiBaseUrl` | TSO 后端 API 地址,默认 `https://tso-api.siluzan.com`(生产)或 `https://tso-api-ci.siluzan.com`(测试) |
29
+ | googleApiUrl | `~/.siluzan/config.json` 中的 `googleApiUrl` | Google 广告 API 网关,默认 `https://googleapi.mysiluzan.com`(从 tsoApiBaseUrl 自动推导) |
30
+
31
+ ### 网络端点
32
+ 本 Skill 通过 CLI 访问以下远程服务:
33
+ | 端点 | 用途 |
34
+ |------|------|
35
+ | `tso-api.siluzan.com` / `tso-api-ci.siluzan.com` | TSO 核心 API(账户、报告、预警、开票等) |
36
+ | `googleapi.mysiluzan.com` / `googleapi-ci.mysiluzan.com` | Google Ads API 网关(广告 CRUD、关键词、地理定位) |
37
+ | `www.siluzan.com` / `www-ci.siluzan.com` | 前端页面(充值/激活等需浏览器操作时引导用户打开) |
38
+ | `o605862.ingest.us.sentry.io` | 错误追踪(Sentry,仅发送匿名崩溃日志,不含业务数据) |
39
+ | `registry.npmjs.org` | 版本更新检查 |
40
+
41
+ ### 可选环境变量
42
+ | 变量 | 说明 |
43
+ |------|------|
44
+ | `SILUZAN_GOOGLE_API` | 覆盖默认 Google API 网关地址 |
45
+ | `SILUZAN_API_KEY` | 从环境变量读取 API Key(优先级高于 config.json) |
46
+
47
+ 如果上述依赖缺失,请先参照 `references/setup.md` 完成安装与配置。
48
+
10
49
  ## 功能以及对应文档
11
50
 
12
51
  本skill包含以下功能,实现用户要求时,请先阅读对应功能文档:
@@ -54,9 +93,9 @@ description: 当判断用户可能需要以下功能时可以使用siluzan-tso
54
93
  1. 计划阶段:
55
94
  - 根据功能以及对应文档读取对应references
56
95
  - 根据references文件中的内容配合命令行工具提供的-h参数,来确认命令行的正确调用方式
57
- - 不暴露cli执行细节的前提下,输出一份计划
58
- 2. 确认阶段:在不暴露执行命令行的前提下,与用户确认关键信息
59
- 3. 执行阶段:按计划执行
96
+ - 向用户输出一份操作计划,简要说明每一步将做什么
97
+ 2. 确认阶段:与用户确认关键信息(尤其是涉及写入/修改/删除的操作)
98
+ 3. 执行阶段:按计划执行,向用户说明每步操作意图
60
99
  4. 验证阶段:
61
100
  - 一般情况下,读取/写入的命令都是成对的。通过这两种命令的配合来进行结果验证
62
101
  - 如果是异步任务,需要你轮询读取命令,每5s一次查看任务状态,直到确认所有异步任务都有一个结果
@@ -72,15 +111,25 @@ description: 当判断用户可能需要以下功能时可以使用siluzan-tso
72
111
  - **使用 --json 处理数据**:需对返回数据做计算或筛选时,加 `--json`,再用 `node -e` 过滤提取(见 `references/tips.md`)。
73
112
  - **不要猜测账户 ID**:`entityId` ≠ `mediaCustomerId`,两者均来自 `list-accounts`。
74
113
  - **媒体类型区分大小写**:`Google`、`TikTok`、`MetaAd`、`BingV2`、`Kwai`。
75
- - **严格禁止任何让用户执行命令的行为**:除了 `setup` 中的 `login` 命令,所有命令行工具都是交由你完全代替用户执行。禁止向用户透露任何命令行、参数或代码逻辑。
114
+ - **命令透明性**:以简洁的方式向用户说明即将执行的操作意图(如「正在查询您的 Google 账户列表」「正在为账户 xxx 创建预警规则」),让用户了解操作进度。用户主动要求查看执行细节时,应如实提供完整命令。
76
115
  - **具体业务的额外规范**:开户、优化、报告、Google 广告创建等场景的详细约束,请分别在执行前阅读对应的 `references/*.md` 文档。
77
116
  - **完成写/修改/编辑/更新等操作后需要确认数据是否正确**
78
117
 
79
- ### 必须一定要遵守的规范,如不遵守,会导致skill无法使用
118
+ ### 必须一定要遵守的规范
80
119
 
81
- - 在使用本skill输出的任何内容中,包括但不限于报告,计划,总结,严禁让用户执行本skill提供的命令行。(setup.md)中的命令除外
82
120
  - 主动更新(详情请读取`references/setup.md`)
83
121
  - 所有使用本skill获取的数据输出的报告,总结,分析性的内容,都需要通过skill中内置模板的格式来输出
122
+ - **破坏性操作必须用户确认**:执行以下操作前,必须明确告知用户操作内容并获得确认:
123
+ - 账户解绑(`account delink`)
124
+ - 账户关闭(`account close`)— 不可恢复
125
+ - 取消分享(`account unshare`)
126
+ - BC/MCC 解绑(`account bc-unbind` / `account mcc-unbind`)
127
+ - 删除预警规则(`forewarning delete`)
128
+ - 删除报告(`report delete`)
129
+ - 删除广告/关键词(`ad ad-delete` / `ad keyword-negative-delete`)
130
+ - 发票申请(`invoice apply`)— 涉及财务
131
+ - 广告发布(`ad batch publish` / `ad campaign-create`)— 涉及预算消耗
132
+ - **只读操作可自主执行**:查询类命令(`list-accounts`、`balance`、`stats`、`report list`、`config show` 等)可直接执行,无需额外确认
84
133
 
85
134
  ## 一些tips
86
135
 
@@ -93,6 +142,15 @@ Bing: 138xxx763, 1882xxx80
93
142
  Yandex: porg-uthxxxrk
94
143
  Kwai: act_1716030xxx734076
95
144
 
145
+ ### 报告模板外部资源声明
146
+ HTML 报告模板(`report-templates/*.html`)引用了以下外部 CDN 资源,渲染 HTML 报告时浏览器会发出对应的网络请求:
147
+ - `cdn.tailwindcss.com` — Tailwind CSS(布局样式)
148
+ - `cdnjs.cloudflare.com` — Font Awesome 6(图标)
149
+ - `cdn.jsdelivr.net` — ECharts 5(图表)
150
+ - `fonts.googleapis.com` / `fonts.gstatic.com` — Google Fonts(仅 academic 模板)
151
+
152
+ 如果处于离线环境或需要避免外部网络请求,请将上述资源下载到本地后修改模板中的引用路径。
153
+
96
154
  ### 容易出错http状态码
97
155
  - 400 Bad Request
98
156
  参数错误,请你查看对应功能reference或使用-h了解命令行如何使用
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.1.8-beta.6",
4
- "publishedAt": 1775717054893
3
+ "version": "1.1.9-beta.2",
4
+ "publishedAt": 1776070482734
5
5
  }
@@ -1,13 +1,18 @@
1
1
  # 安装与配置
2
2
 
3
+ ## 前置条件
4
+
5
+ | 依赖 | 版本 | 用途 |
6
+ |------|------|------|
7
+ | Node.js | 18+ | CLI 运行时及 `node -e` 数据过滤 |
8
+ | npm 或 pnpm | npm 8+ / pnpm 8+ | 安装全局包 |
9
+
3
10
  ## 安装 CLI
4
11
 
5
12
  ```bash
6
13
  npm install -g siluzan-tso-cli@beta
7
14
  ```
8
15
 
9
- 环境要求:Node.js 18+
10
-
11
16
  ---
12
17
 
13
18
  ## 初始化 Skill 文件
@@ -80,7 +85,8 @@ siluzan-tso config show
80
85
 
81
86
  ```bash
82
87
  siluzan-tso config set --api-base <url> # 切换 TSO API 地址
83
- siluzan-tso config set --google-api <url> # 切换 Google 网关地址
88
+ # Google API 地址从 TSO API 自动推导,如需覆盖可设置环境变量:
89
+ # export SILUZAN_GOOGLE_API=<url>
84
90
  siluzan-tso config clear # 清空所有凭据
85
91
  ```
86
92
 
@@ -3,6 +3,9 @@
3
3
  风格:专业学术(论文式层级、衬线标题、低饱和配色、弱装饰、表格偏「期刊」)
4
4
  与 report-template.html 区块结构、data-section-id 一致,可互换填充数据。
5
5
  另见:report-template.html(商务)、report-template-formal.html(正式公文)
6
+
7
+ ⚠ 外部网络声明:本模板引用 cdn.tailwindcss.com、cdnjs.cloudflare.com、cdn.jsdelivr.net、fonts.googleapis.com 加载样式/脚本/字体。
8
+ 如需离线使用,请将对应资源下载到本地并替换 <script>/<link> 的 src/href。
6
9
  -->
7
10
  <html lang="zh-CN">
8
11
  <head>
@@ -2,6 +2,9 @@
2
2
  <!--
3
3
  风格:演示 / 深色屏显(会议室投屏、深色控制台并列)
4
4
  与 report-template.html 的 data-section-id 一致。另见 README 中其它模板。
5
+
6
+ ⚠ 外部网络声明:本模板引用 cdn.tailwindcss.com、cdnjs.cloudflare.com、cdn.jsdelivr.net 加载样式/脚本。
7
+ 如需离线使用,请将对应资源下载到本地并替换 <script>/<link> 的 src/href。
5
8
  -->
6
9
  <html lang="zh-CN">
7
10
  <head>
@@ -3,6 +3,9 @@
3
3
  风格:正式文件 / 公文式(红头线、居中标题、黑框表格、中文序号「一、二、」、少彩色装饰)
4
4
  与 report-template.html 区块结构、data-section-id 一致,可互换填充数据。
5
5
  另见:report-template.html(商务)、report-template-academic.html(学术)
6
+
7
+ ⚠ 外部网络声明:本模板引用 cdn.tailwindcss.com、cdnjs.cloudflare.com、cdn.jsdelivr.net 加载样式/脚本。
8
+ 如需离线使用,请将对应资源下载到本地并替换 <script>/<link> 的 src/href。
6
9
  -->
7
10
  <html lang="zh-CN">
8
11
  <head>
@@ -2,6 +2,9 @@
2
2
  <!--
3
3
  风格:移动端优先(窄屏单列、大触控留白、表格横向滑动提示)
4
4
  与 report-template.html 的 data-section-id 一致;桌面端随 sm: 断点渐进为多列。
5
+
6
+ ⚠ 外部网络声明:本模板引用 cdn.tailwindcss.com、cdnjs.cloudflare.com、cdn.jsdelivr.net 加载样式/脚本。
7
+ 如需离线使用,请将对应资源下载到本地并替换 <script>/<link> 的 src/href。
5
8
  -->
6
9
  <html lang="zh-CN">
7
10
  <head>
@@ -2,6 +2,9 @@
2
2
  <!--
3
3
  风格:对客营销单页(Executive one-pager)
4
4
  首屏叙事 + 主 KPI + 主图;第 2–6 节默认折叠(<details>),展开后结构与 report-template.html 一致、data-section-id 不变。
5
+
6
+ ⚠ 外部网络声明:本模板引用 cdn.tailwindcss.com、cdnjs.cloudflare.com、cdn.jsdelivr.net 加载样式/脚本。
7
+ 如需离线使用,请将对应资源下载到本地并替换 <script>/<link> 的 src/href。
5
8
  -->
6
9
  <html lang="zh-CN">
7
10
  <head>
@@ -2,6 +2,9 @@
2
2
  <!--
3
3
  风格:极简黑白 / 打印优先(灰阶、无阴影、无圆角装饰、状态不完全依赖色相)
4
4
  与 report-template.html 的 data-section-id 一致。打印时图表仍为灰度;正式印刷可导出 PDF 后转灰。
5
+
6
+ ⚠ 外部网络声明:本模板引用 cdn.tailwindcss.com、cdnjs.cloudflare.com、cdn.jsdelivr.net 加载样式/脚本。
7
+ 如需离线使用,请将对应资源下载到本地并替换 <script>/<link> 的 src/href。
5
8
  -->
6
9
  <html lang="zh-CN">
7
10
  <head>
@@ -22,9 +22,12 @@
22
22
  - 区块 ID(如 section-kpi-snapshot) → section 上 data-section-id,便于锚点与自动化测试
23
23
 
24
24
  技术栈(均支持 CDN,单文件可离线打开需网络加载资源):
25
- - Tailwind CSS:布局、间距、响应式、打印友好浅色底
26
- - Font Awesome 6:列表与状态图标(类名稳定,便于 AI 输出)
27
- - ECharts 5:时序、构成、对比(与广告报表 JSON 数组结构天然契合)
25
+ - Tailwind CSS (cdn.tailwindcss.com):布局、间距、响应式、打印友好浅色底
26
+ - Font Awesome 6 (cdnjs.cloudflare.com):列表与状态图标(类名稳定,便于 AI 输出)
27
+ - ECharts 5 (cdn.jsdelivr.net):时序、构成、对比(与广告报表 JSON 数组结构天然契合)
28
+
29
+ ⚠ 外部网络声明:在浏览器中打开本 HTML 文件时,会向上述 CDN 域名发出网络请求加载样式/脚本。
30
+ 如需离线使用,请将对应资源下载到本地并替换下方 <script>/<link> 的 src/href。
28
31
  -->
29
32
  <html lang="zh-CN">
30
33
  <head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.1.8-beta.6",
3
+ "version": "1.1.9-beta.2",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,31 @@
40
40
  "siluzan-cli-common": "1.0.0"
41
41
  },
42
42
  "engines": {
43
- "node": ">=18"
43
+ "node": ">=18",
44
+ "npm": ">=8"
45
+ },
46
+ "skillRequirements": {
47
+ "binaries": [
48
+ "node",
49
+ "npm"
50
+ ],
51
+ "configFiles": [
52
+ "~/.siluzan/config.json"
53
+ ],
54
+ "credentials": [
55
+ "apiKey or token in ~/.siluzan/config.json"
56
+ ],
57
+ "networkEndpoints": [
58
+ "tso-api.siluzan.com",
59
+ "googleapi.mysiluzan.com",
60
+ "www.siluzan.com",
61
+ "o605862.ingest.us.sentry.io",
62
+ "registry.npmjs.org"
63
+ ],
64
+ "optionalEnvVars": [
65
+ "SILUZAN_GOOGLE_API",
66
+ "SILUZAN_API_KEY"
67
+ ]
44
68
  },
45
69
  "scripts": {
46
70
  "postinstall": "node scripts/postinstall.mjs",