siluzan-tso-cli 1.1.17 → 1.1.18-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 +2 -1
- package/dist/index.js +595 -62
- package/dist/skill/SKILL.md +10 -4
- package/dist/skill/_meta.json +2 -2
- package/dist/skill/references/account-analytics.md +7 -5
- package/dist/skill/references/accounts.md +9 -2
- package/dist/skill/references/finance.md +5 -5
- package/dist/skill/references/reporting.md +2 -2
- package/dist/skill/references/setup.md +51 -9
- package/dist/skill/references/tso-home.md +3 -3
- package/dist/skill/scripts/install.ps1 +2 -2
- package/dist/skill/scripts/install.sh +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ HTML 报告模板引用以下 CDN:`cdn.tailwindcss.com`、`cdnjs.cloudflare.co
|
|
|
43
43
|
在**用户的目标项目根目录**执行(根据用户使用的助手选择 `--ai`):
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
npm install -g siluzan-tso-cli
|
|
46
|
+
npm install -g siluzan-tso-cli@beta
|
|
47
47
|
siluzan-tso init --ai cursor # 写入 Cursor(默认)
|
|
48
48
|
siluzan-tso init --ai cursor,claude # 同时写入多个平台
|
|
49
49
|
siluzan-tso init --ai all # 写入所有支持的平台
|
|
@@ -51,6 +51,7 @@ siluzan-tso init -d /path/to/skills # 写入自定义目录
|
|
|
51
51
|
siluzan-tso init --force # 强制覆盖已存在文件
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
> **注意**:当前为测试版(1.1.18-beta.2),供内部测试使用。正式发布后安装命令将改为 `npm install -g siluzan-tso-cli`。
|
|
54
55
|
|
|
55
56
|
| 助手 | 建议 `--ai` |
|
|
56
57
|
| ----------------------- | ------------------------------------ |
|
package/dist/index.js
CHANGED
|
@@ -2054,12 +2054,12 @@ function validateBaseUrl(raw) {
|
|
|
2054
2054
|
if (url.protocol !== "https:") {
|
|
2055
2055
|
return `\u5FC5\u987B\u4F7F\u7528 HTTPS\uFF0C\u5F53\u524D\u534F\u8BAE\uFF1A${url.protocol}`;
|
|
2056
2056
|
}
|
|
2057
|
-
const
|
|
2057
|
+
const hostname2 = url.hostname.toLowerCase();
|
|
2058
2058
|
const ok = ALLOWED_HOSTNAME_SUFFIXES.some(
|
|
2059
|
-
(suffix) =>
|
|
2059
|
+
(suffix) => hostname2 === suffix || hostname2.endsWith(`.${suffix}`)
|
|
2060
2060
|
);
|
|
2061
2061
|
if (!ok) {
|
|
2062
|
-
return `\u4E3B\u673A\u540D "${
|
|
2062
|
+
return `\u4E3B\u673A\u540D "${hostname2}" \u4E0D\u5728\u5141\u8BB8\u5217\u8868\uFF08${ALLOWED_HOSTNAME_SUFFIXES.join("\u3001")}\uFF09\u5185\u3002
|
|
2063
2063
|
\u5982\u9700\u8FDE\u63A5\u81EA\u5B9A\u4E49\u90E8\u7F72\u7AEF\u70B9\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u6DFB\u52A0\u767D\u540D\u5355\u3002`;
|
|
2064
2064
|
}
|
|
2065
2065
|
return null;
|
|
@@ -2735,7 +2735,207 @@ async function readPreSnapshotPayload(namespace, preSnapshotRef) {
|
|
|
2735
2735
|
return null;
|
|
2736
2736
|
}
|
|
2737
2737
|
}
|
|
2738
|
-
|
|
2738
|
+
function deriveSsoBaseUrl(anyApiBase) {
|
|
2739
|
+
try {
|
|
2740
|
+
const u = new URL(anyApiBase);
|
|
2741
|
+
u.hostname = u.hostname.replace(/^(tso-api|cso|api)/, "sso");
|
|
2742
|
+
return u.origin;
|
|
2743
|
+
} catch {
|
|
2744
|
+
return "https://sso.siluzan.com";
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
function deriveCsoApiBaseUrl(anyApiBase) {
|
|
2748
|
+
try {
|
|
2749
|
+
const u = new URL(anyApiBase);
|
|
2750
|
+
u.hostname = u.hostname.replace(/^(tso-api|api)/, "cso");
|
|
2751
|
+
return u.origin;
|
|
2752
|
+
} catch {
|
|
2753
|
+
return "https://cso.siluzan.com";
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function normalizeChinaPhone(input) {
|
|
2757
|
+
if (!input) return input;
|
|
2758
|
+
const cleaned = input.replace(/[\s\-()\u00A0]/g, "");
|
|
2759
|
+
if (/^\+861\d{10}$/.test(cleaned)) {
|
|
2760
|
+
return cleaned;
|
|
2761
|
+
}
|
|
2762
|
+
if (/^861\d{10}$/.test(cleaned)) {
|
|
2763
|
+
return `+${cleaned}`;
|
|
2764
|
+
}
|
|
2765
|
+
if (/^00861\d{10}$/.test(cleaned)) {
|
|
2766
|
+
return `+${cleaned.slice(2)}`;
|
|
2767
|
+
}
|
|
2768
|
+
if (/^1\d{10}$/.test(cleaned)) {
|
|
2769
|
+
return `+86${cleaned}`;
|
|
2770
|
+
}
|
|
2771
|
+
return cleaned;
|
|
2772
|
+
}
|
|
2773
|
+
function isValidChinaPhone(input) {
|
|
2774
|
+
return /^\+861\d{10}$/.test(normalizeChinaPhone(input));
|
|
2775
|
+
}
|
|
2776
|
+
async function sendPhoneLoginCode(opts) {
|
|
2777
|
+
const phone = normalizeChinaPhone(opts.phone);
|
|
2778
|
+
const url = `${opts.ssoBaseUrl}/Account/SendVaildCode?Phone=${encodeURIComponent(
|
|
2779
|
+
phone
|
|
2780
|
+
)}&RandStr=&Iicket=`;
|
|
2781
|
+
if (opts.verbose) {
|
|
2782
|
+
process.stderr.write(`[phone-login] GET ${url}
|
|
2783
|
+
`);
|
|
2784
|
+
}
|
|
2785
|
+
const res = await rawRequest(url, {
|
|
2786
|
+
method: "GET",
|
|
2787
|
+
headers: {
|
|
2788
|
+
Accept: "application/json",
|
|
2789
|
+
"Accept-Language": "zh-CN"
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2792
|
+
if (res.status < 200 || res.status >= 300) {
|
|
2793
|
+
return { ok: false, message: `HTTP ${res.status}` };
|
|
2794
|
+
}
|
|
2795
|
+
let body;
|
|
2796
|
+
try {
|
|
2797
|
+
body = JSON.parse(res.text);
|
|
2798
|
+
} catch {
|
|
2799
|
+
return { ok: false, message: `\u54CD\u5E94\u975E JSON\uFF1A${res.text.slice(0, 120)}` };
|
|
2800
|
+
}
|
|
2801
|
+
const state = (body.State ?? body.state ?? "").toLowerCase();
|
|
2802
|
+
const message = body.Message ?? body.message ?? "";
|
|
2803
|
+
return { ok: state === "ok", message };
|
|
2804
|
+
}
|
|
2805
|
+
async function loginByPhoneCode(opts) {
|
|
2806
|
+
const phone = normalizeChinaPhone(opts.phone);
|
|
2807
|
+
const url = `${opts.ssoBaseUrl}/Account/LoginByMiniCode?phone=${encodeURIComponent(
|
|
2808
|
+
phone
|
|
2809
|
+
)}&code=${encodeURIComponent(opts.code)}&key=`;
|
|
2810
|
+
if (opts.verbose) {
|
|
2811
|
+
process.stderr.write(`[phone-login] GET ${url}
|
|
2812
|
+
`);
|
|
2813
|
+
}
|
|
2814
|
+
const res = await rawRequest(url, {
|
|
2815
|
+
method: "GET",
|
|
2816
|
+
headers: {
|
|
2817
|
+
Accept: "application/json",
|
|
2818
|
+
"Accept-Language": "zh-CN"
|
|
2819
|
+
}
|
|
2820
|
+
});
|
|
2821
|
+
if (opts.verbose) {
|
|
2822
|
+
process.stderr.write(
|
|
2823
|
+
`[phone-login] LoginByMiniCode HTTP ${res.status} body=${res.text.slice(0, 500)}
|
|
2824
|
+
`
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
if (res.status < 200 || res.status >= 300) {
|
|
2828
|
+
throw new Error(`\u767B\u5F55\u5931\u8D25\uFF1AHTTP ${res.status}`);
|
|
2829
|
+
}
|
|
2830
|
+
let body;
|
|
2831
|
+
try {
|
|
2832
|
+
body = JSON.parse(res.text);
|
|
2833
|
+
} catch {
|
|
2834
|
+
throw new Error(`\u767B\u5F55\u54CD\u5E94\u975E JSON\uFF1A${res.text.slice(0, 120)}`);
|
|
2835
|
+
}
|
|
2836
|
+
const errMsg = body.msg ?? body.Msg ?? "";
|
|
2837
|
+
if (!body.token || typeof body.token === "string") {
|
|
2838
|
+
throw new Error(errMsg || "\u767B\u5F55\u5931\u8D25\uFF08\u540E\u7AEF\u672A\u8FD4\u56DE\u539F\u56E0\uFF09");
|
|
2839
|
+
}
|
|
2840
|
+
const token = body.token;
|
|
2841
|
+
const isError = token.isError ?? token.IsError ?? false;
|
|
2842
|
+
if (isError) {
|
|
2843
|
+
const err = token.errorDescription ?? token.ErrorDescription ?? token.error ?? token.Error ?? "\u672A\u77E5\u9519\u8BEF";
|
|
2844
|
+
throw new Error(`OAuth \u5931\u8D25\uFF1A${err}`);
|
|
2845
|
+
}
|
|
2846
|
+
let accessToken = token.accessToken ?? token.access_token ?? "";
|
|
2847
|
+
let tokenType = token.tokenType ?? token.token_type ?? "Bearer";
|
|
2848
|
+
let expiresIn = token.expiresIn ?? token.expires_in;
|
|
2849
|
+
if (!accessToken) {
|
|
2850
|
+
const rawJson = token.raw ?? token.Raw;
|
|
2851
|
+
if (rawJson && typeof rawJson === "string") {
|
|
2852
|
+
try {
|
|
2853
|
+
const parsed = JSON.parse(rawJson);
|
|
2854
|
+
accessToken = parsed.access_token ?? "";
|
|
2855
|
+
tokenType = parsed.token_type ?? tokenType;
|
|
2856
|
+
expiresIn = parsed.expires_in ?? expiresIn;
|
|
2857
|
+
} catch {
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
if (!accessToken) {
|
|
2862
|
+
throw new Error(
|
|
2863
|
+
`\u767B\u5F55\u54CD\u5E94\u7F3A\u5C11 access_token\uFF08\u54CD\u5E94\u5B57\u6BB5\uFF1A${Object.keys(token).join(", ") || "\u65E0"}\uFF09`
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
return { accessToken, tokenType, expiresIn };
|
|
2867
|
+
}
|
|
2868
|
+
async function createApiKeyByBearer(opts) {
|
|
2869
|
+
if (opts.allowedServices.length === 0) {
|
|
2870
|
+
throw new Error("createApiKey \u81F3\u5C11\u9700\u8981\u4F20\u5165\u4E00\u4E2A allowedServices");
|
|
2871
|
+
}
|
|
2872
|
+
if (opts.validDays === void 0 && opts.expiresAt === void 0) {
|
|
2873
|
+
throw new Error("createApiKey \u5FC5\u987B\u6307\u5B9A validDays \u6216 expiresAt");
|
|
2874
|
+
}
|
|
2875
|
+
const body = JSON.stringify({
|
|
2876
|
+
name: opts.name,
|
|
2877
|
+
validDays: opts.validDays,
|
|
2878
|
+
expiresAt: opts.expiresAt,
|
|
2879
|
+
allowedServices: opts.allowedServices.map((s) => API_KEY_SERVICE_VALUES[s])
|
|
2880
|
+
});
|
|
2881
|
+
const url = `${opts.csoBaseUrl}/cso/v1/apikey`;
|
|
2882
|
+
if (opts.verbose) {
|
|
2883
|
+
process.stderr.write(`[phone-login] POST ${url} body=${body}
|
|
2884
|
+
`);
|
|
2885
|
+
}
|
|
2886
|
+
const res = await rawRequest(url, {
|
|
2887
|
+
method: "POST",
|
|
2888
|
+
headers: {
|
|
2889
|
+
"Content-Type": "application/json",
|
|
2890
|
+
Accept: "application/json",
|
|
2891
|
+
"Accept-Language": "zh-CN",
|
|
2892
|
+
Authorization: `Bearer ${opts.bearerToken}`,
|
|
2893
|
+
"Content-Length": String(Buffer.byteLength(body, "utf8"))
|
|
2894
|
+
},
|
|
2895
|
+
body
|
|
2896
|
+
});
|
|
2897
|
+
if (res.status < 200 || res.status >= 300) {
|
|
2898
|
+
throw new Error(`\u521B\u5EFA API Key \u5931\u8D25\uFF1AHTTP ${res.status}\uFF08${res.text.slice(0, 200)}\uFF09`);
|
|
2899
|
+
}
|
|
2900
|
+
let json;
|
|
2901
|
+
try {
|
|
2902
|
+
json = JSON.parse(res.text);
|
|
2903
|
+
} catch {
|
|
2904
|
+
throw new Error(`\u521B\u5EFA API Key \u54CD\u5E94\u975E JSON\uFF1A${res.text.slice(0, 120)}`);
|
|
2905
|
+
}
|
|
2906
|
+
const code = json.code ?? json.Code;
|
|
2907
|
+
if (code !== 1) {
|
|
2908
|
+
throw new Error(`\u521B\u5EFA API Key \u5931\u8D25\uFF1A${json.message ?? json.Message ?? "\u672A\u77E5\u9519\u8BEF"}`);
|
|
2909
|
+
}
|
|
2910
|
+
const data = json.data ?? json.Data;
|
|
2911
|
+
if (!data?.rawKey) {
|
|
2912
|
+
throw new Error("\u521B\u5EFA API Key \u54CD\u5E94\u7F3A\u5C11 rawKey \u5B57\u6BB5");
|
|
2913
|
+
}
|
|
2914
|
+
return data;
|
|
2915
|
+
}
|
|
2916
|
+
async function issueApiKeyWithPhoneCode(opts) {
|
|
2917
|
+
const code = opts.code?.trim() ?? "";
|
|
2918
|
+
if (!code) {
|
|
2919
|
+
throw new Error("\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A");
|
|
2920
|
+
}
|
|
2921
|
+
const tokenInfo = await loginByPhoneCode({
|
|
2922
|
+
ssoBaseUrl: opts.ssoBaseUrl,
|
|
2923
|
+
phone: opts.phone,
|
|
2924
|
+
code,
|
|
2925
|
+
verbose: opts.verbose
|
|
2926
|
+
});
|
|
2927
|
+
const apiKey = await createApiKeyByBearer({
|
|
2928
|
+
csoBaseUrl: opts.csoBaseUrl,
|
|
2929
|
+
bearerToken: tokenInfo.accessToken,
|
|
2930
|
+
name: opts.apiKeyName,
|
|
2931
|
+
validDays: opts.validDays ?? (opts.expiresAt ? void 0 : 90),
|
|
2932
|
+
expiresAt: opts.expiresAt,
|
|
2933
|
+
allowedServices: opts.allowedServices,
|
|
2934
|
+
verbose: opts.verbose
|
|
2935
|
+
});
|
|
2936
|
+
return apiKey;
|
|
2937
|
+
}
|
|
2938
|
+
var import_semver, SILUZAN_DIR, CONFIG_FILE, ALLOWED_HOSTNAME_SUFFIXES, DEFAULT_TIMEOUT_MS, MAX_RESPONSE_BYTES, httpsAgent, httpAgent, PERF_PREFIX, MAX_INVOCATION_CHARS, currentInvocation, MAX_COMMIT_CHARS, cliCommitOverride, WRITE_AUDIT_SCHEMA_VERSION, MUTATING, BEIJING_TZ, AUDIT_LOG_FILENAME_RE, DEFAULT_MAX_FILE_MB, MIN_SEGMENT_BYTES, MAX_SEGMENT_BYTES, DEFAULT_RETENTION_DAYS, MIN_RETENTION_DAYS, MAX_RETENTION_DAYS, PRUNE_THROTTLE_MS, lastPruneAtMs, MAX_BODY_CHARS, MAX_ERROR_CHARS, DEFAULT_FIND_AUDIT_MAX_DAYS, MAX_SNAPSHOT_FILE_BYTES, API_KEY_SERVICE_VALUES;
|
|
2739
2939
|
var init_dist = __esm({
|
|
2740
2940
|
"../common/dist/index.js"() {
|
|
2741
2941
|
"use strict";
|
|
@@ -2783,6 +2983,11 @@ var init_dist = __esm({
|
|
|
2783
2983
|
MAX_ERROR_CHARS = 800;
|
|
2784
2984
|
DEFAULT_FIND_AUDIT_MAX_DAYS = 400;
|
|
2785
2985
|
MAX_SNAPSHOT_FILE_BYTES = 2 * 1024 * 1024;
|
|
2986
|
+
API_KEY_SERVICE_VALUES = {
|
|
2987
|
+
CSO: 0,
|
|
2988
|
+
TSO: 1,
|
|
2989
|
+
CUT: 2
|
|
2990
|
+
};
|
|
2786
2991
|
}
|
|
2787
2992
|
});
|
|
2788
2993
|
|
|
@@ -2791,7 +2996,7 @@ var DEFAULT_API_BASE;
|
|
|
2791
2996
|
var init_defaults = __esm({
|
|
2792
2997
|
"src/config/defaults.ts"() {
|
|
2793
2998
|
"use strict";
|
|
2794
|
-
DEFAULT_API_BASE = "https://tso-api.siluzan.com";
|
|
2999
|
+
DEFAULT_API_BASE = "https://tso-api-ci.siluzan.com";
|
|
2795
3000
|
}
|
|
2796
3001
|
});
|
|
2797
3002
|
|
|
@@ -3842,18 +4047,78 @@ async function fetchBalanceMap(media, accountIds, config, startDate, endDate, ve
|
|
|
3842
4047
|
}
|
|
3843
4048
|
return result;
|
|
3844
4049
|
}
|
|
4050
|
+
function parseGoogleAccountSpendOverviewRows(raw) {
|
|
4051
|
+
const fromDbItem = (it) => {
|
|
4052
|
+
if (it == null || it.mediaAccountId == null) return null;
|
|
4053
|
+
return {
|
|
4054
|
+
mode: "database",
|
|
4055
|
+
mediaAccountId: String(it.mediaAccountId),
|
|
4056
|
+
mediaCustomerName: it.mediaCustomerName ?? void 0,
|
|
4057
|
+
spend: typeof it.spend === "number" ? it.spend : void 0,
|
|
4058
|
+
impressions: typeof it.impressions === "number" ? it.impressions : void 0,
|
|
4059
|
+
clicks: typeof it.clicks === "number" ? it.clicks : void 0,
|
|
4060
|
+
conversions: typeof it.conversions === "number" ? it.conversions : void 0,
|
|
4061
|
+
costPerClick: typeof it.costPerClick === "number" ? it.costPerClick : void 0,
|
|
4062
|
+
currencyCode: it.currencyCode ?? void 0,
|
|
4063
|
+
status: it.status ?? void 0,
|
|
4064
|
+
remainingAccountBudget: typeof it.remainingAccountBudget === "number" ? it.remainingAccountBudget : void 0
|
|
4065
|
+
};
|
|
4066
|
+
};
|
|
4067
|
+
if (Array.isArray(raw)) {
|
|
4068
|
+
return raw.map(fromDbItem).filter((row) => row !== null);
|
|
4069
|
+
}
|
|
4070
|
+
if (!raw || typeof raw !== "object") return [];
|
|
4071
|
+
const r = raw;
|
|
4072
|
+
if (r.mode === "googleCombined") {
|
|
4073
|
+
const accounts = r.accounts ?? {};
|
|
4074
|
+
const rows = [];
|
|
4075
|
+
for (const [id, item] of Object.entries(accounts)) {
|
|
4076
|
+
const data = item?.data;
|
|
4077
|
+
if (!data) continue;
|
|
4078
|
+
rows.push({
|
|
4079
|
+
mode: "googleCombined",
|
|
4080
|
+
mediaAccountId: String(id),
|
|
4081
|
+
spend: typeof data.spend === "number" ? data.spend : void 0,
|
|
4082
|
+
impressions: typeof data.impressions === "number" ? data.impressions : void 0,
|
|
4083
|
+
clicks: typeof data.clicks === "number" ? data.clicks : void 0,
|
|
4084
|
+
conversions: typeof data.conversions === "number" ? data.conversions : void 0,
|
|
4085
|
+
// 实时模式下 averageCpc 即点击均价(CPC)
|
|
4086
|
+
costPerClick: typeof data.averageCpc === "number" ? data.averageCpc : void 0
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
return rows;
|
|
4090
|
+
}
|
|
4091
|
+
const items = Array.isArray(r.items) ? r.items : [];
|
|
4092
|
+
return items.map(fromDbItem).filter((row) => row !== null);
|
|
4093
|
+
}
|
|
3845
4094
|
async function fetchOverviewMap(media, accountIds, config, startDate, endDate, verbose) {
|
|
3846
4095
|
const result = /* @__PURE__ */ new Map();
|
|
3847
4096
|
if (accountIds.length === 0 || media === "MetaAd") return result;
|
|
3848
4097
|
const range = defaultDateRange();
|
|
3849
|
-
const
|
|
3850
|
-
|
|
3851
|
-
startDate: startDate ?? range.startDate,
|
|
3852
|
-
endDate: endDate ?? range.endDate,
|
|
3853
|
-
mediaCustomerIds: accountIds.join(",")
|
|
3854
|
-
});
|
|
3855
|
-
const url = `${config.apiBaseUrl}/report/media-account/${media}/accountsoverview?${params}`;
|
|
4098
|
+
const start = startDate ?? range.startDate;
|
|
4099
|
+
const end = endDate ?? range.endDate;
|
|
3856
4100
|
try {
|
|
4101
|
+
if (media === "Google") {
|
|
4102
|
+
const params2 = new URLSearchParams({
|
|
4103
|
+
startDate: start,
|
|
4104
|
+
endDate: end,
|
|
4105
|
+
mediaCustomerIds: accountIds.join("|")
|
|
4106
|
+
});
|
|
4107
|
+
const url2 = `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params2}`;
|
|
4108
|
+
const raw2 = await apiFetch2(url2, config, {}, verbose);
|
|
4109
|
+
const rows = parseGoogleAccountSpendOverviewRows(raw2);
|
|
4110
|
+
for (const row of rows) {
|
|
4111
|
+
result.set(row.mediaAccountId, row);
|
|
4112
|
+
}
|
|
4113
|
+
return result;
|
|
4114
|
+
}
|
|
4115
|
+
const params = new URLSearchParams({
|
|
4116
|
+
period: "true",
|
|
4117
|
+
startDate: start,
|
|
4118
|
+
endDate: end,
|
|
4119
|
+
mediaCustomerIds: accountIds.join(",")
|
|
4120
|
+
});
|
|
4121
|
+
const url = `${config.apiBaseUrl}/report/media-account/${media}/accountsoverview?${params}`;
|
|
3857
4122
|
const raw = await apiFetch2(url, config, {}, verbose);
|
|
3858
4123
|
const items = Array.isArray(raw) ? raw : [];
|
|
3859
4124
|
for (const item of items) {
|
|
@@ -3861,7 +4126,13 @@ async function fetchOverviewMap(media, accountIds, config, startDate, endDate, v
|
|
|
3861
4126
|
result.set(String(item.mediaAccountId), item);
|
|
3862
4127
|
}
|
|
3863
4128
|
}
|
|
3864
|
-
} catch {
|
|
4129
|
+
} catch (err) {
|
|
4130
|
+
if (verbose) {
|
|
4131
|
+
process.stderr.write(
|
|
4132
|
+
`[fetchOverviewMap] \u5F02\u5E38\u88AB\u541E\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
4133
|
+
`
|
|
4134
|
+
);
|
|
4135
|
+
}
|
|
3865
4136
|
}
|
|
3866
4137
|
return result;
|
|
3867
4138
|
}
|
|
@@ -3941,24 +4212,36 @@ async function runAccountsDigest(opts) {
|
|
|
3941
4212
|
const pageSize = Math.max(1, Math.min(500, opts.pageSize ?? 200));
|
|
3942
4213
|
const maxPages = Math.max(1, Math.min(200, opts.maxPages ?? 20));
|
|
3943
4214
|
let accountsList;
|
|
3944
|
-
|
|
3945
|
-
const res = await fetchAccountsByMedia(media, config, {
|
|
3946
|
-
pageSize,
|
|
3947
|
-
maxPages,
|
|
3948
|
-
verbose: opts.verbose
|
|
3949
|
-
});
|
|
3950
|
-
accountsList = res.items;
|
|
3951
|
-
} catch (err) {
|
|
3952
|
-
console.error(`
|
|
3953
|
-
\u274C \u62C9\u53D6\u8D26\u6237\u6E05\u5355\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
3954
|
-
`);
|
|
3955
|
-
process.exit(1);
|
|
3956
|
-
}
|
|
4215
|
+
let scannedFromList = false;
|
|
3957
4216
|
if (filterSet.size > 0) {
|
|
3958
|
-
accountsList =
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
4217
|
+
accountsList = filterIds.map((id) => ({
|
|
4218
|
+
ma: {
|
|
4219
|
+
entityId: "",
|
|
4220
|
+
mediaCustomerId: id,
|
|
4221
|
+
mediaCustomerName: null,
|
|
4222
|
+
mediaAccountType: media,
|
|
4223
|
+
invalidOAuthToken: false
|
|
4224
|
+
},
|
|
4225
|
+
accepted: true,
|
|
4226
|
+
mag: null
|
|
4227
|
+
}));
|
|
4228
|
+
} else {
|
|
4229
|
+
try {
|
|
4230
|
+
const res = await fetchAccountsByMedia(media, config, {
|
|
4231
|
+
pageSize,
|
|
4232
|
+
maxPages,
|
|
4233
|
+
verbose: opts.verbose
|
|
4234
|
+
});
|
|
4235
|
+
accountsList = res.items;
|
|
4236
|
+
scannedFromList = true;
|
|
4237
|
+
} catch (err) {
|
|
4238
|
+
console.error(
|
|
4239
|
+
`
|
|
4240
|
+
\u274C \u62C9\u53D6\u8D26\u6237\u6E05\u5355\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}
|
|
4241
|
+
`
|
|
4242
|
+
);
|
|
4243
|
+
process.exit(1);
|
|
4244
|
+
}
|
|
3962
4245
|
}
|
|
3963
4246
|
const validIds = accountsList.filter((it) => it.ma?.mediaCustomerId && !it.ma?.invalidOAuthToken).map((it) => String(it.ma.mediaCustomerId));
|
|
3964
4247
|
const CHUNK = 100;
|
|
@@ -4003,9 +4286,9 @@ async function runAccountsDigest(opts) {
|
|
|
4003
4286
|
const cpa = conversions && conversions > 0 && spend != null ? spend / conversions : null;
|
|
4004
4287
|
rows.push({
|
|
4005
4288
|
mediaCustomerId: id,
|
|
4006
|
-
name: ma.mediaCustomerName ?? null,
|
|
4289
|
+
name: ma.mediaCustomerName ?? bal?.name ?? ov?.mediaCustomerName ?? null,
|
|
4007
4290
|
advertiserName: item.mag?.advertiserName ?? null,
|
|
4008
|
-
currencyCode: bal?.currencyCode ?? ma.currencyCode ?? null,
|
|
4291
|
+
currencyCode: bal?.currencyCode ?? ma.currencyCode ?? ov?.currencyCode ?? null,
|
|
4009
4292
|
balance: round2(bal?.remainingAccountBudget ?? null),
|
|
4010
4293
|
spend: round2(spend),
|
|
4011
4294
|
impressions: impressions != null ? Math.round(impressions) : null,
|
|
@@ -4037,6 +4320,8 @@ async function runAccountsDigest(opts) {
|
|
|
4037
4320
|
note: opts.startDate && opts.endDate ? "\u7528\u6237/AI \u660E\u786E\u6307\u5B9A\u533A\u95F4" : "\u672A\u6307\u5B9A\u533A\u95F4\uFF0CCLI \u9ED8\u8BA4\u8FD1 7 \u5929\uFF1BSKILL \u8981\u6C42\u5148\u5411\u7528\u6237\u786E\u8BA4"
|
|
4038
4321
|
},
|
|
4039
4322
|
scanned: accountsList.length,
|
|
4323
|
+
/** 取数策略:list = 全量翻清单后过滤,subset = 跳过翻页直接对 -a 指定的 ID 拉数据 */
|
|
4324
|
+
source: scannedFromList ? "list" : "subset",
|
|
4040
4325
|
returned: rows.length,
|
|
4041
4326
|
totals: {
|
|
4042
4327
|
spend: +totals.spend.toFixed(2),
|
|
@@ -8004,21 +8289,39 @@ async function runBalanceScan(opts) {
|
|
|
8004
8289
|
const targetDays = opts.targetDays ?? 30;
|
|
8005
8290
|
const pageSize = Math.max(1, Math.min(500, opts.pageSize ?? 200));
|
|
8006
8291
|
const maxPages = Math.max(1, Math.min(200, opts.maxPages ?? 20));
|
|
8007
|
-
const
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8292
|
+
const filterIds = (opts.accounts ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
8293
|
+
const isSubset = filterIds.length > 0;
|
|
8294
|
+
let allItems;
|
|
8295
|
+
let total;
|
|
8296
|
+
if (isSubset) {
|
|
8297
|
+
allItems = filterIds.map((id) => ({
|
|
8298
|
+
ma: {
|
|
8299
|
+
entityId: "",
|
|
8300
|
+
mediaCustomerId: id,
|
|
8301
|
+
mediaCustomerName: null,
|
|
8302
|
+
mediaAccountType: media,
|
|
8303
|
+
invalidOAuthToken: false
|
|
8304
|
+
},
|
|
8305
|
+
accepted: true,
|
|
8306
|
+
mag: null
|
|
8307
|
+
}));
|
|
8308
|
+
process.stderr.write(
|
|
8309
|
+
`\u23F3 [balance-scan] \u5B50\u96C6\u6A21\u5F0F\uFF1A\u8DF3\u8FC7\u6E05\u5355\u7FFB\u9875\uFF0C\u76F4\u63A5\u5BF9 -a \u6307\u5B9A\u7684 ${filterIds.length} \u4E2A\u8D26\u6237\u62C9\u4F59\u989D\u4E0E\u6D88\u8017\u3002
|
|
8016
8310
|
`
|
|
8017
|
-
|
|
8311
|
+
);
|
|
8312
|
+
} else {
|
|
8313
|
+
const res = await fetchAllAccountPages(media, pageSize, maxPages, config, opts.verbose);
|
|
8314
|
+
allItems = res.items;
|
|
8315
|
+
total = res.total;
|
|
8316
|
+
process.stderr.write(
|
|
8317
|
+
`\r\u23F3 \u8D26\u6237\u6E05\u5355\u5DF2\u62C9\u53D6\uFF1A\u5171 ${allItems.length} \u6761${total !== void 0 ? `\uFF08\u63A5\u53E3 total\u2248${total}\uFF09` : ""}
|
|
8318
|
+
`
|
|
8319
|
+
);
|
|
8320
|
+
}
|
|
8018
8321
|
const validIds = [];
|
|
8019
8322
|
for (const item of allItems) {
|
|
8020
8323
|
const id = item.ma?.mediaCustomerId;
|
|
8021
|
-
if (id && !item.ma?.invalidOAuthToken) validIds.push(String(id));
|
|
8324
|
+
if (id && (isSubset || !item.ma?.invalidOAuthToken)) validIds.push(String(id));
|
|
8022
8325
|
}
|
|
8023
8326
|
let balanceMap = /* @__PURE__ */ new Map();
|
|
8024
8327
|
let overviewMap = /* @__PURE__ */ new Map();
|
|
@@ -8096,19 +8399,19 @@ async function runBalanceScan(opts) {
|
|
|
8096
8399
|
}
|
|
8097
8400
|
const lowDays = remainingDays !== null && remainingDays <= thresholdDays;
|
|
8098
8401
|
const lowBalance = minBalance !== null && balance !== null && balance <= minBalance;
|
|
8099
|
-
if (!lowDays && !lowBalance) continue;
|
|
8100
|
-
const hitReason = lowDays && lowBalance ? "both" : lowDays ? "low-days" : "low-balance";
|
|
8402
|
+
if (!lowDays && !lowBalance && !isSubset) continue;
|
|
8403
|
+
const hitReason = lowDays && lowBalance ? "both" : lowDays ? "low-days" : lowBalance ? "low-balance" : "none";
|
|
8101
8404
|
evaluated.push({
|
|
8102
8405
|
mediaCustomerId: id,
|
|
8103
|
-
name: ma.mediaCustomerName ?? null,
|
|
8406
|
+
name: ma.mediaCustomerName ?? bal?.name ?? ov?.mediaCustomerName ?? null,
|
|
8104
8407
|
advertiserName: item.mag?.advertiserName ?? null,
|
|
8105
|
-
currencyCode: bal?.currencyCode ?? ma.currencyCode ?? null,
|
|
8408
|
+
currencyCode: bal?.currencyCode ?? ma.currencyCode ?? ov?.currencyCode ?? null,
|
|
8106
8409
|
balance,
|
|
8107
8410
|
dailySpend: dailySpend > 0 ? +dailySpend.toFixed(2) : null,
|
|
8108
8411
|
remainingDays,
|
|
8109
8412
|
recommendedTopup,
|
|
8110
8413
|
invalidOAuthToken: !!ma.invalidOAuthToken,
|
|
8111
|
-
status: bal?.status ?? bal?.accountStatus ?? null,
|
|
8414
|
+
status: bal?.status ?? bal?.accountStatus ?? ov?.status ?? null,
|
|
8112
8415
|
hitReason
|
|
8113
8416
|
});
|
|
8114
8417
|
}
|
|
@@ -8119,6 +8422,8 @@ async function runBalanceScan(opts) {
|
|
|
8119
8422
|
});
|
|
8120
8423
|
const meta = {
|
|
8121
8424
|
media,
|
|
8425
|
+
/** 取数策略:list = 全量翻清单后阈值过滤,subset = -a 指定 ID 跳过翻页全部展示 */
|
|
8426
|
+
source: isSubset ? "subset" : "list",
|
|
8122
8427
|
scannedAccounts: allItems.length,
|
|
8123
8428
|
validAccounts: validIds.length,
|
|
8124
8429
|
skippedInvalidOAuth: allItems.length - validIds.length,
|
|
@@ -8144,11 +8449,20 @@ async function runBalanceScan(opts) {
|
|
|
8144
8449
|
})) {
|
|
8145
8450
|
return;
|
|
8146
8451
|
}
|
|
8147
|
-
|
|
8148
|
-
|
|
8452
|
+
if (isSubset) {
|
|
8453
|
+
console.log(
|
|
8454
|
+
`
|
|
8455
|
+
${media} \u8D26\u6237\u4F59\u989D\u753B\u50CF\uFF08\u5B50\u96C6\u6A21\u5F0F\uFF0C-a \u6307\u5B9A ${filterIds.length} \u4E2A\u8D26\u6237\uFF0C\u5168\u90E8\u5C55\u793A\uFF09
|
|
8456
|
+
\u9608\u503C\u53C2\u8003\uFF1A\u7EED\u822A \u2264 ${thresholdDays} \u5929` + (minBalance !== null ? ` \u6216 \u4F59\u989D \u2264 ${minBalance}` : "") + `
|
|
8457
|
+
`
|
|
8458
|
+
);
|
|
8459
|
+
} else {
|
|
8460
|
+
console.log(
|
|
8461
|
+
`
|
|
8149
8462
|
${media} \u8D26\u6237\u4F59\u989D\u626B\u63CF\u5B8C\u6210\uFF1A\u5171\u626B\u63CF ${meta.scannedAccounts} \u4E2A\u8D26\u6237\uFF08\u6709\u6548 ${meta.validAccounts}\uFF09\uFF0C\u547D\u4E2D ${meta.hitCount} \u4E2A\uFF08\u9608\u503C\uFF1A\u7EED\u822A \u2264 ${thresholdDays} \u5929` + (minBalance !== null ? ` \u6216 \u4F59\u989D \u2264 ${minBalance}` : "") + `\uFF09
|
|
8150
8463
|
`
|
|
8151
|
-
|
|
8464
|
+
);
|
|
8465
|
+
}
|
|
8152
8466
|
if (evaluated.length === 0) {
|
|
8153
8467
|
console.log(" \u2705 \u65E0\u547D\u4E2D\u8D26\u6237\u3002\n");
|
|
8154
8468
|
return;
|
|
@@ -8164,6 +8478,7 @@ ${media} \u8D26\u6237\u4F59\u989D\u626B\u63CF\u5B8C\u6210\uFF1A\u5171\u626B\u63C
|
|
|
8164
8478
|
{ key: "reason", header: "\u547D\u4E2D\u539F\u56E0" }
|
|
8165
8479
|
];
|
|
8166
8480
|
const reasonText = {
|
|
8481
|
+
none: "\u2014\uFF08\u5B50\u96C6\u5C55\u793A\uFF0C\u672A\u89E6\u9608\u503C\uFF09",
|
|
8167
8482
|
"low-days": `\u7EED\u822A \u2264 ${thresholdDays} \u5929`,
|
|
8168
8483
|
"low-balance": `\u4F59\u989D \u2264 ${minBalance ?? 0}`,
|
|
8169
8484
|
both: `\u7EED\u822A + \u4F59\u989D\u53CC\u4F4E`
|
|
@@ -8197,6 +8512,7 @@ init_accounts_digest();
|
|
|
8197
8512
|
init_auth();
|
|
8198
8513
|
init_cli_json_snapshot();
|
|
8199
8514
|
init_cli_table();
|
|
8515
|
+
init_balance();
|
|
8200
8516
|
var VALID_MEDIA_TYPES3 = ["Google", "TikTok", "Yandex", "MetaAd", "BingV2", "Kwai"];
|
|
8201
8517
|
function defaultDateRange2() {
|
|
8202
8518
|
const end = /* @__PURE__ */ new Date();
|
|
@@ -8222,7 +8538,6 @@ async function runStats(opts) {
|
|
|
8222
8538
|
...opts.startDate ? { startDate: opts.startDate } : {},
|
|
8223
8539
|
...opts.endDate ? { endDate: opts.endDate } : {}
|
|
8224
8540
|
};
|
|
8225
|
-
const params = new URLSearchParams({ startDate, endDate, period: "true" });
|
|
8226
8541
|
if (!opts.accounts) {
|
|
8227
8542
|
console.error(
|
|
8228
8543
|
`
|
|
@@ -8234,11 +8549,33 @@ async function runStats(opts) {
|
|
|
8234
8549
|
process.exit(1);
|
|
8235
8550
|
}
|
|
8236
8551
|
const ids = opts.accounts.split(",").map((id) => id.trim()).filter(Boolean);
|
|
8237
|
-
|
|
8238
|
-
const
|
|
8239
|
-
|
|
8552
|
+
const isGoogle = opts.media === "Google";
|
|
8553
|
+
const params = isGoogle ? new URLSearchParams({ startDate, endDate, mediaCustomerIds: ids.join("|") }) : (() => {
|
|
8554
|
+
const p = new URLSearchParams({ startDate, endDate, period: "true" });
|
|
8555
|
+
p.set("mediaCustomerIds", ids.join(","));
|
|
8556
|
+
return p;
|
|
8557
|
+
})();
|
|
8558
|
+
const url = isGoogle ? `${config.apiBaseUrl}/report/media-account/google/account-spend-overview?${params.toString()}` : `${config.apiBaseUrl}/report/media-account/${opts.media}/accountsoverview?${params.toString()}`;
|
|
8559
|
+
let items;
|
|
8240
8560
|
try {
|
|
8241
|
-
|
|
8561
|
+
if (isGoogle) {
|
|
8562
|
+
const raw = await apiFetch2(url, config, {}, opts.verbose);
|
|
8563
|
+
items = parseGoogleAccountSpendOverviewRows(raw).map((row) => ({
|
|
8564
|
+
mediaAccountId: row.mediaAccountId,
|
|
8565
|
+
mediaCustomerName: row.mediaCustomerName,
|
|
8566
|
+
spend: row.spend,
|
|
8567
|
+
impressions: row.impressions,
|
|
8568
|
+
clicks: row.clicks,
|
|
8569
|
+
conversions: row.conversions,
|
|
8570
|
+
costPerClick: row.costPerClick,
|
|
8571
|
+
currencyCode: row.currencyCode,
|
|
8572
|
+
remainingAccountBudget: row.remainingAccountBudget,
|
|
8573
|
+
status: row.status
|
|
8574
|
+
}));
|
|
8575
|
+
} else {
|
|
8576
|
+
const raw = await apiFetch2(url, config, {}, opts.verbose);
|
|
8577
|
+
items = Array.isArray(raw) ? raw : raw.items ?? [];
|
|
8578
|
+
}
|
|
8242
8579
|
} catch (err) {
|
|
8243
8580
|
const message = err instanceof Error ? err.message : String(err);
|
|
8244
8581
|
if (await emitCliJsonOrSnapshot(opts, {
|
|
@@ -8255,7 +8592,6 @@ async function runStats(opts) {
|
|
|
8255
8592
|
`);
|
|
8256
8593
|
process.exit(1);
|
|
8257
8594
|
}
|
|
8258
|
-
const items = Array.isArray(raw) ? raw : raw.items ?? [];
|
|
8259
8595
|
const n = items.length;
|
|
8260
8596
|
const statsPayload = wrapListJson({ page: 1, pageSize: Math.max(n, 1), total: n, items });
|
|
8261
8597
|
if (await emitCliJsonOrSnapshot(opts, {
|
|
@@ -16026,6 +16362,7 @@ TikTok \u6CE8\u518C\u5730\u5217\u8868\uFF08\u7B2C 1 \u9875\uFF0C\u672C\u9875 ${r
|
|
|
16026
16362
|
init_defaults();
|
|
16027
16363
|
init_dist();
|
|
16028
16364
|
import * as readline2 from "readline";
|
|
16365
|
+
import * as os5 from "os";
|
|
16029
16366
|
function deriveWebBaseUrl(tsoApiBase) {
|
|
16030
16367
|
try {
|
|
16031
16368
|
const u = new URL(tsoApiBase);
|
|
@@ -16075,6 +16412,167 @@ function printPostLoginReminderBanner() {
|
|
|
16075
16412
|
];
|
|
16076
16413
|
console.log(lines.join("\n"));
|
|
16077
16414
|
}
|
|
16415
|
+
function parseAllowedServices(raw) {
|
|
16416
|
+
const allowed = ["CSO", "TSO", "CUT"];
|
|
16417
|
+
if (!raw) return ["TSO", "CUT"];
|
|
16418
|
+
const parts = raw.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
16419
|
+
const result = [];
|
|
16420
|
+
for (const p of parts) {
|
|
16421
|
+
if (!allowed.includes(p)) {
|
|
16422
|
+
throw new Error(`\u672A\u77E5\u670D\u52A1\u540D\u300C${p}\u300D\uFF0C\u53EF\u9009\uFF1A${allowed.join(" / ")}`);
|
|
16423
|
+
}
|
|
16424
|
+
if (!result.includes(p)) result.push(p);
|
|
16425
|
+
}
|
|
16426
|
+
if (result.length === 0) {
|
|
16427
|
+
throw new Error("--services \u81F3\u5C11\u9700\u8981\u6307\u5B9A\u4E00\u4E2A\u670D\u52A1");
|
|
16428
|
+
}
|
|
16429
|
+
return result;
|
|
16430
|
+
}
|
|
16431
|
+
function defaultApiKeyName() {
|
|
16432
|
+
const host = (() => {
|
|
16433
|
+
try {
|
|
16434
|
+
return os5.hostname() || "unknown";
|
|
16435
|
+
} catch {
|
|
16436
|
+
return "unknown";
|
|
16437
|
+
}
|
|
16438
|
+
})();
|
|
16439
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
16440
|
+
return `CLI - ${host} - ${today}`;
|
|
16441
|
+
}
|
|
16442
|
+
function validateAndNormalizePhone(rawInput) {
|
|
16443
|
+
const rawPhone = rawInput?.trim() ?? "";
|
|
16444
|
+
if (!isValidChinaPhone(rawPhone)) {
|
|
16445
|
+
console.error(
|
|
16446
|
+
"\n\u274C \u624B\u673A\u53F7\u683C\u5F0F\u9519\u8BEF\uFF1A\u4EC5\u652F\u6301\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7\uFF0C\u53EF\u5E26\u6216\u4E0D\u5E26 +86\uFF08\u5982 13800138000 / +8613800138000\uFF09\u3002\n"
|
|
16447
|
+
);
|
|
16448
|
+
process.exit(1);
|
|
16449
|
+
}
|
|
16450
|
+
return normalizeChinaPhone(rawPhone);
|
|
16451
|
+
}
|
|
16452
|
+
async function runSendLoginCode(opts) {
|
|
16453
|
+
const phone = validateAndNormalizePhone(opts.phone);
|
|
16454
|
+
const tsoApiBase = process.env.SILUZAN_TSO_API_BASE ?? DEFAULT_API_BASE;
|
|
16455
|
+
const ssoBaseUrl = deriveSsoBaseUrl(tsoApiBase);
|
|
16456
|
+
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");
|
|
16457
|
+
console.log(" Siluzan \u53D1\u9001\u767B\u5F55\u77ED\u4FE1\u9A8C\u8BC1\u7801");
|
|
16458
|
+
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");
|
|
16459
|
+
console.log(` \u624B\u673A\u53F7 : ${phone}`);
|
|
16460
|
+
console.log(` SSO : ${ssoBaseUrl}
|
|
16461
|
+
`);
|
|
16462
|
+
console.log("\u2192 \u6B63\u5728\u5411\u624B\u673A\u53D1\u9001\u9A8C\u8BC1\u7801...");
|
|
16463
|
+
const r = await sendPhoneLoginCode({ ssoBaseUrl, phone, verbose: opts.verbose });
|
|
16464
|
+
if (!r.ok) {
|
|
16465
|
+
console.error(`
|
|
16466
|
+
\u274C \u77ED\u4FE1\u9A8C\u8BC1\u7801\u53D1\u9001\u5931\u8D25\uFF1A${r.message || "(\u540E\u7AEF\u672A\u8FD4\u56DE\u539F\u56E0)"}
|
|
16467
|
+
`);
|
|
16468
|
+
process.exit(1);
|
|
16469
|
+
}
|
|
16470
|
+
console.log(`\u2713 \u9A8C\u8BC1\u7801\u5DF2\u53D1\u9001\uFF0810 \u5206\u949F\u5185\u6709\u6548\uFF09\u3002
|
|
16471
|
+
`);
|
|
16472
|
+
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");
|
|
16473
|
+
console.log(` siluzan-tso login --phone ${phone} --code <6\u4F4D\u9A8C\u8BC1\u7801>
|
|
16474
|
+
`);
|
|
16475
|
+
console.log("\u53EF\u9009\u53C2\u6570\uFF1A--name / --valid-days / --expires-at / --services");
|
|
16476
|
+
console.log("\uFF08\u9ED8\u8BA4\u521B\u5EFA 90 \u5929\u6709\u6548\u3001\u52FE\u9009 TSO + CUT \u670D\u52A1\u7684 API Key\uFF09\n");
|
|
16477
|
+
}
|
|
16478
|
+
async function runPhoneLogin(opts) {
|
|
16479
|
+
const phone = validateAndNormalizePhone(opts.phone);
|
|
16480
|
+
const code = opts.code?.trim() ?? "";
|
|
16481
|
+
if (!code) {
|
|
16482
|
+
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");
|
|
16483
|
+
console.error(` 1) siluzan-tso send-login-code --phone ${phone}`);
|
|
16484
|
+
console.error(` 2) siluzan-tso login --phone ${phone} --code <\u6536\u5230\u76846\u4F4D\u9A8C\u8BC1\u7801>
|
|
16485
|
+
`);
|
|
16486
|
+
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");
|
|
16487
|
+
process.exit(1);
|
|
16488
|
+
}
|
|
16489
|
+
let allowedServices;
|
|
16490
|
+
try {
|
|
16491
|
+
allowedServices = parseAllowedServices(opts.services);
|
|
16492
|
+
} catch (e) {
|
|
16493
|
+
console.error(`
|
|
16494
|
+
\u274C ${e instanceof Error ? e.message : String(e)}
|
|
16495
|
+
`);
|
|
16496
|
+
process.exit(1);
|
|
16497
|
+
return;
|
|
16498
|
+
}
|
|
16499
|
+
if (opts.validDays !== void 0 && opts.expiresAt !== void 0) {
|
|
16500
|
+
console.warn("\u26A0\uFE0F --valid-days \u4E0E --expires-at \u540C\u65F6\u4F20\u5165\uFF0C\u5C06\u4EE5 --expires-at \u4E3A\u51C6\u3002");
|
|
16501
|
+
}
|
|
16502
|
+
const tsoApiBase = process.env.SILUZAN_TSO_API_BASE ?? DEFAULT_API_BASE;
|
|
16503
|
+
const ssoBaseUrl = deriveSsoBaseUrl(tsoApiBase);
|
|
16504
|
+
const csoBaseUrl = deriveCsoApiBaseUrl(tsoApiBase);
|
|
16505
|
+
const apiKeyName = opts.name ?? defaultApiKeyName();
|
|
16506
|
+
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");
|
|
16507
|
+
console.log(" Siluzan \u767B\u5F55\uFF08\u624B\u673A\u53F7 + \u77ED\u4FE1\u9A8C\u8BC1\u7801\uFF09");
|
|
16508
|
+
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");
|
|
16509
|
+
console.log(` \u624B\u673A\u53F7 : ${phone}`);
|
|
16510
|
+
console.log(` \u9A8C\u8BC1\u7801 : ${code}`);
|
|
16511
|
+
console.log(` SSO : ${ssoBaseUrl}`);
|
|
16512
|
+
console.log(` CSO API : ${csoBaseUrl}`);
|
|
16513
|
+
console.log(` \u670D\u52A1\u8303\u56F4 : ${allowedServices.join(", ")}`);
|
|
16514
|
+
if (opts.expiresAt) {
|
|
16515
|
+
console.log(` \u8FC7\u671F\u65F6\u95F4 : ${opts.expiresAt}`);
|
|
16516
|
+
} else {
|
|
16517
|
+
console.log(` \u6709\u6548\u671F : ${opts.validDays ?? 90} \u5929`);
|
|
16518
|
+
}
|
|
16519
|
+
console.log(` API Key \u540D\u79F0 : ${apiKeyName}
|
|
16520
|
+
`);
|
|
16521
|
+
console.log("\u2192 \u6B63\u5728\u6821\u9A8C\u9A8C\u8BC1\u7801\u5E76\u521B\u5EFA API Key...");
|
|
16522
|
+
try {
|
|
16523
|
+
const created = await issueApiKeyWithPhoneCode({
|
|
16524
|
+
ssoBaseUrl,
|
|
16525
|
+
csoBaseUrl,
|
|
16526
|
+
phone,
|
|
16527
|
+
code,
|
|
16528
|
+
apiKeyName,
|
|
16529
|
+
validDays: opts.expiresAt ? void 0 : opts.validDays ?? 90,
|
|
16530
|
+
expiresAt: opts.expiresAt,
|
|
16531
|
+
allowedServices,
|
|
16532
|
+
verbose: opts.verbose
|
|
16533
|
+
});
|
|
16534
|
+
writeSharedConfig({ apiKey: created.rawKey });
|
|
16535
|
+
console.log("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
16536
|
+
console.log("\u2705 \u767B\u5F55\u6210\u529F\uFF0CAPI Key \u5DF2\u521B\u5EFA\u5E76\u4FDD\u5B58");
|
|
16537
|
+
console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
16538
|
+
console.log(` Key ID : ${created.id}`);
|
|
16539
|
+
console.log(` Key Name : ${created.name}`);
|
|
16540
|
+
console.log(` Key (\u663E\u793A) : ${maskSecret(created.rawKey)}`);
|
|
16541
|
+
console.log(` Key (\u8131\u654F) : ${created.keyMasked}`);
|
|
16542
|
+
console.log(` \u751F\u6548\u65F6\u95F4 : ${created.validFrom}`);
|
|
16543
|
+
console.log(` \u8FC7\u671F\u65F6\u95F4 : ${created.expiresAt}`);
|
|
16544
|
+
console.log(` \u5269\u4F59\u5929\u6570 : ${created.remainingDays} \u5929`);
|
|
16545
|
+
console.log(` \u72B6\u6001 : ${created.status}`);
|
|
16546
|
+
console.log(` \u914D\u7F6E\u6587\u4EF6 : ${CONFIG_FILE}`);
|
|
16547
|
+
printPostLoginReminderBanner();
|
|
16548
|
+
console.log("\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
|
|
16549
|
+
console.log(" siluzan-tso list-accounts \u67E5\u770B\u5E7F\u544A\u8D26\u6237\u5217\u8868");
|
|
16550
|
+
console.log(" siluzan-tso balance -m Google \u67E5\u770B\u8D26\u6237\u4F59\u989D\n");
|
|
16551
|
+
} catch (e) {
|
|
16552
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
16553
|
+
console.error(`
|
|
16554
|
+
\u274C \u767B\u5F55\u5931\u8D25\uFF1A${msg}`);
|
|
16555
|
+
if (/手机未注册|未注册/.test(msg)) {
|
|
16556
|
+
console.error(
|
|
16557
|
+
`
|
|
16558
|
+
\u8BE5\u624B\u673A\u53F7\u5C1A\u672A\u6CE8\u518C\u4E1D\u8DEF\u8D5E\u8D26\u53F7\u3002\u8BF7\u5148\u5728\u7F51\u9875\u7AEF\u6CE8\u518C\uFF1A
|
|
16559
|
+
${WEB_BASE_URL}
|
|
16560
|
+
\u6CE8\u518C\u6210\u529F\u540E\u518D\u56DE\u5230 CLI \u91CD\u8BD5\u4E24\u6BB5\u5F0F\u767B\u5F55\uFF1A
|
|
16561
|
+
siluzan-tso send-login-code --phone ${phone}
|
|
16562
|
+
siluzan-tso login --phone ${phone} --code <6\u4F4D\u9A8C\u8BC1\u7801>
|
|
16563
|
+
`
|
|
16564
|
+
);
|
|
16565
|
+
} else if (/验证码错误|验证码/.test(msg)) {
|
|
16566
|
+
console.error(
|
|
16567
|
+
`
|
|
16568
|
+
\u9A8C\u8BC1\u7801\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u8F93\u9519\u3002\u8BF7\u91CD\u65B0\u53D1\u7801\u540E\u518D\u8BD5\uFF1A
|
|
16569
|
+
siluzan-tso send-login-code --phone ${phone}
|
|
16570
|
+
`
|
|
16571
|
+
);
|
|
16572
|
+
}
|
|
16573
|
+
process.exit(1);
|
|
16574
|
+
}
|
|
16575
|
+
}
|
|
16078
16576
|
async function runLogin(opts = {}) {
|
|
16079
16577
|
if (opts.apiKey !== void 0) {
|
|
16080
16578
|
const key = opts.apiKey.trim();
|
|
@@ -16090,6 +16588,10 @@ async function runLogin(opts = {}) {
|
|
|
16090
16588
|
console.log("\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C siluzan-tso -h \u547D\u4EE4\u67E5\u770B\u5E2E\u52A9\u4E86\n");
|
|
16091
16589
|
return;
|
|
16092
16590
|
}
|
|
16591
|
+
if (opts.phone !== void 0) {
|
|
16592
|
+
await runPhoneLogin(opts);
|
|
16593
|
+
return;
|
|
16594
|
+
}
|
|
16093
16595
|
const shared = readSharedConfig();
|
|
16094
16596
|
const currentKey = shared.apiKey ?? "";
|
|
16095
16597
|
if (currentKey) {
|
|
@@ -16183,14 +16685,38 @@ program.hook("preAction", async () => {
|
|
|
16183
16685
|
setWriteAuditCliCommit(extractCommitFromArgv(process.argv.slice(2)));
|
|
16184
16686
|
setCliInvocationForAudit(process.argv.slice(2));
|
|
16185
16687
|
const activeCmd = process.argv[2];
|
|
16186
|
-
if (activeCmd !== "update" && activeCmd !== "login" && activeCmd !== "audit") {
|
|
16688
|
+
if (activeCmd !== "update" && activeCmd !== "login" && activeCmd !== "send-login-code" && activeCmd !== "audit") {
|
|
16187
16689
|
await notifyIfOutdated().catch(() => {
|
|
16188
16690
|
});
|
|
16189
16691
|
}
|
|
16190
16692
|
});
|
|
16191
|
-
program.command("login").description(
|
|
16192
|
-
|
|
16693
|
+
program.command("send-login-code").description(
|
|
16694
|
+
"\u5411\u624B\u673A\u53D1\u9001 SSO_LOGIN \u77ED\u4FE1\u9A8C\u8BC1\u7801\uFF08\u624B\u673A\u53F7\u767B\u5F55\u7684\u7B2C\u4E00\u6B65\uFF09\uFF1B\u7ACB\u5373\u8FD4\u56DE\uFF0C\u65E0\u4EFB\u4F55\u4EA4\u4E92\u3002\n \u6536\u5230\u9A8C\u8BC1\u7801\u540E\u7528 `siluzan-tso login --phone xxx --code xxx` \u5B8C\u6210\u767B\u5F55\u3002"
|
|
16695
|
+
).requiredOption(
|
|
16696
|
+
"--phone <phone>",
|
|
16697
|
+
"\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7\uFF0C\u53EF\u5E26\u6216\u4E0D\u5E26 +86\uFF08\u5982 13800138000 / +8613800138000\uFF09"
|
|
16698
|
+
).option("--verbose", "\u8F93\u51FA\u6BCF\u6B21 HTTP \u8BF7\u6C42 URL\uFF08\u8C03\u8BD5\u7528\uFF09").action(async (opts) => {
|
|
16699
|
+
await runSendLoginCode({ phone: opts.phone, verbose: opts.verbose });
|
|
16193
16700
|
});
|
|
16701
|
+
program.command("login").description(
|
|
16702
|
+
"\u4FDD\u5B58\u8BA4\u8BC1\u51ED\u636E\u5230 ~/.siluzan/config.json\uFF1A\n \xB7 --api-key <key> \u76F4\u63A5\u4FDD\u5B58 API Key\uFF08CI/CD \u63A8\u8350\uFF09\n \xB7 --phone <\u624B\u673A\u53F7> --code <\u9A8C\u8BC1\u7801> \u4E24\u6BB5\u5F0F\u767B\u5F55\u7B2C\u4E8C\u6B65\uFF08\u5148\u8C03 send-login-code\uFF09\n \xB7 \u65E0\u53C2\u6570 \u4EA4\u4E92\u5F0F\u5F15\u5BFC\u7528\u6237\u53BB\u7F51\u9875\u7C98\u8D34 API Key"
|
|
16703
|
+
).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\u6700\u9AD8\u4F18\u5148\u7EA7").option("--phone <phone>", "\u4E2D\u56FD\u5927\u9646\u624B\u673A\u53F7\uFF0C\u9700\u914D\u5408 --code \u4E00\u8D77\u4F7F\u7528\uFF08\u5FC5\u987B\u5148\u8C03 send-login-code \u53D1\u7801\uFF09").option("--code <code>", "\u77ED\u4FE1\u9A8C\u8BC1\u7801\uFF086 \u4F4D\uFF09\uFF1B\u4E0E --phone \u4E00\u8D77\u5FC5\u586B").option("--name <name>", "\u81EA\u52A8\u521B\u5EFA\u7684 API Key \u540D\u79F0\uFF0C\u9ED8\u8BA4 `CLI - <hostname> - <yyyy-MM-dd>`").option("--valid-days <days>", "API Key \u6709\u6548\u671F\uFF08\u5929\uFF09\uFF0C\u9ED8\u8BA4 90", (v) => parseInt(v, 10)).option("--expires-at <iso8601>", "API Key \u7EDD\u5BF9\u8FC7\u671F\u65F6\u95F4\uFF08ISO 8601\uFF09\uFF0C\u4E0E --valid-days \u4E8C\u9009\u4E00").option(
|
|
16704
|
+
"--services <list>",
|
|
16705
|
+
"API Key \u53EF\u8BBF\u95EE\u7684\u670D\u52A1\u5217\u8868\uFF0C\u9017\u53F7\u5206\u9694\uFF1B\u53EF\u9009 CSO/TSO/CUT\uFF0C\u9ED8\u8BA4 `TSO,CUT`"
|
|
16706
|
+
).option("--manual", "\u4EA4\u4E92\u5F0F\u7C98\u8D34 JWT Token\uFF08\u65E0 API Key \u65F6\u4F7F\u7528\uFF0C\u5DF2\u5E9F\u5F03\uFF09").option("--verbose", "\u8F93\u51FA\u6BCF\u6B21 HTTP \u8BF7\u6C42 URL\uFF08\u8C03\u8BD5\u7528\uFF09").action(
|
|
16707
|
+
async (opts) => {
|
|
16708
|
+
await runLogin({
|
|
16709
|
+
apiKey: opts.apiKey,
|
|
16710
|
+
phone: opts.phone,
|
|
16711
|
+
code: opts.code,
|
|
16712
|
+
name: opts.name,
|
|
16713
|
+
validDays: opts.validDays,
|
|
16714
|
+
expiresAt: opts.expiresAt,
|
|
16715
|
+
services: opts.services,
|
|
16716
|
+
verbose: opts.verbose
|
|
16717
|
+
});
|
|
16718
|
+
}
|
|
16719
|
+
);
|
|
16194
16720
|
var configCmd = program.command("config").description("\u67E5\u770B\u6216\u8BBE\u7F6E\u8BA4\u8BC1\u914D\u7F6E\uFF08\u4E0E siluzan-cso \u5171\u7528 ~/.siluzan/config.json\uFF09");
|
|
16195
16721
|
configCmd.command("show").description("\u5C55\u793A\u5F53\u524D\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\uFF08Token \u8131\u654F\u663E\u793A\uFF09").action(() => cmdConfigShow());
|
|
16196
16722
|
configCmd.command("set").description("\u4FDD\u5B58\u914D\u7F6E\u5230 ~/.siluzan/config.json").option("--api-key <key>", "API Key\uFF08\u5728\u4E1D\u8DEF\u8D5E\u300C\u8BBE\u7F6E \u2192 API Key \u7BA1\u7406\u300D\u521B\u5EFA\uFF0C\u63A8\u8350\uFF09").option("-t, --token <token>", "JWT \u7528\u6237 Auth Token\uFF08\u4E0E --api-key \u4E8C\u9009\u4E00\uFF09").action((opts) => {
|
|
@@ -16363,10 +16889,13 @@ program.command("balance").description("\u67E5\u8BE2\u5E7F\u544A\u8D26\u6237\u5B
|
|
|
16363
16889
|
}
|
|
16364
16890
|
);
|
|
16365
16891
|
program.command("balance-scan").description(
|
|
16366
|
-
"\u4E00\u952E\u626B\u63CF\u67D0\u5A92\u4F53\u4E0B\u6240\u6709\u8D26\u6237\u7684\u4F59\u989D\uFF0C\u7B5B\u51FA\u7EED\u822A\u5929\u6570\u4E0D\u8DB3\u7684\u8D26\u6237\u3002\u66FF\u4EE3\u5FAA\u73AF\u8C03\u7528 balance \u7684\u4F20\u7EDF\u505A\u6CD5\uFF0C\u5178\u578B\u573A\u666F\uFF1A\u300C\u5E2E\u6211\u627E\u51FA 117 \u4E2A Bing \u8D26\u6237\u91CC\u4F59\u989D\u4E0D\u8DB3 7 \u5929\u7684\u300D\u3002"
|
|
16892
|
+
"\u4E00\u952E\u626B\u63CF\u67D0\u5A92\u4F53\u4E0B\u6240\u6709\u8D26\u6237\u7684\u4F59\u989D\uFF0C\u7B5B\u51FA\u7EED\u822A\u5929\u6570\u4E0D\u8DB3\u7684\u8D26\u6237\u3002\u66FF\u4EE3\u5FAA\u73AF\u8C03\u7528 balance \u7684\u4F20\u7EDF\u505A\u6CD5\uFF0C\u5178\u578B\u573A\u666F\uFF1A\u300C\u5E2E\u6211\u627E\u51FA 117 \u4E2A Bing \u8D26\u6237\u91CC\u4F59\u989D\u4E0D\u8DB3 7 \u5929\u7684\u300D\u3002\u5982\u5DF2\u77E5\u8D26\u6237 ID \u5B50\u96C6\uFF08\u5C11\u91CF\u8D26\u6237\u753B\u50CF\uFF09\uFF0C\u52A0 -a <id1,id2> \u8DF3\u8FC7\u6E05\u5355\u7FFB\u9875\u76F4\u63A5\u67E5\u3002"
|
|
16367
16893
|
).requiredOption(
|
|
16368
16894
|
"-m, --media <type>",
|
|
16369
16895
|
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | BingV2 | Kwai\uFF08MetaAd \u63A5\u53E3\u672A\u5F00\u653E\u4F59\u989D\u67E5\u8BE2\uFF09"
|
|
16896
|
+
).option(
|
|
16897
|
+
"-a, --accounts <ids>",
|
|
16898
|
+
"\u6307\u5B9A\u8D26\u6237 mediaCustomerId\uFF08\u9017\u53F7\u5206\u9694\uFF09\u3002\u5B50\u96C6\u6A21\u5F0F\uFF1A\u8DF3\u8FC7\u6E05\u5355\u7FFB\u9875\u76F4\u63A5\u67E5\u8FD9\u4E9B ID\uFF0C\u5168\u90E8\u5C55\u793A\uFF08\u4E0D\u518D\u6309\u9608\u503C\u4E22\u5F03\uFF09\uFF0C\u9002\u5408\u300C\u770B\u51E0\u4E2A\u53F7\u4F59\u989D/\u7EED\u822A\u300D\u7684\u8F7B\u91CF\u573A\u666F\uFF1B\u7559\u7A7A\u5219\u5168\u91CF\u626B\u63CF+\u9608\u503C\u8FC7\u6EE4\u3002"
|
|
16370
16899
|
).option(
|
|
16371
16900
|
"--threshold-days <n>",
|
|
16372
16901
|
"\u5269\u4F59\u7EED\u822A\u5929\u6570\u9608\u503C\uFF08\u9ED8\u8BA4 7\uFF0C\u6309\u8FD1 7 \u65E5\u65E5\u5747\u6D88\u8017\u4F30\u7B97\uFF09",
|
|
@@ -16392,6 +16921,7 @@ program.command("balance-scan").description(
|
|
|
16392
16921
|
await runBalanceScan({
|
|
16393
16922
|
token: opts.token,
|
|
16394
16923
|
media: opts.media,
|
|
16924
|
+
accounts: opts.accounts,
|
|
16395
16925
|
thresholdDays: opts.thresholdDays,
|
|
16396
16926
|
minBalance: opts.minBalance,
|
|
16397
16927
|
minDailySpend: opts.minDailySpend,
|
|
@@ -16410,7 +16940,10 @@ program.command("accounts-digest").description(
|
|
|
16410
16940
|
).requiredOption(
|
|
16411
16941
|
"-m, --media <type>",
|
|
16412
16942
|
"\u5A92\u4F53\u7C7B\u578B\uFF1AGoogle | TikTok | Yandex | BingV2 | Kwai\uFF08MetaAd \u65E0\u6D88\u8017\u6C47\u603B\u63A5\u53E3\uFF09"
|
|
16413
|
-
).option(
|
|
16943
|
+
).option(
|
|
16944
|
+
"-a, --accounts <ids>",
|
|
16945
|
+
"\u6307\u5B9A\u8D26\u6237 mediaCustomerId\uFF08\u9017\u53F7\u5206\u9694\uFF09\u3002\u5B50\u96C6\u6A21\u5F0F\uFF1A\u8DF3\u8FC7\u6E05\u5355\u7FFB\u9875\u76F4\u63A5\u67E5\u8FD9\u4E9B ID\uFF08\u516C\u53F8\u540D advertiserName \u4F1A\u7F3A\u5931\uFF09\uFF0C\u9002\u5408\u300C\u770B\u51E0\u4E2A\u53F7\u753B\u50CF\u300D\u7684\u8F7B\u91CF\u573A\u666F\uFF1B\u7559\u7A7A\u5219\u5168\u91CF\u7FFB\u6E05\u5355\u3002"
|
|
16946
|
+
).option("--start <date>", "\u7EDF\u8BA1\u5F00\u59CB\u65E5\u671F YYYY-MM-DD\uFF08SKILL \u8981\u6C42 AI \u5148\u4E0E\u7528\u6237\u786E\u8BA4\uFF09").option("--end <date>", "\u7EDF\u8BA1\u7ED3\u675F\u65E5\u671F YYYY-MM-DD\uFF08SKILL \u8981\u6C42 AI \u5148\u4E0E\u7528\u6237\u786E\u8BA4\uFF09").option("--min-spend <n>", "\u8FC7\u6EE4\uFF1A\u533A\u95F4\u5185\u6D88\u8017 \u2264 \u6B64\u503C\u7684\u8D26\u6237\u4E0D\u8FD4\u56DE\uFF08\u9ED8\u8BA4 0\uFF09", (v) => parseFloat(v)).option("--page-size <n>", "\u8D26\u6237\u6E05\u5355\u5206\u9875\u5927\u5C0F\uFF08\u9ED8\u8BA4 200\uFF0C\u4E0A\u9650 500\uFF09", (v) => parseInt(v, 10)).option("--max-pages <n>", "\u6700\u591A\u626B\u63CF\u9875\u6570\uFF08\u9ED8\u8BA4 20\uFF0C\u4E0A\u9650 200\uFF09", (v) => parseInt(v, 10)).option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--json", "\u8F93\u51FA\u7ED3\u6784\u5316 JSON\uFF0C\u542B data.items \u4E0E meta\uFF08\u533A\u95F4/\u6C47\u603B/\u5E01\u79CD\u5907\u6CE8\uFF09", false).option(
|
|
16414
16947
|
"--json-out <path>",
|
|
16415
16948
|
"\u843D\u76D8\uFF08\u76EE\u5F55\u6216 *.json \u6587\u4EF6\u8DEF\u5F84\uFF09\u5E76\u66F4\u65B0 cli-manifest[-<\u67E5\u8BE2id>].json\uFF08\u4E0E --json \u4E92\u65A5\uFF09\uFF1B\u76EE\u5F55\u6A21\u5F0F\u6587\u4EF6\u540D\u4E3A `<section>[-<\u67E5\u8BE2id>].json`\uFF1Bstdout \u4E00\u884C\u6458\u8981 JSON\uFF0C\u542B outlineFile\uFF08TS \u5F0F\u7C7B\u578B\u5728 `*.outline.txt`\uFF09",
|
|
16416
16949
|
void 0
|
package/dist/skill/SKILL.md
CHANGED
|
@@ -94,7 +94,7 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
|
|
|
94
94
|
### 硬规范
|
|
95
95
|
|
|
96
96
|
- **账户状态 ≠ 广告系列状态**:`stats`/`balance`/`list-accounts` 的 `status` 只表示账户是否可用,系列状态**必须**来自 `ad campaigns`。
|
|
97
|
-
- **数据时效性(实时 vs 每日同步)**:涉及「今天/当天/今日消耗」「实时消耗排行」前,**必须**先看 `references/account-analytics.md`
|
|
97
|
+
- **数据时效性(实时 vs 每日同步)**:涉及「今天/当天/今日消耗」「实时消耗排行」前,**必须**先看 `references/account-analytics.md` 顶部「数据时效性」表选对接口,TikTok / Yandex / BingV2 / Kwai 仍是 `accountsoverview` 同步昨天数据,**不能查今天**。
|
|
98
98
|
- **不确定时读文档**:先读对应 references 或用 `-h` 查看帮助,不要猜参数。
|
|
99
99
|
- **先查账户再操作**:`list-accounts -m [mediaType] -k [mediaCustomerId]`。
|
|
100
100
|
- **使用 `--json-out` 处理数据**:处理顺序:先读 `outlineFile`(schema 描述,扩展名 **`.outline.txt`** 不是 `.json`,**禁止 `require()`**,用 `fs.readFileSync(outlineFile,'utf8')` 取最后一行 TS 式类型字面量即可了解全部字段路径)→ 再让脚本读 `writtenFiles[0]`(真实数据 JSON)做聚合。**outline 通常几百字节、JSON 常见几 MB,先读 outline 能节省 2~3 个数量级的上下文**——尤其多账户多维度场景,直接 `Read` 全量 JSON 几次就把对话窗口塞满了。**禁止**把 outline 当数据、跳过 outline 猜字段、把 outline 内容贴给用户当结论。
|
|
@@ -135,8 +135,8 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
|
|
|
135
135
|
|
|
136
136
|
| 任务 | 推荐命令 | 禁止做法 |
|
|
137
137
|
|------|---------|---------|
|
|
138
|
-
| 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7
|
|
139
|
-
| 多账户投放画像 | `accounts-digest -m <媒体> [-a id1,id2] --start --end --json-out <dir
|
|
138
|
+
| 多账户余额 / 预算不足预警 | `balance-scan -m <媒体> --threshold-days 7`(已知 ID 子集加 `-a id1,id2,...` 跳过翻页) | 逐账户循环 `balance --accounts ...` |
|
|
139
|
+
| 多账户投放画像 | `accounts-digest -m <媒体> [-a id1,id2,...] --start --end --json-out <dir>`(传 `-a` 时跳过清单翻页直接查;公司名 advertiserName 会缺失) | 逐账户 `stats` |
|
|
140
140
|
| 多账户 × 多维度 Google 数据 | **全量账号**:`google-analysis-batch run`(省略 `-a`)**2~10 子集**:`google-analysis -a id1,id2,...`(自动转发 batch);**≥10 子集或需 resume**:`google-analysis-batch run -a id1,id2,...` | 外层 for-loop;先跑 `list-accounts -m Google` 再把 ID 拼进 `-a` |
|
|
141
141
|
| 多系列诊断 | `ad campaigns --json-out <dir>` + node 读文件过滤 | 逐系列 `ad campaign-get` |
|
|
142
142
|
|
|
@@ -167,13 +167,19 @@ Windows 注意:部分 Agent 客户端通过 PowerShell / cmd 代执行命令
|
|
|
167
167
|
|
|
168
168
|
### P2 · 多账户余额扫描
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
**全量巡检**:
|
|
171
171
|
```bash
|
|
172
172
|
siluzan-tso balance-scan -m BingV2 --threshold-days 7 --json-out ./snap-p2
|
|
173
173
|
# 可选:--min-balance 100 筛绝对余额;--target-days 60 算建议充值额
|
|
174
174
|
```
|
|
175
175
|
按 `remainingDays` 升序输出;消耗过低的僵尸账户不纳入预警。
|
|
176
176
|
|
|
177
|
+
**已知 ID 子集**(轻量画像,仅看几个号余额/续航):
|
|
178
|
+
```bash
|
|
179
|
+
siluzan-tso balance-scan -m Google -a id1,id2,id3 --json-out ./snap-p2-subset
|
|
180
|
+
```
|
|
181
|
+
跳过清单翻页,全部展示(不按阈值丢弃);`hitReason="none"` 表示该账户未触阈值。
|
|
182
|
+
|
|
177
183
|
### P3 · 多账户投放画像汇总
|
|
178
184
|
|
|
179
185
|
```bash
|
package/dist/skill/_meta.json
CHANGED
|
@@ -13,15 +13,17 @@
|
|
|
13
13
|
| `google-analysis --sections overview`(Google 网关 `OverviewSectionData`) | **实时** | ✅ 可查当天 | 当天/今日消耗、当天高消耗账号排行 |
|
|
14
14
|
| `google-analysis --sections campaign-types`(`types-summary`) | **实时** | ✅ 可查当天 | 当天系列类型分布 |
|
|
15
15
|
| `google-analysis` 其他维度(`campaigns` / `keywords` / `devices` / ...) | 实时(受 Google Ads API 同步延迟影响) | 可查当天,但当天可能尚未结算 | 周期分析、报告 |
|
|
16
|
-
| `stats
|
|
16
|
+
| `stats -m Google` / `balance-scan -m Google` / `accounts-digest -m Google` / `list-accounts -m Google` 合并消耗(TSO `account-spend-overview`,2026-05 起) | **后端自动分流**:窗口完全在历史 → `database` 模式(含余额/状态/币种/账户名 + 当期消耗);窗口含今天 → `googleCombined` 模式(仅实时消耗,无余额/状态/币种/账户名) | ✅ 可查当天(含今天时切实时聚合) | 历史回溯、巡检、余额续航估算;含今天时也能给出实时消耗 |
|
|
17
|
+
| `stats` / `balance-scan` / `accounts-digest` / `list-accounts` 的 **TikTok / Yandex / BingV2 / Kwai** 合并消耗(TSO `accountsoverview`) | **每日同步昨天** | ❌ 查今天会全为 0 | 历史回溯、巡检、余额续航估算(口径为"截至昨天") |
|
|
17
18
|
| `balance`(`GetMediaAccountInfo`) | 实时 | — | 仅当前余额,不反映消耗 |
|
|
18
19
|
|
|
19
20
|
**选用规则**:
|
|
20
21
|
|
|
21
|
-
- 「今天/当天/今日消耗」「实时消耗排行」 →
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
22
|
+
- 「今天/当天/今日消耗」「实时消耗排行」 → 优先 `google-analysis(-batch) --sections overview`,`--start` / `--end` 都设为今天;
|
|
23
|
+
- Google 单账户/批量取数也可直接 `stats -m Google` 把 `--end-date` 设为今天,后端会切到 `googleCombined` 模式给实时消耗(但**不会**返回余额/币种/账户名)。
|
|
24
|
+
- 「最近 N 天消耗 / 周报 / 月报 / 余额续航」 → `stats` / `balance-scan` / `accounts-digest`,默认窗口截至昨天即可(Google 此时走 `database`,包含完整字段)。
|
|
25
|
+
- **禁止**用 TikTok / Yandex / BingV2 / Kwai 的 `accountsoverview` 接口判断当天消耗(仍是每日同步)。
|
|
26
|
+
- **禁止**给非 Google 媒体的当天高消耗场景加 `--min-spend`:其预筛选来自非实时 `accountsoverview`,会把今天有消耗的账号当成 0 给筛掉。Google 媒体当 `--end-date` 设为今天时,预筛选走的是 `googleCombined` 实时数据,可以使用 `--min-spend`。
|
|
25
27
|
|
|
26
28
|
---
|
|
27
29
|
|
|
@@ -125,11 +125,18 @@ siluzan-tso balance -m Google -a 6326027735 --json
|
|
|
125
125
|
|
|
126
126
|
**单户余额与续航**:`balance` 只反映当前余额;判断「还能跑几天 / 是否够花」需结合 `stats`(或业务侧日均消耗)等数据。向用户展示 JSON 时,`mediaCustomerId` 须与本次 `-a` 查询的 ID 及命令输出一致。
|
|
127
127
|
|
|
128
|
+
**少量账户的画像/续航估算**:直接用 `balance-scan -m <媒体> -a id1,id2`(子集模式)一条命令拿齐余额 + 近 7 日消耗 + 续航天数 + 充值建议,比 `balance` + `stats` 拼接更省事;或者用 `accounts-digest -m <媒体> -a id1,id2` 拿齐消耗 + CTR/CPC/CPA 派生指标 + 余额。两者都会跳过清单翻页,直接对指定 ID 拉数据。
|
|
129
|
+
|
|
128
130
|
---
|
|
129
131
|
|
|
130
|
-
## stats —
|
|
132
|
+
## stats — 查询投放消耗数据
|
|
131
133
|
|
|
132
|
-
>
|
|
134
|
+
> **数据时效性**:
|
|
135
|
+
> - **Google**:走 `account-spend-overview`(2026-05 起),后端按日期窗口分流——
|
|
136
|
+
> - 窗口完全在历史 → `database` 模式:含余额、状态、币种、账户名、当期展点消等完整字段;
|
|
137
|
+
> - 窗口包含今天 → `googleCombined` 模式:只返回实时聚合的展点消(**没有**余额/状态/币种/账户名)。
|
|
138
|
+
> - **TikTok / Yandex / BingV2 / Kwai**:走旧版 `accountsoverview`,每日凌晨同步昨天数据,**不能查今天**。判断这几家的「今天/当天/今日消耗」仍需走 `google-analysis(-batch) --sections overview`(仅 Google)。
|
|
139
|
+
> - 完整时效性表见 `references/account-analytics.md` 顶部。
|
|
133
140
|
|
|
134
141
|
```bash
|
|
135
142
|
siluzan-tso stats -m <媒体类型> [选项]
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## invoice-info — 发票抬头管理
|
|
9
9
|
|
|
10
|
-
对应页面:`https://www.siluzan.com/v3/foreign_trade/settings/invoiceInformation`
|
|
10
|
+
对应页面:`https://www-ci.siluzan.com/v3/foreign_trade/settings/invoiceInformation`
|
|
11
11
|
|
|
12
12
|
发票抬头是开票申请时使用的公司/企业信息模板,支持三种类型:
|
|
13
13
|
|
|
@@ -133,10 +133,10 @@ siluzan-tso config show
|
|
|
133
133
|
**示例:**
|
|
134
134
|
|
|
135
135
|
```
|
|
136
|
-
- 现金充值(单笔):https://www.siluzan.com/recharge/pay
|
|
137
|
-
- 现金充值(批量):https://www.siluzan.com/recharge/pay_batch
|
|
138
|
-
- 月结充值: https://www.siluzan.com/recharge/accountBillingQuota
|
|
139
|
-
- 丝路赞钱包: https://www.siluzan.com/recharge/siluzanWallet
|
|
136
|
+
- 现金充值(单笔):https://www-ci.siluzan.com/recharge/pay
|
|
137
|
+
- 现金充值(批量):https://www-ci.siluzan.com/recharge/pay_batch
|
|
138
|
+
- 月结充值: https://www-ci.siluzan.com/recharge/accountBillingQuota
|
|
139
|
+
- 丝路赞钱包: https://www-ci.siluzan.com/recharge/siluzanWallet
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
---
|
|
@@ -214,8 +214,8 @@ siluzan-tso report list -m Google --json
|
|
|
214
214
|
|
|
215
215
|
# 第二步:查看 webUrl
|
|
216
216
|
siluzan-tso config show
|
|
217
|
-
# webUrl: https://www.siluzan.com
|
|
217
|
+
# webUrl: https://www-ci.siluzan.com
|
|
218
218
|
|
|
219
219
|
# 第三步:拼接链接(Google 日报)
|
|
220
|
-
# https://www.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
|
|
220
|
+
# https://www-ci.siluzan.com/media-report/publish/rpt_abc123?culture=zh-CN
|
|
221
221
|
```
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
## 安装 CLI
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install -g siluzan-tso-cli
|
|
13
|
+
npm install -g siluzan-tso-cli@beta
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
---
|
|
@@ -41,13 +41,55 @@ siluzan-tso init -d /path/to-your/skills # 写入自定义目录
|
|
|
41
41
|
`siluzan-tso` 与 `siluzan-cso` **共用同一份凭据**,存储在 `~/.siluzan/config.json`,配置一次两个 CLI 均可使用。
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
siluzan-tso login
|
|
45
|
-
siluzan-tso login --api-key <YOUR_API_KEY>
|
|
46
|
-
siluzan-tso
|
|
47
|
-
siluzan-tso
|
|
44
|
+
siluzan-tso login # 交互式登录,按提示创建 API Key 后粘贴
|
|
45
|
+
siluzan-tso login --api-key <YOUR_API_KEY> # 直接设置 API Key(跳过交互)
|
|
46
|
+
siluzan-tso send-login-code --phone 138xxxx # 两段式登录第 1 步:发送短信验证码
|
|
47
|
+
siluzan-tso login --phone 138xxxx --code 123456 # 两段式登录第 2 步:用验证码完成登录
|
|
48
|
+
siluzan-tso config set --api-key <Key> # 或通过 config set 直接写入
|
|
49
|
+
siluzan-tso config set --token <Token> # 备用:设置 JWT Token
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
API Key 获取入口:`https://www.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
|
|
52
|
+
API Key 获取入口:`https://www-ci.siluzan.com/v3/foreign_trade/settings/apiKeyManagement`
|
|
53
|
+
|
|
54
|
+
### 通过手机号 + 验证码登录(对话式 AI 推荐)
|
|
55
|
+
|
|
56
|
+
**两段式调用**,专为 AI Agent 设计——任何一步都不会进入交互等待,绝不会卡住 stdout。
|
|
57
|
+
拆分后单一职责:第 1 步只发码;第 2 步只用 code 换 API Key。这样 Agent 不会因为"看到 stdout 卡住就重试"而触发短信轰炸。
|
|
58
|
+
|
|
59
|
+
| 步骤 | 命令 | 说明 |
|
|
60
|
+
| ---- | ---- | ---- |
|
|
61
|
+
| 1 | `siluzan-tso send-login-code --phone <手机号>` | 仅向手机发送 6 位验证码,立即返回;**绝不创建 API Key** |
|
|
62
|
+
| 2 | `siluzan-tso login --phone <手机号> --code <验证码>` | 用 code 完成登录并自动签发 API Key 写入 `~/.siluzan/config.json` |
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 第 1 步:让用户报出手机号后,立刻发码(命令立即返回,不会等待输入)
|
|
66
|
+
siluzan-tso send-login-code --phone 13800138000
|
|
67
|
+
|
|
68
|
+
# 第 2 步:让用户读出收到的 6 位验证码,再调登录命令
|
|
69
|
+
siluzan-tso login --phone 13800138000 --code 123456
|
|
70
|
+
|
|
71
|
+
# 自定义 API Key 名称 / 有效期 / 服务范围
|
|
72
|
+
siluzan-tso login --phone 13800138000 --code 123456 \
|
|
73
|
+
--name "Cursor - my-mac - 2026" \
|
|
74
|
+
--valid-days 30 \
|
|
75
|
+
--services TSO,CUT
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| 参数 | 命令 | 说明 | 默认值 |
|
|
79
|
+
| ---- | ---- | ---- | ---- |
|
|
80
|
+
| `--phone` | 两个命令都需要 | 中国大陆手机号,可带或不带 +86(如 `13800138000` / `+8613800138000`);底层会自动补 `+86` 前缀;**手机号必须已在丝路赞网页端注册** | 必填 |
|
|
81
|
+
| `--code` | 仅 `login` | 6 位短信验证码(来自第 1 步发码后的短信);**login 命令必填**,未传会直接报错指引重新走两段式 | 必填(login 命令) |
|
|
82
|
+
| `--name` | 仅 `login` | 自动创建的 API Key 显示名称 | `CLI - <hostname> - <yyyy-MM-dd>` |
|
|
83
|
+
| `--valid-days` | 仅 `login` | API Key 有效期(天),与 `--expires-at` 二选一 | `90` |
|
|
84
|
+
| `--expires-at` | 仅 `login` | API Key 绝对过期时间(ISO 8601) | 不传则用 `--valid-days` |
|
|
85
|
+
| `--services` | 仅 `login` | 可访问的服务列表,逗号分隔;可选 `CSO`/`TSO`/`CUT` | `TSO,CUT`(广告投放 + 素材中心) |
|
|
86
|
+
| `--verbose` | 两个命令都支持 | 输出每次 HTTP 请求的 URL,便于排错 | 关闭 |
|
|
87
|
+
|
|
88
|
+
> **未注册手机号**:`login` 第 2 步会返回 `❌ 登录失败:手机未注册` 并附带网页注册地址,引导用户先去网页注册再回来重试两段式。
|
|
89
|
+
>
|
|
90
|
+
> **验证码错误/过期**:返回明确提示,让用户重新跑 `send-login-code` 拿新验证码。
|
|
91
|
+
>
|
|
92
|
+
> **AI 助手用法**:先调 `send-login-code` 发码、立即返回继续对话;等用户报出收到的验证码后,再调 `login --phone xxx --code xxx` 完成登录。**永远不要在没拿到 `--code` 的情况下调用 `login --phone xxx`,那会直接报错。**
|
|
51
93
|
|
|
52
94
|
### 通过环境变量传入凭据(CI/CD 推荐)
|
|
53
95
|
|
|
@@ -82,9 +124,9 @@ siluzan-tso config show
|
|
|
82
124
|
|
|
83
125
|
```
|
|
84
126
|
构建环境 : production
|
|
85
|
-
apiBaseUrl : https://tso-api.siluzan.com
|
|
86
|
-
googleApiUrl : https://googleapi.mysiluzan.com
|
|
87
|
-
webUrl : https://www.siluzan.com
|
|
127
|
+
apiBaseUrl : https://tso-api-ci.siluzan.com
|
|
128
|
+
googleApiUrl : https://googleapi-ci.mysiluzan.com
|
|
129
|
+
webUrl : https://www-ci.siluzan.com
|
|
88
130
|
apiKey : abcd****1234
|
|
89
131
|
```
|
|
90
132
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
用 `siluzan-tso config show` 读取 **`webUrl`**,再拼接路径:
|
|
8
8
|
|
|
9
|
-
首页地址:`https://www.siluzan.com/v3/foreign_trade/tso/home`
|
|
9
|
+
首页地址:`https://www-ci.siluzan.com/v3/foreign_trade/tso/home`
|
|
10
10
|
|
|
11
11
|
> 若用户已登录 TSO,也可从左侧菜单进入「首页」。
|
|
12
12
|
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
**CLI 对应关系(近似,非同一接口):**
|
|
36
36
|
|
|
37
|
-
- 单账户余额、消耗趋势:**`balance`**、**`stats
|
|
37
|
+
- 单账户余额、消耗趋势:**`balance`**、**`stats`**(Google 走新版 `account-spend-overview`,含今天会切实时模式;其余媒体仍是 `accountsoverview`),需已知 `mediaCustomerId`。
|
|
38
38
|
- 开户进度:**`account-history`**、**`open-account`** 系列。
|
|
39
39
|
- **充值**:无 CLI,见 `references/finance.md`,用 `webUrl` 引导至充值页。
|
|
40
40
|
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
**CLI 对应关系:**
|
|
54
54
|
|
|
55
|
-
- 单账户一段时间消耗/展示/点击:**`stats -m <媒体> -a <mediaCustomerId
|
|
55
|
+
- 单账户一段时间消耗/展示/点击:**`stats -m <媒体> -a <mediaCustomerId>`**(Google 用 `account-spend-overview` 自动按窗口分流历史/实时;其余媒体仍是 `accountsoverview`),日期可用默认或后续若支持传参则对齐页面。
|
|
56
56
|
- **图表级、多账户联动** 与 **`GetAccountDataOverview`**:**CLI 未封装**,需网页或后续扩展命令。
|
|
57
57
|
|
|
58
58
|
### 4. 服务推荐(`MoreFunctionSection`,部分首页布局显示)
|
|
@@ -10,8 +10,8 @@ $ErrorActionPreference = 'Stop'
|
|
|
10
10
|
$PKG_NAME = 'siluzan-tso-cli'
|
|
11
11
|
$CLI_BIN = 'siluzan-tso'
|
|
12
12
|
$SKILL_LABEL = 'Siluzan TSO'
|
|
13
|
-
$INSTALL_CMD = 'npm install -g siluzan-tso-cli'
|
|
14
|
-
$WEB_BASE = 'https://www.siluzan.com'
|
|
13
|
+
$INSTALL_CMD = 'npm install -g siluzan-tso-cli@beta'
|
|
14
|
+
$WEB_BASE = 'https://www-ci.siluzan.com'
|
|
15
15
|
|
|
16
16
|
# -- Constants ----------------------------------------------------------------
|
|
17
17
|
$NODE_MAJOR_MIN = 18
|
|
@@ -10,8 +10,8 @@ set -euo pipefail
|
|
|
10
10
|
readonly PKG_NAME="siluzan-tso-cli"
|
|
11
11
|
readonly CLI_BIN="siluzan-tso"
|
|
12
12
|
readonly SKILL_LABEL="Siluzan TSO"
|
|
13
|
-
readonly INSTALL_CMD="npm install -g siluzan-tso-cli"
|
|
14
|
-
readonly WEB_BASE="https://www.siluzan.com"
|
|
13
|
+
readonly INSTALL_CMD="npm install -g siluzan-tso-cli@beta"
|
|
14
|
+
readonly WEB_BASE="https://www-ci.siluzan.com"
|
|
15
15
|
|
|
16
16
|
# -- Constants ----------------------------------------------------------------
|
|
17
17
|
readonly NODE_MAJOR_MIN=18
|