siluzan-tso-cli 1.0.0-beta.16 → 1.0.0-beta.18

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
@@ -20,7 +20,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
20
20
  siluzan-tso init --force # 强制覆盖已存在文件
21
21
  ```
22
22
 
23
- > **注意**:当前为测试版(1.0.0-beta.16),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
23
+ > **注意**:当前为测试版(1.0.0-beta.18),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
24
24
 
25
25
  | 助手 | 建议 `--ai` |
26
26
  |------|-------------|
@@ -12,7 +12,7 @@ export interface SetConfigOptions {
12
12
  apiBase?: string;
13
13
  googleApi?: string;
14
14
  }
15
- /** siluzan-tso config set [--api-key <key>] [--token <token>] [--api-base <url>] */
15
+ /** siluzan-tso config set [--api-key <key>] [--token <token>] [--api-base <url>] [--google-api <url>] */
16
16
  export declare function cmdConfigSet(opts: SetConfigOptions): void;
17
17
  /** siluzan-tso config clear */
18
18
  export declare function cmdConfigClear(): void;
@@ -1,32 +1,6 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import * as os from "node:os";
4
1
  import { validateBaseUrl, BUILD_ENV } from "../utils/auth.js";
5
2
  import { DEFAULT_API_BASE, DEFAULT_GOOGLE_API } from "../config/defaults.js";
6
- const CONFIG_DIR = path.join(os.homedir(), ".siluzan");
7
- const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
8
- function readConfigFile() {
9
- if (!fs.existsSync(CONFIG_FILE))
10
- return {};
11
- try {
12
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
13
- }
14
- catch {
15
- return {};
16
- }
17
- }
18
- function writeConfigFile(cfg) {
19
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
20
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
21
- if (process.platform !== "win32") {
22
- try {
23
- fs.chmodSync(CONFIG_FILE, 0o600);
24
- }
25
- catch {
26
- console.warn("⚠️ 未能收敛配置文件权限,请手动执行:chmod 600 " + CONFIG_FILE);
27
- }
28
- }
29
- }
3
+ import { readSharedConfig, writeSharedConfig, clearSharedConfig, maskSecret, CONFIG_FILE, } from "siluzan-cli-common";
30
4
  // ─────────────────── 工具函数 ───────────────────
31
5
  /**
32
6
  * 从 TSO API 地址推导前端 Web 页面基地址。
@@ -44,16 +18,14 @@ export function deriveWebUrl(apiBaseUrl) {
44
18
  }
45
19
  }
46
20
  // ─────────────────── 子命令 ───────────────────
47
- function maskSecret(s) {
48
- return s.length > 8 ? `${s.slice(0, 4)}****${s.slice(-4)}` : "****";
49
- }
50
21
  /** siluzan-tso config show */
51
22
  export function cmdConfigShow() {
52
- const cfg = readConfigFile();
53
- const authToken = typeof cfg.authToken === "string" ? cfg.authToken : "";
54
- const apiKey = typeof cfg.apiKey === "string" && cfg.apiKey ? cfg.apiKey : "";
55
- const apiBaseUrl = typeof cfg.apiBaseUrl === "string" ? cfg.apiBaseUrl : DEFAULT_API_BASE;
56
- const googleApiUrl = typeof cfg.googleApiUrl === "string" ? cfg.googleApiUrl : DEFAULT_GOOGLE_API;
23
+ const shared = readSharedConfig();
24
+ const authToken = shared.authToken;
25
+ const apiKey = shared.apiKey ?? "";
26
+ // TSO API 地址从 config.json.tsoApiBaseUrl 读取(不与 CSO apiBaseUrl 冲突)
27
+ const apiBaseUrl = shared.tsoApiBaseUrl ?? DEFAULT_API_BASE;
28
+ const googleApiUrl = shared.googleApiUrl ?? DEFAULT_GOOGLE_API;
57
29
  const webUrl = deriveWebUrl(apiBaseUrl);
58
30
  if (!apiKey && !authToken) {
59
31
  console.log("\n尚未配置认证凭据。\n\n" +
@@ -64,27 +36,25 @@ export function cmdConfigShow() {
64
36
  return;
65
37
  }
66
38
  console.log("\n当前配置(~/.siluzan/config.json):");
67
- console.log(` 构建环境 : ${BUILD_ENV}`);
68
- console.log(` apiBaseUrl : ${apiBaseUrl}`);
69
- console.log(` googleApiUrl : ${googleApiUrl}`);
70
- console.log(` webUrl : ${webUrl} ← 前端页面基地址(引导用户跳转时使用)`);
39
+ console.log(` 构建环境 : ${BUILD_ENV}`);
40
+ console.log(` tsoApiBaseUrl : ${apiBaseUrl}`);
41
+ console.log(` googleApiUrl : ${googleApiUrl}`);
42
+ console.log(` webUrl : ${webUrl}`);
71
43
  if (apiKey) {
72
- console.log(` apiKey : ${maskSecret(apiKey)} ← 当前生效(X-Api-Key 鉴权)`);
44
+ console.log(` apiKey : ${maskSecret(apiKey)} ← 当前生效(X-Api-Key 鉴权)`);
73
45
  }
74
46
  if (authToken) {
75
- const label = apiKey ? " authToken :" : " authToken :";
76
47
  const note = apiKey ? " (已被 apiKey 覆盖)" : " ← 当前生效(Bearer 鉴权)";
77
- console.log(`${label} ${maskSecret(authToken)}${note}`);
48
+ console.log(` authToken : ${maskSecret(authToken)}${note}`);
78
49
  }
79
50
  console.log();
80
- console.log(" 切换测试环境(覆盖此次 build 的默认值):");
51
+ console.log(" 切换测试环境(写入 config.json.tsoApiBaseUrl,不影响 CSO CLI 的 apiBaseUrl):");
81
52
  console.log(" siluzan-tso config set --api-base https://tso-api-ci.siluzan.com");
82
- console.log(" siluzan-tso config set --google-api https://googleapi-ci.mysiluzan.com");
83
53
  console.log(`\n配置文件位置:${CONFIG_FILE}`);
84
54
  console.log(" ⚠️ 凭据明文存储,请勿提交至代码仓库,通过 SILUZAN_API_KEY 环境变量传入更安全。");
85
55
  console.log();
86
56
  }
87
- /** siluzan-tso config set [--api-key <key>] [--token <token>] [--api-base <url>] */
57
+ /** siluzan-tso config set [--api-key <key>] [--token <token>] [--api-base <url>] [--google-api <url>] */
88
58
  export function cmdConfigSet(opts) {
89
59
  if (!opts.token && !opts.apiKey && !opts.apiBase && !opts.googleApi) {
90
60
  console.error("\n❌ 请至少提供一个要更新的配置项(--api-key、--token、--api-base 或 --google-api)\n");
@@ -96,6 +66,8 @@ export function cmdConfigSet(opts) {
96
66
  console.error(`\n❌ --api-base 不合法:${err}\n`);
97
67
  process.exit(1);
98
68
  }
69
+ // 写入 config.json.tsoApiBaseUrl(TSO 专属字段,不影响 CSO 的 apiBaseUrl)
70
+ writeSharedConfig({ tsoApiBaseUrl: opts.apiBase });
99
71
  }
100
72
  if (opts.googleApi) {
101
73
  const err = validateBaseUrl(opts.googleApi);
@@ -103,33 +75,25 @@ export function cmdConfigSet(opts) {
103
75
  console.error(`\n❌ --google-api 不合法:${err}\n`);
104
76
  process.exit(1);
105
77
  }
78
+ writeSharedConfig({ googleApiUrl: opts.googleApi });
106
79
  }
107
- const cfg = readConfigFile();
108
80
  if (opts.apiKey)
109
- cfg.apiKey = opts.apiKey;
81
+ writeSharedConfig({ apiKey: opts.apiKey });
110
82
  if (opts.token)
111
- cfg.authToken = opts.token;
112
- if (opts.apiBase)
113
- cfg.apiBaseUrl = opts.apiBase;
114
- if (opts.googleApi)
115
- cfg.googleApiUrl = opts.googleApi;
116
- writeConfigFile(cfg);
83
+ writeSharedConfig({ authToken: opts.token });
117
84
  console.log(`\n✅ 配置已保存到 ${CONFIG_FILE}`);
118
85
  if (opts.apiKey)
119
- console.log(` apiKey: ${maskSecret(opts.apiKey)}`);
86
+ console.log(` apiKey: ${maskSecret(opts.apiKey)}`);
120
87
  if (opts.token)
121
- console.log(` authToken: ${maskSecret(opts.token)}`);
88
+ console.log(` authToken: ${maskSecret(opts.token)}`);
122
89
  if (opts.apiBase)
123
- console.log(` apiBaseUrl: ${opts.apiBase}`);
90
+ console.log(` tsoApiBaseUrl: ${opts.apiBase}`);
124
91
  if (opts.googleApi)
125
- console.log(` googleApiUrl: ${opts.googleApi}`);
92
+ console.log(` googleApiUrl: ${opts.googleApi}`);
126
93
  console.log("\n后续所有命令自动读取此配置。\n");
127
94
  }
128
95
  /** siluzan-tso config clear */
129
96
  export function cmdConfigClear() {
130
- const cfg = readConfigFile();
131
- cfg.authToken = "";
132
- cfg.apiKey = "";
133
- writeConfigFile(cfg);
97
+ clearSharedConfig();
134
98
  console.log(`\n✅ 认证凭据已清空(authToken + apiKey)(${CONFIG_FILE})\n`);
135
99
  }
@@ -1,10 +1,6 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import * as os from "node:os";
4
1
  import * as readline from "node:readline";
5
2
  import { DEFAULT_API_BASE } from "../config/defaults.js";
6
- const CONFIG_DIR = path.join(os.homedir(), ".siluzan");
7
- const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
3
+ import { writeSharedConfig, readSharedConfig, maskSecret, CONFIG_FILE } from "siluzan-cli-common";
8
4
  /**
9
5
  * 根据 TSO API 地址推导前端 Web 地址。
10
6
  * tso-api.siluzan.com → www.siluzan.com
@@ -22,37 +18,6 @@ function deriveWebBaseUrl(tsoApiBase) {
22
18
  }
23
19
  const WEB_BASE_URL = deriveWebBaseUrl(DEFAULT_API_BASE);
24
20
  const API_KEY_MANAGEMENT_URL = `${WEB_BASE_URL}/v3/foreign_trade/settings/apiKeyManagement`;
25
- // ─── 配置存储 ─────────────────────────────────────────────────────────────────
26
- function readConfigFile() {
27
- if (!fs.existsSync(CONFIG_FILE))
28
- return {};
29
- try {
30
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
31
- }
32
- catch {
33
- return {};
34
- }
35
- }
36
- function writeConfigFile(cfg) {
37
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
39
- if (process.platform !== "win32") {
40
- try {
41
- fs.chmodSync(CONFIG_FILE, 0o600);
42
- }
43
- catch {
44
- console.warn("⚠️ 请手动执行:chmod 600 " + CONFIG_FILE);
45
- }
46
- }
47
- }
48
- function saveApiKey(key) {
49
- // 保留已有配置,只更新 apiKey
50
- const cfg = { ...readConfigFile(), apiKey: key };
51
- writeConfigFile(cfg);
52
- }
53
- function maskSecret(s) {
54
- return s.length > 8 ? `${s.slice(0, 4)}****${s.slice(-4)}` : "****";
55
- }
56
21
  /**
57
22
  * siluzan-tso login [--api-key <key>]
58
23
  *
@@ -69,7 +34,7 @@ export async function runLogin(opts = {}) {
69
34
  console.error("\n❌ API Key 不能为空。\n");
70
35
  process.exit(1);
71
36
  }
72
- saveApiKey(key);
37
+ writeSharedConfig({ apiKey: key });
73
38
  console.log(`\n✅ API Key 已保存(${maskSecret(key)})`);
74
39
  console.log(` 配置文件:${CONFIG_FILE}`);
75
40
  console.log("\n现在可以运行:");
@@ -77,8 +42,8 @@ export async function runLogin(opts = {}) {
77
42
  return;
78
43
  }
79
44
  // ── 交互式 API Key 输入流程 ───────────────────────────────────────────────
80
- const existing = readConfigFile();
81
- const currentKey = existing.apiKey ?? "";
45
+ const shared = readSharedConfig();
46
+ const currentKey = shared.apiKey ?? "";
82
47
  if (currentKey) {
83
48
  console.log(`\n已检测到已保存的 API Key(${maskSecret(currentKey)})。`);
84
49
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -113,7 +78,7 @@ export async function runLogin(opts = {}) {
113
78
  console.error("\n❌ 多次输入无效,请重试。\n");
114
79
  process.exit(1);
115
80
  }
116
- saveApiKey(apiKey);
81
+ writeSharedConfig({ apiKey });
117
82
  console.log(`\n✅ API Key 已保存(${maskSecret(apiKey)})`);
118
83
  console.log(` 配置文件:${CONFIG_FILE}`);
119
84
  console.log("\n现在可以运行:");
@@ -73,7 +73,7 @@ description: >-
73
73
  | 查广告主组(TikTok 等取 magKey) | `siluzan-tso open-account list-groups` |
74
74
  | Google 开户(交互,推荐) | `siluzan-tso open-account google-wizard`(对齐 `/openAnAccount` 五步说明 + 两步表单) |
75
75
  | Google 开户(脚本) | `siluzan-tso open-account google --company "…" --promotion-link "…" --promotion-type b2c ...`(**无需 magKey**) |
76
- | Google 开户时区列表 | `siluzan-tso open-account google-timezones`(与网页下拉同源,可加 `--keyword`) |
76
+ | Google 开户时区列表 | `siluzan-tso open-account google-timezones`(可加 `--keyword`) |
77
77
  | TikTok 开户时区列表 | `siluzan-tso open-account tiktok-timezones`(可加 `--keyword`) |
78
78
  | TikTok 行业列表 | `siluzan-tso open-account tiktok-industries`(两级结构,传叶子节点 ID) |
79
79
  | TikTok 注册地列表 | `siluzan-tso open-account tiktok-areas`(`--registered-area` 的合法值) |
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "slug": "siluzan-tso",
3
- "version": "1.0.0-beta.16",
4
- "publishedAt": 1774349583979
3
+ "version": "1.0.0-beta.18",
4
+ "publishedAt": 1774402366053
5
5
  }
@@ -1,40 +1,27 @@
1
1
  import type { AdsConfig } from "../types/ads.js";
2
2
  import { BUILD_ENV } from "../config/defaults.js";
3
+ import { validateBaseUrl, sleep, pollUntil, rawRequest } from "siluzan-cli-common";
4
+ import type { HttpConfig } from "siluzan-cli-common";
5
+ export { BUILD_ENV, validateBaseUrl, sleep, pollUntil, rawRequest };
3
6
  /**
4
7
  * 从主平台权限菜单接口获取 TSO 数据权限标识(dp)。
5
8
  * 首次调用后应由调用方写入 config.json 缓存,避免重复请求。
6
9
  */
7
10
  export declare function fetchDataPermission(mainApiUrl: string, authToken: string): Promise<string>;
8
- export { BUILD_ENV };
9
11
  /**
10
- * 校验 API 基地址是否在白名单范围内。
11
- * 要求:HTTPS 协议 + 主机名属于白名单域名。
12
- * 返回错误原因字符串;合法则返回 null。
13
- */
14
- export declare function validateBaseUrl(raw: string): string | null;
15
- /**
16
- * 从 ~/.siluzan/config.json 加载配置(与 siluzan-cso-cli 共享同一文件)。
12
+ * ~/.siluzan/config.json 加载配置。
17
13
  *
18
- * 鉴权优先级(由高到低):
19
- * 1. API Key:SILUZAN_API_KEY 环境变量 → config.json apiKey 字段
20
- * 2. JWT Token:tokenArg(--token 参数)→ SILUZAN_AUTH_TOKEN 环境变量 → config.json authToken
14
+ * 鉴权优先级:
15
+ * 1. API Key:SILUZAN_API_KEY 环境变量 → config.json.apiKey
16
+ * 2. JWT Token:tokenArg SILUZAN_AUTH_TOKEN 环境变量 → config.json.authToken
21
17
  *
22
- * API Key 与 Token 二选一即可,API Key 优先级更高。
18
+ * API 地址:
19
+ * - tsoApiBaseUrl:SILUZAN_TSO_API_BASE 环境变量 → config.json.tsoApiBaseUrl → 内置默认值
20
+ * - googleApiUrl:SILUZAN_GOOGLE_API 环境变量 → config.json.googleApiUrl → 内置默认值
23
21
  */
24
22
  export declare function loadConfig(tokenArg?: string): AdsConfig;
25
23
  /**
26
- * 带认证头的 HTTP 请求封装。
27
- * 使用 rawRequest(基于 https 模块)而非 Node 18 内置 fetch,
28
- * 避免因 Sec-Fetch-Mode: cors 头导致服务端 CORS 中间件剥除 Datapermission 等自定义头。
29
- * @param verbose 为 true 时在错误信息中附加原始响应片段(默认 false)
30
- */
31
- export declare function apiFetch<T>(url: string, config: AdsConfig, options?: RequestInit, verbose?: boolean): Promise<T>;
32
- /** 休眠指定毫秒(用于轮询等待) */
33
- export declare function sleep(ms: number): Promise<void>;
34
- /**
35
- * 带超时的轮询函数。
36
- * @param fn 每轮执行的异步函数,返回 null 表示继续等待,返回值表示完成
37
- * @param intervalMs 轮询间隔(毫秒)
38
- * @param timeoutMs 最大等待时间(毫秒)
24
+ * 带认证头的 HTTP 请求封装(使用公共实现)。
25
+ * AdsConfig 满足 HttpConfig 接口(含 authToken / apiKey / dataPermission 字段)。
39
26
  */
40
- export declare function pollUntil<T>(fn: () => Promise<T | null>, intervalMs: number, timeoutMs: number): Promise<T | null>;
27
+ export declare function apiFetch<T>(url: string, config: HttpConfig, options?: RequestInit, verbose?: boolean): Promise<T>;
@@ -1,9 +1,6 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import * as os from "node:os";
4
- import * as https from "node:https";
5
- import * as http from "node:http";
6
1
  import { DEFAULT_API_BASE, DEFAULT_GOOGLE_API, BUILD_ENV } from "../config/defaults.js";
2
+ import { readSharedConfig, validateBaseUrl, sleep, pollUntil, apiFetch as _apiFetch, rawRequest, } from "siluzan-cli-common";
3
+ export { BUILD_ENV, validateBaseUrl, sleep, pollUntil, rawRequest };
7
4
  // ─────────────────── 主 API 推导 ───────────────────
8
5
  /**
9
6
  * 从 TSO API 地址推导主平台 API 地址。
@@ -20,11 +17,6 @@ function deriveMainApiUrl(tsoApiBaseUrl) {
20
17
  return "";
21
18
  }
22
19
  }
23
- /**
24
- * 根据 TSO API 地址判断是否为 CI 环境,推导媒体网关地址。
25
- * tso-api-ci.siluzan.com → tiktokapi-ci.mysiluzan.com
26
- * tso-api.siluzan.com → tiktokapi.mysiluzan.com
27
- */
28
20
  function deriveTiktokApiUrl(tsoApiBaseUrl) {
29
21
  return tsoApiBaseUrl.includes("-ci")
30
22
  ? "https://tiktokapi-ci.mysiluzan.com"
@@ -41,9 +33,7 @@ function deriveChatGptApiUrl(tsoApiBaseUrl) {
41
33
  : "https://chatgpt.mysiluzan.com";
42
34
  }
43
35
  // ─────────────────── 数据权限获取 ───────────────────
44
- /** 已知的一个 TSO 路由路径,用于在权限菜单中定位 dp 字段 */
45
36
  const TSO_KNOWN_ROUTE = "/v2/foreign_trade/recharge/invoiceManagement/invoiceList";
46
- /** 递归在菜单树中按 URL 查找 dp */
47
37
  function findDpByUrl(nodes, targetUrl) {
48
38
  for (const node of nodes) {
49
39
  if (node.url === targetUrl && node.dp)
@@ -76,67 +66,26 @@ export async function fetchDataPermission(mainApiUrl, authToken) {
76
66
  return "";
77
67
  }
78
68
  }
79
- export { BUILD_ENV };
80
- // ─────────────────── URL 白名单校验 ───────────────────
81
- /**
82
- * 允许的域名后缀。
83
- * mysiluzan.com 用于 Google/TikTok/Facebook/YouTube 等第三方媒体网关。
84
- */
85
- const ALLOWED_HOSTNAME_SUFFIXES = ["siluzan.com", "siluzan.cn", "mysiluzan.com"];
86
- /**
87
- * 校验 API 基地址是否在白名单范围内。
88
- * 要求:HTTPS 协议 + 主机名属于白名单域名。
89
- * 返回错误原因字符串;合法则返回 null。
90
- */
91
- export function validateBaseUrl(raw) {
92
- let url;
93
- try {
94
- url = new URL(raw);
95
- }
96
- catch {
97
- return `不是合法 URL:${raw}`;
98
- }
99
- if (url.protocol !== "https:") {
100
- return `必须使用 HTTPS,当前协议:${url.protocol}`;
101
- }
102
- const hostname = url.hostname.toLowerCase();
103
- const ok = ALLOWED_HOSTNAME_SUFFIXES.some((suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`));
104
- if (!ok) {
105
- return (`主机名 "${hostname}" 不在允许列表(${ALLOWED_HOSTNAME_SUFFIXES.join("、")})内。` +
106
- `\n 如需连接自定义部署端点,请联系管理员添加白名单。`);
107
- }
108
- return null;
109
- }
110
69
  // ─────────────────── 配置加载 ───────────────────
111
70
  /**
112
- * 从 ~/.siluzan/config.json 加载配置(与 siluzan-cso-cli 共享同一文件)。
71
+ * 从 ~/.siluzan/config.json 加载配置。
113
72
  *
114
- * 鉴权优先级(由高到低):
115
- * 1. API Key:SILUZAN_API_KEY 环境变量 → config.json apiKey 字段
116
- * 2. JWT Token:tokenArg(--token 参数)→ SILUZAN_AUTH_TOKEN 环境变量 → config.json authToken
73
+ * 鉴权优先级:
74
+ * 1. API Key:SILUZAN_API_KEY 环境变量 → config.json.apiKey
75
+ * 2. JWT Token:tokenArg SILUZAN_AUTH_TOKEN 环境变量 → config.json.authToken
117
76
  *
118
- * API Key 与 Token 二选一即可,API Key 优先级更高。
77
+ * API 地址:
78
+ * - tsoApiBaseUrl:SILUZAN_TSO_API_BASE 环境变量 → config.json.tsoApiBaseUrl → 内置默认值
79
+ * - googleApiUrl:SILUZAN_GOOGLE_API 环境变量 → config.json.googleApiUrl → 内置默认值
119
80
  */
120
81
  export function loadConfig(tokenArg) {
121
- const configPath = path.join(os.homedir(), ".siluzan", "config.json");
122
- let fileConfig = {};
123
- if (fs.existsSync(configPath)) {
124
- try {
125
- fileConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
126
- }
127
- catch {
128
- // 解析失败则忽略,使用默认值
129
- }
130
- }
131
- // API Key 优先(X-Api-Key 鉴权)
82
+ const shared = readSharedConfig();
132
83
  const apiKey = process.env.SILUZAN_API_KEY ??
133
- (typeof fileConfig.apiKey === "string" && fileConfig.apiKey ? fileConfig.apiKey : undefined);
134
- // JWT Token(Bearer 鉴权)
84
+ (shared.apiKey ? shared.apiKey : undefined);
135
85
  const authToken = tokenArg ??
136
86
  process.env.SILUZAN_AUTH_TOKEN ??
137
- (typeof fileConfig.authToken === "string" ? fileConfig.authToken : "") ??
87
+ shared.authToken ??
138
88
  "";
139
- // 两种鉴权方式至少需要一种
140
89
  if (!apiKey && !authToken) {
141
90
  console.error("\n❌ 未找到认证凭据。请选择以下任意一种方式:\n\n" +
142
91
  " 方式一(推荐):API Key\n" +
@@ -144,138 +93,39 @@ export function loadConfig(tokenArg) {
144
93
  " siluzan-tso login --api-key <YOUR_API_KEY>\n\n");
145
94
  process.exit(1);
146
95
  }
147
- const apiBaseUrl = process.env.SILUZAN_API_BASE ??
148
- (typeof fileConfig.apiBaseUrl === "string" ? fileConfig.apiBaseUrl : DEFAULT_API_BASE);
96
+ const apiBaseUrl = process.env.SILUZAN_TSO_API_BASE ??
97
+ shared.tsoApiBaseUrl ??
98
+ DEFAULT_API_BASE;
149
99
  const apiErr = validateBaseUrl(apiBaseUrl);
150
100
  if (apiErr) {
151
- console.error(`\n❌ apiBaseUrl 不合法:${apiErr}`);
101
+ console.error(`\n❌ tsoApiBaseUrl 不合法:${apiErr}`);
152
102
  process.exit(1);
153
103
  }
154
104
  const googleApiUrl = process.env.SILUZAN_GOOGLE_API ??
155
- (typeof fileConfig.googleApiUrl === "string" ? fileConfig.googleApiUrl : DEFAULT_GOOGLE_API);
105
+ shared.googleApiUrl ??
106
+ DEFAULT_GOOGLE_API;
156
107
  const googleApiErr = validateBaseUrl(googleApiUrl);
157
108
  if (googleApiErr) {
158
109
  console.error(`\n❌ googleApiUrl 不合法:${googleApiErr}`);
159
110
  process.exit(1);
160
111
  }
161
- const mainApiUrl = deriveMainApiUrl(apiBaseUrl);
162
- const tiktokApiUrl = deriveTiktokApiUrl(apiBaseUrl);
163
- const facebookApiUrl = deriveFacebookApiUrl(apiBaseUrl);
164
- const chatGptApiUrl = deriveChatGptApiUrl(apiBaseUrl);
165
- const dataPermission = typeof fileConfig.dataPermission === "string" ? fileConfig.dataPermission : undefined;
166
- return { apiBaseUrl, authToken, apiKey, googleApiUrl, mainApiUrl, dataPermission, tiktokApiUrl, facebookApiUrl, chatGptApiUrl };
167
- }
168
- // ─────────────────── fetch 封装 ───────────────────
169
- /** Google/TikTok/Facebook 等媒体网关域名,当前仅支持 Bearer,不支持 X-Api-Key */
170
- function isMediaGatewayUrl(url) {
171
- try {
172
- return new URL(url).hostname.toLowerCase().endsWith(".mysiluzan.com");
173
- }
174
- catch {
175
- return false;
176
- }
177
- }
178
- /**
179
- * 使用 Node.js 原生 https/http 模块发送请求,避免 Node 18 内置 fetch 自动添加的
180
- * Sec-Fetch-Mode: cors 等浏览器化请求头(这些头会触发服务端 CORS 中间件剥除自定义头)。
181
- */
182
- function rawRequest(url, options) {
183
- return new Promise((resolve, reject) => {
184
- const parsed = new URL(url);
185
- const transport = parsed.protocol === "https:" ? https : http;
186
- const reqOpts = {
187
- hostname: parsed.hostname,
188
- port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
189
- path: parsed.pathname + parsed.search,
190
- method: options.method ?? "GET",
191
- headers: options.headers,
192
- };
193
- const req = transport.request(reqOpts, (res) => {
194
- let data = "";
195
- res.setEncoding("utf8");
196
- res.on("data", (chunk) => { data += chunk; });
197
- res.on("end", () => resolve({ status: res.statusCode ?? 0, text: data }));
198
- });
199
- req.on("error", reject);
200
- if (options.body)
201
- req.write(options.body);
202
- req.end();
203
- });
204
- }
205
- /**
206
- * 带认证头的 HTTP 请求封装。
207
- * 使用 rawRequest(基于 https 模块)而非 Node 18 内置 fetch,
208
- * 避免因 Sec-Fetch-Mode: cors 头导致服务端 CORS 中间件剥除 Datapermission 等自定义头。
209
- * @param verbose 为 true 时在错误信息中附加原始响应片段(默认 false)
210
- */
211
- export async function apiFetch(url, config, options = {}, verbose = false) {
212
- // 鉴权策略:有 API Key 时所有接口统一使用 x-api-key;否则降级为 Bearer Token
213
- const authHeaders = config.apiKey
214
- ? { "x-api-key": config.apiKey }
215
- : { Authorization: `Bearer ${config.authToken}` };
216
- // const authHeaders: Record<string, string> = { Authorization: `Bearer ${config.authToken}` };
217
- const reqHeaders = {
218
- "Content-Type": "application/json",
219
- // 语言头:服务端按此选择对应语言的 JSON 文件(如 zh-CNGoogleTimeZoneInfo.json)
220
- "Accept-Language": "zh-CN",
221
- ...authHeaders,
222
- // 数据权限标识:前端按当前路由菜单的 dp 字段传递,此处透传缓存值
223
- Datapermission: config.dataPermission ?? "",
224
- ...(options.headers ?? {}),
112
+ return {
113
+ apiBaseUrl,
114
+ authToken,
115
+ apiKey,
116
+ googleApiUrl,
117
+ mainApiUrl: deriveMainApiUrl(apiBaseUrl),
118
+ tiktokApiUrl: deriveTiktokApiUrl(apiBaseUrl),
119
+ facebookApiUrl: deriveFacebookApiUrl(apiBaseUrl),
120
+ chatGptApiUrl: deriveChatGptApiUrl(apiBaseUrl),
121
+ dataPermission: shared.dataPermission,
225
122
  };
226
- const body = typeof options.body === "string" ? options.body : undefined;
227
- const res = await rawRequest(url, {
228
- method: options.method ?? "GET",
229
- headers: reqHeaders,
230
- body,
231
- });
232
- const text = res.text;
233
- if (res.status < 200 || res.status >= 300) {
234
- const detail = verbose ? `:${redactSensitive(text).slice(0, 300)}` : "";
235
- throw new Error(`HTTP ${res.status}${detail}`);
236
- }
237
- // 204 No Content 或空 body:返回 null(调用方忽略返回值的操作如 stop/start/delete)
238
- if (!text.trim())
239
- return null;
240
- try {
241
- return JSON.parse(text);
242
- }
243
- catch {
244
- // HTTP 状态正常但 body 不是 JSON(部分接口成功时返回纯文本):
245
- // 视为成功并透传原始文本,避免误报失败。
246
- if (verbose) {
247
- console.error(` ⚠️ 响应非 JSON,原始内容:${redactSensitive(text).slice(0, 200)}`);
248
- }
249
- return text;
250
- }
251
- }
252
- // ─────────────────── 工具 ───────────────────
253
- /**
254
- * 对响应文本中常见凭据字段做脱敏,避免在 --verbose 下直接泄露。
255
- */
256
- function redactSensitive(input) {
257
- let output = input;
258
- output = output.replace(/(Bearer\s+)[^\s",]+/gi, "$1***");
259
- output = output.replace(/("?(?:apiKey|authToken|accessToken|refreshToken|token|authorization)"?\s*[:=]\s*"?)([^"\s,}]+)/gi, "$1***");
260
- return output;
261
- }
262
- /** 休眠指定毫秒(用于轮询等待) */
263
- export function sleep(ms) {
264
- return new Promise((resolve) => setTimeout(resolve, ms));
265
123
  }
124
+ // ─────────────────── fetch 封装 ───────────────────
266
125
  /**
267
- * 带超时的轮询函数。
268
- * @param fn 每轮执行的异步函数,返回 null 表示继续等待,返回值表示完成
269
- * @param intervalMs 轮询间隔(毫秒)
270
- * @param timeoutMs 最大等待时间(毫秒)
126
+ * 带认证头的 HTTP 请求封装(使用公共实现)。
127
+ * AdsConfig 满足 HttpConfig 接口(含 authToken / apiKey / dataPermission 字段)。
271
128
  */
272
- export async function pollUntil(fn, intervalMs, timeoutMs) {
273
- const deadline = Date.now() + timeoutMs;
274
- while (Date.now() < deadline) {
275
- const result = await fn();
276
- if (result !== null)
277
- return result;
278
- await sleep(intervalMs);
279
- }
280
- return null;
129
+ export function apiFetch(url, config, options = {}, verbose = false) {
130
+ return _apiFetch(url, config, options, verbose);
281
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "siluzan-tso-cli",
3
- "version": "1.0.0-beta.16",
3
+ "version": "1.0.0-beta.18",
4
4
  "description": "Siluzan 广告账户管理 CLI — 查询账户、余额、消耗数据,管理绑定关系与充值。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,7 +36,8 @@
36
36
  "dependencies": {
37
37
  "commander": "^12.1.0",
38
38
  "open": "^10.1.0",
39
- "qrcode": "^1.5.4"
39
+ "qrcode": "^1.5.4",
40
+ "siluzan-cli-common": "*"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "^22.10.0",