siluzan-cso-cli 1.0.0-beta.28

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/dist/index.js ADDED
@@ -0,0 +1,2461 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runExtractCover
4
+ } from "./chunk-FZS2K2T3.js";
5
+
6
+ // src/index.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/commands/init.ts
10
+ import * as fs2 from "fs/promises";
11
+ import * as fsSync from "fs";
12
+ import * as os from "os";
13
+ import * as path2 from "path";
14
+ import { fileURLToPath } from "url";
15
+
16
+ // src/templates/load-templates.ts
17
+ import * as fs from "fs/promises";
18
+ import * as path from "path";
19
+ async function getSkillFiles(skillDir, apiBaseUrl) {
20
+ const out = {};
21
+ async function walk(dir, prefix) {
22
+ const entries = await fs.readdir(dir, { withFileTypes: true });
23
+ for (const ent of entries) {
24
+ const rel = prefix ? `${prefix}/${ent.name}` : ent.name;
25
+ const full = path.join(dir, ent.name);
26
+ if (ent.isDirectory()) {
27
+ await walk(full, rel);
28
+ } else {
29
+ let text = await fs.readFile(full, "utf8");
30
+ text = text.replace(/\{\{SILUZAN_API_BASE_URL\}\}/g, apiBaseUrl);
31
+ out[rel] = text;
32
+ }
33
+ }
34
+ }
35
+ await walk(skillDir, "");
36
+ return out;
37
+ }
38
+
39
+ // src/commands/init.ts
40
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
41
+ var TARGET_DIRS = {
42
+ cursor: (cwd) => path2.join(cwd, ".cursor", "skills", "siluzan-platform"),
43
+ claude: (cwd) => path2.join(cwd, ".claude", "skills", "siluzan-platform"),
44
+ /** OpenClaw 工作区技能目录,见 https://docs.openclaw.ai/skills */
45
+ "openclaw-workspace": (cwd) => path2.join(cwd, "skills", "siluzan-platform"),
46
+ "openclaw-global": (_cwd, home) => path2.join(home, ".openclaw", "skills", "siluzan-platform"),
47
+ /** WorkBuddy (CodeBuddy) 项目级技能目录 */
48
+ "workbuddy-workspace": (cwd) => path2.join(cwd, ".workbuddy", "skills", "siluzan-platform"),
49
+ /** WorkBuddy (CodeBuddy) 用户级技能目录(全局可用) */
50
+ "workbuddy-global": (_cwd, home) => path2.join(home, ".workbuddy", "skills", "siluzan-platform")
51
+ };
52
+ function parseTargets(raw) {
53
+ const normalized = raw.trim().toLowerCase();
54
+ if (normalized === "all") {
55
+ return [
56
+ "cursor",
57
+ "claude",
58
+ "openclaw-workspace",
59
+ "openclaw-global",
60
+ "workbuddy-workspace",
61
+ "workbuddy-global"
62
+ ];
63
+ }
64
+ const parts = normalized.split(",").map((s) => s.trim()).map((p) => {
65
+ if (p === "openclaw") return "openclaw-workspace";
66
+ if (p === "workbuddy") return "workbuddy-workspace";
67
+ return p;
68
+ });
69
+ const allowed = [
70
+ "cursor",
71
+ "claude",
72
+ "openclaw-workspace",
73
+ "openclaw-global",
74
+ "workbuddy-workspace",
75
+ "workbuddy-global"
76
+ ];
77
+ const result = [];
78
+ for (const p of parts) {
79
+ if (!allowed.includes(p)) {
80
+ console.error(
81
+ `\u672A\u77E5\u5E73\u53F0: ${p}\u3002\u53EF\u9009: cursor, claude, openclaw-workspace, openclaw-global, workbuddy-workspace, workbuddy-global, all`
82
+ );
83
+ process.exitCode = 1;
84
+ return [];
85
+ }
86
+ result.push(p);
87
+ }
88
+ return [...new Set(result)];
89
+ }
90
+ function skillRoot() {
91
+ return path2.join(__dirname, "skill");
92
+ }
93
+ function saveInstalledTargets(newEntries) {
94
+ const CONFIG_FILE4 = path2.join(os.homedir(), ".siluzan", "config.json");
95
+ try {
96
+ fsSync.mkdirSync(path2.dirname(CONFIG_FILE4), { recursive: true });
97
+ let existing = {};
98
+ if (fsSync.existsSync(CONFIG_FILE4)) {
99
+ existing = JSON.parse(fsSync.readFileSync(CONFIG_FILE4, "utf8"));
100
+ }
101
+ const prev = Array.isArray(existing.installedTargets) ? existing.installedTargets : [];
102
+ const merged = /* @__PURE__ */ new Map();
103
+ for (const e of [...prev, ...newEntries]) {
104
+ merged.set(`${e.target}::${e.cwd}`, e);
105
+ }
106
+ fsSync.writeFileSync(
107
+ CONFIG_FILE4,
108
+ JSON.stringify({ ...existing, installedTargets: [...merged.values()] }, null, 2),
109
+ "utf8"
110
+ );
111
+ if (process.platform !== "win32") {
112
+ fsSync.chmodSync(CONFIG_FILE4, 384);
113
+ }
114
+ } catch {
115
+ }
116
+ }
117
+ async function writeSkillFilesToDir(destDir, skillFiles, force) {
118
+ await fs2.mkdir(destDir, { recursive: true });
119
+ let anyWritten = false;
120
+ for (const [relativePath, content] of Object.entries(skillFiles)) {
121
+ const fullPath = path2.join(destDir, relativePath);
122
+ await fs2.mkdir(path2.dirname(fullPath), { recursive: true });
123
+ try {
124
+ await fs2.access(fullPath);
125
+ if (!force) {
126
+ console.warn(`\u8DF3\u8FC7\uFF08\u5DF2\u5B58\u5728\uFF0C\u4F7F\u7528 --force \u8986\u76D6\uFF09: ${fullPath}`);
127
+ continue;
128
+ }
129
+ } catch {
130
+ }
131
+ await fs2.writeFile(fullPath, content, "utf8");
132
+ console.log(`\u5DF2\u5199\u5165: ${fullPath}`);
133
+ anyWritten = true;
134
+ }
135
+ return anyWritten;
136
+ }
137
+ async function runInit(options) {
138
+ const home = os.homedir();
139
+ const skillFiles = await getSkillFiles(skillRoot(), options.apiBaseUrl);
140
+ const installedEntries = [];
141
+ if (options.dir) {
142
+ const destDir = path2.resolve(options.cwd, options.dir);
143
+ console.log(`\u5B89\u88C5\u76EE\u6807\u76EE\u5F55\uFF1A${destDir}`);
144
+ const anyWritten = await writeSkillFilesToDir(destDir, skillFiles, options.force);
145
+ if (anyWritten) {
146
+ installedEntries.push({ target: "custom", cwd: "", dir: destDir });
147
+ }
148
+ } else {
149
+ const targets = parseTargets(options.aiTargets);
150
+ if (targets.length === 0) return;
151
+ for (const target of targets) {
152
+ const destDir = TARGET_DIRS[target](options.cwd, home);
153
+ console.log(`[${target}] \u2192 ${destDir}`);
154
+ const anyWritten = await writeSkillFilesToDir(destDir, skillFiles, options.force);
155
+ if (anyWritten) {
156
+ const isGlobal = target === "openclaw-global" || target === "workbuddy-global";
157
+ installedEntries.push({ target, cwd: isGlobal ? "" : options.cwd });
158
+ }
159
+ }
160
+ }
161
+ if (installedEntries.length > 0) {
162
+ saveInstalledTargets(installedEntries);
163
+ }
164
+ console.log("\n\u4E0B\u4E00\u6B65\uFF1A");
165
+ console.log(
166
+ "1. \u9996\u6B21\u4F7F\u7528\u8BF7\u8FD0\u884C\uFF1Asiluzan-cso login \uFF08\u5F15\u5BFC\u6CE8\u518C/\u767B\u5F55\u5E76\u4FDD\u5B58 Token\uFF09"
167
+ );
168
+ console.log(
169
+ "2. \u540E\u7EED\u5347\u7EA7\u8FD0\u884C\uFF1Asiluzan-cso update \uFF08\u81EA\u52A8\u66F4\u65B0 CLI \u4E0E skill \u6587\u4EF6\uFF09"
170
+ );
171
+ console.log(
172
+ "3. OpenClaw \u5168\u5C40\u6280\u80FD\u82E5\u672A\u751F\u6548\uFF0C\u8BF7\u5728 ~/.openclaw/openclaw.json \u7684 skills.load.extraDirs \u4E2D\u52A0\u5165\u6280\u80FD\u7236\u76EE\u5F55\u3002"
173
+ );
174
+ console.log(
175
+ "4. WorkBuddy \u6280\u80FD\u5B89\u88C5\u540E\u91CD\u542F WorkBuddy \u5373\u53EF\u751F\u6548\uFF08~/.workbuddy/skills/ \u6216\u9879\u76EE .workbuddy/skills/\uFF09\u3002"
176
+ );
177
+ }
178
+
179
+ // src/commands/login.ts
180
+ import * as readline from "readline";
181
+
182
+ // ../common/dist/index.js
183
+ import * as fs3 from "fs";
184
+ import * as path3 from "path";
185
+ import * as os2 from "os";
186
+ import * as https from "https";
187
+ import * as http from "http";
188
+ import * as os22 from "os";
189
+ import { randomUUID as _randomUUID } from "crypto";
190
+ import * as fs22 from "fs";
191
+ import * as path22 from "path";
192
+ import { fileURLToPath as fileURLToPath2 } from "url";
193
+ var SILUZAN_DIR = path3.join(os2.homedir(), ".siluzan");
194
+ var CONFIG_FILE = path3.join(SILUZAN_DIR, "config.json");
195
+ function readStr(raw, key) {
196
+ const v = raw[key];
197
+ return typeof v === "string" && v ? v : void 0;
198
+ }
199
+ function readSharedConfig() {
200
+ if (!fs3.existsSync(CONFIG_FILE)) {
201
+ return { authToken: "" };
202
+ }
203
+ try {
204
+ const raw = JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf8"));
205
+ return {
206
+ authToken: readStr(raw, "authToken") ?? "",
207
+ apiKey: readStr(raw, "apiKey"),
208
+ apiBaseUrl: readStr(raw, "apiBaseUrl"),
209
+ tsoApiBaseUrl: readStr(raw, "tsoApiBaseUrl"),
210
+ googleApiUrl: readStr(raw, "googleApiUrl"),
211
+ dataPermission: readStr(raw, "dataPermission")
212
+ };
213
+ } catch {
214
+ return { authToken: "" };
215
+ }
216
+ }
217
+ function writeSharedConfig(partial) {
218
+ fs3.mkdirSync(SILUZAN_DIR, { recursive: true });
219
+ let existing = {};
220
+ if (fs3.existsSync(CONFIG_FILE)) {
221
+ try {
222
+ existing = JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf8"));
223
+ } catch {
224
+ }
225
+ }
226
+ const keys = [
227
+ "authToken",
228
+ "apiKey",
229
+ "apiBaseUrl",
230
+ "tsoApiBaseUrl",
231
+ "googleApiUrl",
232
+ "dataPermission"
233
+ ];
234
+ for (const k of keys) {
235
+ if (partial[k] !== void 0) existing[k] = partial[k];
236
+ }
237
+ fs3.writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2), "utf8");
238
+ if (process.platform !== "win32") {
239
+ try {
240
+ fs3.chmodSync(CONFIG_FILE, 384);
241
+ } catch {
242
+ console.warn("\u26A0\uFE0F \u672A\u80FD\u6536\u655B\u914D\u7F6E\u6587\u4EF6\u6743\u9650\uFF0C\u8BF7\u624B\u52A8\u6267\u884C\uFF1Achmod 600 " + CONFIG_FILE);
243
+ }
244
+ }
245
+ }
246
+ function clearSharedConfig() {
247
+ writeSharedConfig({ authToken: "", apiKey: "" });
248
+ }
249
+ function maskSecret(s) {
250
+ if (!s) return "(\u672A\u8BBE\u7F6E)";
251
+ return s.length > 8 ? `${s.slice(0, 4)}****${s.slice(-4)}` : "****";
252
+ }
253
+ var ALLOWED_HOSTNAME_SUFFIXES = [
254
+ "siluzan.com",
255
+ "siluzan.cn",
256
+ /** Google / TikTok / Facebook 等媒体网关域名(TSO 专用,CSO 不会主动产生但兼容写入) */
257
+ "mysiluzan.com"
258
+ ];
259
+ function validateBaseUrl(raw) {
260
+ let url;
261
+ try {
262
+ url = new URL(raw);
263
+ } catch {
264
+ return `\u4E0D\u662F\u5408\u6CD5 URL\uFF1A${raw}`;
265
+ }
266
+ if (url.protocol !== "https:") {
267
+ return `\u5FC5\u987B\u4F7F\u7528 HTTPS\uFF0C\u5F53\u524D\u534F\u8BAE\uFF1A${url.protocol}`;
268
+ }
269
+ const hostname = url.hostname.toLowerCase();
270
+ const ok = ALLOWED_HOSTNAME_SUFFIXES.some(
271
+ (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`)
272
+ );
273
+ if (!ok) {
274
+ return `\u4E3B\u673A\u540D "${hostname}" \u4E0D\u5728\u5141\u8BB8\u5217\u8868\uFF08${ALLOWED_HOSTNAME_SUFFIXES.join("\u3001")}\uFF09\u5185\u3002
275
+ \u5982\u9700\u8FDE\u63A5\u81EA\u5B9A\u4E49\u90E8\u7F72\u7AEF\u70B9\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u6DFB\u52A0\u767D\u540D\u5355\u3002`;
276
+ }
277
+ return null;
278
+ }
279
+ function rawRequest(url, options) {
280
+ return new Promise((resolve4, reject) => {
281
+ const parsed = new URL(url);
282
+ const transport = parsed.protocol === "https:" ? https : http;
283
+ const reqOpts = {
284
+ hostname: parsed.hostname,
285
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
286
+ path: parsed.pathname + parsed.search,
287
+ method: options.method ?? "GET",
288
+ headers: options.headers
289
+ };
290
+ const req = transport.request(reqOpts, (res) => {
291
+ let data = "";
292
+ res.setEncoding("utf8");
293
+ res.on("data", (chunk) => {
294
+ data += chunk;
295
+ });
296
+ res.on("end", () => resolve4({ status: res.statusCode ?? 0, text: data }));
297
+ });
298
+ req.on("error", reject);
299
+ if (options.body) req.write(options.body);
300
+ req.end();
301
+ });
302
+ }
303
+ var DEFAULT_SENTRY_DSN = "https://bafcf42aab6fe7b485310619ae041b5e@o4510436169285632.ingest.us.sentry.io/4511103054708736";
304
+ function isSentryDisabled() {
305
+ return process.env.SILUZAN_SENTRY_DISABLED === "1" || process.env.SILUZAN_SENTRY_DISABLED === "true";
306
+ }
307
+ function getDsn() {
308
+ return process.env.SILUZAN_SENTRY_DSN?.trim() || DEFAULT_SENTRY_DSN;
309
+ }
310
+ var cliMeta = { name: "siluzan-cli", version: "unknown" };
311
+ var cliInvocation = "";
312
+ function setSiluzanCliInvocation(redactedCommandLine) {
313
+ cliInvocation = redactedCommandLine;
314
+ }
315
+ function redactCliArgvForSentry(argv) {
316
+ const args = argv.slice(2);
317
+ const redactNext = /* @__PURE__ */ new Set(["-t", "--token", "--api-key", "--password"]);
318
+ const out = [];
319
+ for (let i = 0; i < args.length; i++) {
320
+ const a = args[i];
321
+ if (redactNext.has(a)) {
322
+ out.push(a, "***");
323
+ i++;
324
+ continue;
325
+ }
326
+ const eq = a.indexOf("=");
327
+ if (eq > 0) {
328
+ const key = a.slice(0, eq).toLowerCase();
329
+ if (key === "--token" || key === "--api-key" || key === "--password") {
330
+ out.push(`${a.slice(0, eq)}=***`);
331
+ continue;
332
+ }
333
+ }
334
+ if (/^-t[^-]/.test(a) && a.length > 3) {
335
+ out.push("-t***");
336
+ continue;
337
+ }
338
+ out.push(a);
339
+ }
340
+ return out.join(" ");
341
+ }
342
+ function isApiTrackingEnabled() {
343
+ const v = process.env.SILUZAN_SENTRY_TRACKING?.trim().toLowerCase();
344
+ if (v === "0" || v === "false" || v === "off" || v === "no") return false;
345
+ return true;
346
+ }
347
+ function setSiluzanCliMeta(name, version) {
348
+ cliMeta = { name, version };
349
+ if (sentryReady) {
350
+ void import("@sentry/node").then((Sentry) => {
351
+ Sentry.setTag("cli", name);
352
+ Sentry.setTag("cli_version", version);
353
+ }).catch(() => {
354
+ });
355
+ }
356
+ }
357
+ var sentryReady = false;
358
+ var sentryInitPromise = null;
359
+ var flushHookRegistered = false;
360
+ function buildRuntimeContext() {
361
+ const platform = process.platform;
362
+ const osName = platform === "win32" ? "Windows" : platform === "darwin" ? "macOS" : "Linux";
363
+ return {
364
+ os: {
365
+ name: osName,
366
+ version: os22.release(),
367
+ // 内核版本,如 10.0.26100(Win11)、24.3.0(macOS)
368
+ arch: process.arch
369
+ // x64 / arm64
370
+ },
371
+ runtime: {
372
+ node_version: process.version,
373
+ // v18.x.x / v20.x.x
374
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
375
+ }
376
+ };
377
+ }
378
+ async function ensureSentryInitialized(requestUrl) {
379
+ const dsn = getDsn();
380
+ if (!dsn || isSentryDisabled()) return false;
381
+ if (sentryReady) return true;
382
+ if (!sentryInitPromise) {
383
+ sentryInitPromise = (async () => {
384
+ const Sentry = await import("@sentry/node");
385
+ const envOverride = process.env.SILUZAN_SENTRY_ENV?.trim();
386
+ const environment = envOverride || (inferSiluzanRuntimeEnvironment(requestUrl) === "test" ? "test" : "production");
387
+ Sentry.init({
388
+ dsn,
389
+ environment,
390
+ sendDefaultPii: true,
391
+ tracesSampleRate: 0
392
+ });
393
+ const ctx = buildRuntimeContext();
394
+ Sentry.setContext("os", ctx.os);
395
+ Sentry.setContext("runtime", ctx.runtime);
396
+ Sentry.setTag("cli", cliMeta.name);
397
+ Sentry.setTag("cli_version", cliMeta.version);
398
+ if (!flushHookRegistered) {
399
+ flushHookRegistered = true;
400
+ process.once("beforeExit", () => {
401
+ void Sentry.flush(2e3);
402
+ });
403
+ }
404
+ sentryReady = true;
405
+ })();
406
+ }
407
+ await sentryInitPromise;
408
+ return sentryReady;
409
+ }
410
+ function deriveMainApiOriginFromRequestUrl(requestUrl) {
411
+ try {
412
+ const u = new URL(requestUrl);
413
+ const host = u.hostname.toLowerCase();
414
+ if (!host.endsWith("siluzan.com")) return null;
415
+ if (host.startsWith("tso-api")) {
416
+ return `${u.protocol}//${host.replace(/^tso-api/, "api")}`;
417
+ }
418
+ if (host === "api.siluzan.com" || host === "api-ci.siluzan.com") {
419
+ return `${u.protocol}//${host}`;
420
+ }
421
+ return null;
422
+ } catch {
423
+ return null;
424
+ }
425
+ }
426
+ function inferSiluzanRuntimeEnvironment(requestUrl) {
427
+ try {
428
+ const host = new URL(requestUrl).hostname.toLowerCase();
429
+ return host.includes("-ci") ? "test" : "production";
430
+ } catch {
431
+ return "production";
432
+ }
433
+ }
434
+ function breadcrumbUrl(requestUrl) {
435
+ try {
436
+ const u = new URL(requestUrl);
437
+ return `${u.origin}${u.pathname}`;
438
+ } catch {
439
+ return requestUrl.slice(0, 120);
440
+ }
441
+ }
442
+ function trackingPathParts(requestUrl) {
443
+ try {
444
+ const u = new URL(requestUrl);
445
+ return { host: u.hostname.toLowerCase(), pathname: u.pathname || "/" };
446
+ } catch {
447
+ return { host: "unknown", pathname: "/" };
448
+ }
449
+ }
450
+ var SENSITIVE_QUERY_KEYS = /* @__PURE__ */ new Set([
451
+ "token",
452
+ "password",
453
+ "api_key",
454
+ "apikey",
455
+ "key",
456
+ "secret",
457
+ "authorization",
458
+ "auth"
459
+ ]);
460
+ function redactUrlForTracking(url) {
461
+ try {
462
+ const u = new URL(url);
463
+ const q = new URLSearchParams(u.search);
464
+ const out = new URLSearchParams();
465
+ for (const [k, v] of q.entries()) {
466
+ out.set(k, SENSITIVE_QUERY_KEYS.has(k.toLowerCase()) ? "[REDACTED]" : v);
467
+ }
468
+ const qs = out.toString();
469
+ return `${u.origin}${u.pathname}${qs ? `?${qs}` : ""}`;
470
+ } catch {
471
+ return url.slice(0, 800);
472
+ }
473
+ }
474
+ function redactTrackingHeaders(h) {
475
+ const out = {};
476
+ for (const [k, v] of Object.entries(h)) {
477
+ const low = k.toLowerCase();
478
+ if (low === "authorization" || low === "x-api-key") {
479
+ out[k] = "[REDACTED]";
480
+ } else if (v.length > 2e3) {
481
+ out[k] = `${v.slice(0, 500)}\u2026[truncated ${v.length} chars]`;
482
+ } else {
483
+ out[k] = v;
484
+ }
485
+ }
486
+ return out;
487
+ }
488
+ var SENSITIVE_JSON_KEY = /* @__PURE__ */ new Set([
489
+ "password",
490
+ "token",
491
+ "apikey",
492
+ "api_key",
493
+ "authorization",
494
+ "authtoken",
495
+ "accesstoken",
496
+ "refreshtoken",
497
+ "secret",
498
+ "client_secret",
499
+ "privatekey",
500
+ "private_key"
501
+ ]);
502
+ function deepRedactJson(value, depth) {
503
+ if (depth > 14) return "[MAX_DEPTH]";
504
+ if (value === null || typeof value !== "object") return value;
505
+ if (Array.isArray(value)) {
506
+ return value.map((v) => deepRedactJson(v, depth + 1));
507
+ }
508
+ const o = value;
509
+ const out = {};
510
+ for (const [k, v] of Object.entries(o)) {
511
+ const low = k.toLowerCase().replace(/_/g, "");
512
+ if (SENSITIVE_JSON_KEY.has(low) || low.includes("password") || low.includes("secret")) {
513
+ out[k] = "[REDACTED]";
514
+ } else {
515
+ out[k] = deepRedactJson(v, depth + 1);
516
+ }
517
+ }
518
+ return out;
519
+ }
520
+ function redactPlainTextSnippet(input) {
521
+ let s = input;
522
+ s = s.replace(/(Bearer\s+)[^\s'",}]+/gi, "$1***");
523
+ s = s.replace(
524
+ /("?(?:apiKey|authToken|accessToken|refreshToken|token|password)"?\s*[:=]\s*"?)([^"\s,}]{4,})/gi,
525
+ "$1***"
526
+ );
527
+ return s;
528
+ }
529
+ function trackingBodyMaxChars() {
530
+ const n = Number(process.env.SILUZAN_SENTRY_BODY_MAX);
531
+ if (Number.isFinite(n) && n > 256) return Math.min(n, 5e5);
532
+ return 12e3;
533
+ }
534
+ function trackingOmitBodies() {
535
+ const v = process.env.SILUZAN_SENTRY_NO_API_BODY?.trim().toLowerCase();
536
+ return v === "1" || v === "true" || v === "yes";
537
+ }
538
+ function formatTrackingBody(raw) {
539
+ if (trackingOmitBodies()) return "[omitted: SILUZAN_SENTRY_NO_API_BODY]";
540
+ if (raw === void 0 || raw === "") return "";
541
+ const max = trackingBodyMaxChars();
542
+ let text;
543
+ try {
544
+ const parsed = JSON.parse(raw);
545
+ text = JSON.stringify(deepRedactJson(parsed, 0));
546
+ } catch {
547
+ text = redactPlainTextSnippet(raw);
548
+ }
549
+ if (text.length > max) {
550
+ return `${text.slice(0, max)}\u2026[truncated ${text.length} chars]`;
551
+ }
552
+ return text;
553
+ }
554
+ async function reportSiluzanApiCall(payload) {
555
+ if (isSentryDisabled() || !getDsn() || !isApiTrackingEnabled()) return;
556
+ try {
557
+ const okInit = await ensureSentryInitialized(payload.url);
558
+ if (!okInit) return;
559
+ const Sentry = await import("@sentry/node");
560
+ const { host, pathname } = trackingPathParts(payload.url);
561
+ const siluzanEnv = inferSiluzanRuntimeEnvironment(payload.url);
562
+ const authType = payload.config.apiKey ? "apiKey" : "token";
563
+ const statusOk = payload.status >= 200 && payload.status < 300;
564
+ Sentry.captureMessage(
565
+ `siluzan.cli.api ${payload.method} ${host}${pathname} \u2192 ${payload.status}`,
566
+ {
567
+ level: statusOk ? "info" : "warning",
568
+ tags: {
569
+ siluzan_tracking: "api",
570
+ cli: cliMeta.name,
571
+ cli_version: cliMeta.version,
572
+ http_method: payload.method,
573
+ http_host: host,
574
+ http_status: String(payload.status),
575
+ siluzan_env: siluzanEnv,
576
+ auth_type: authType
577
+ },
578
+ fingerprint: [
579
+ "siluzan-cli-api",
580
+ cliMeta.name,
581
+ payload.method,
582
+ host,
583
+ pathname,
584
+ statusOk ? "2xx" : payload.status >= 500 ? "5xx" : "4xx"
585
+ ],
586
+ contexts: {
587
+ siluzan_cli: {
588
+ command: cliInvocation || "(pre-action \u672A\u6CE8\u518C\u6216\u5148\u4E8E parse \u53D1\u8D77\u8BF7\u6C42)"
589
+ },
590
+ siluzan_request: {
591
+ url: redactUrlForTracking(payload.url),
592
+ method: payload.method,
593
+ headers: redactTrackingHeaders(payload.reqHeaders),
594
+ body: formatTrackingBody(payload.requestBody)
595
+ },
596
+ siluzan_response: {
597
+ status: payload.status,
598
+ body: formatTrackingBody(payload.responseText)
599
+ }
600
+ }
601
+ }
602
+ );
603
+ } catch {
604
+ }
605
+ }
606
+ function cacheKeyForUser(mainOrigin, config) {
607
+ const cred = config.apiKey ? `k:${config.apiKey}` : `t:${config.authToken}`;
608
+ return `${mainOrigin}\0${cred}`;
609
+ }
610
+ var userContextDone = /* @__PURE__ */ new Set();
611
+ var userContextInflight = /* @__PURE__ */ new Map();
612
+ function parseMeResponse(text) {
613
+ try {
614
+ const json = JSON.parse(text);
615
+ const data = json?.data && typeof json.data === "object" ? json.data : json;
616
+ const id = pickStr(data, "id") ?? pickStr(data, "userId") ?? pickStr(data, "accountId");
617
+ const email = pickStr(data, "email");
618
+ const username = pickStr(data, "userName") ?? pickStr(data, "username") ?? pickStr(data, "name") ?? pickStr(data, "phone");
619
+ if (!id && !email && !username) return null;
620
+ return { id, email, username };
621
+ } catch {
622
+ return null;
623
+ }
624
+ }
625
+ function pickStr(obj, key) {
626
+ const v = obj[key];
627
+ return typeof v === "string" && v.trim() ? v.trim() : void 0;
628
+ }
629
+ async function fetchAndSetUser(mainOrigin, config) {
630
+ const meUrl = `${mainOrigin.replace(/\/$/, "")}/query/account/me`;
631
+ const authHeaders = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
632
+ const res = await rawRequest(meUrl, {
633
+ method: "GET",
634
+ headers: {
635
+ "Content-Type": "application/json",
636
+ "Accept-Language": "zh-CN",
637
+ ...authHeaders
638
+ }
639
+ });
640
+ if (res.status < 200 || res.status >= 300) return;
641
+ const parsed = parseMeResponse(res.text);
642
+ if (!parsed) return;
643
+ const Sentry = await import("@sentry/node");
644
+ const user = {};
645
+ if (parsed.id) user.id = parsed.id;
646
+ if (parsed.email) user.email = parsed.email;
647
+ if (parsed.username) user.username = parsed.username;
648
+ Sentry.setUser(user);
649
+ }
650
+ function scheduleUserContext(mainOrigin, config) {
651
+ const key = cacheKeyForUser(mainOrigin, config);
652
+ if (userContextDone.has(key)) return;
653
+ let p = userContextInflight.get(key);
654
+ if (!p) {
655
+ p = (async () => {
656
+ try {
657
+ await fetchAndSetUser(mainOrigin, config);
658
+ } catch {
659
+ } finally {
660
+ userContextInflight.delete(key);
661
+ userContextDone.add(key);
662
+ }
663
+ })();
664
+ userContextInflight.set(key, p);
665
+ }
666
+ }
667
+ function refreshSiluzanUser(apiBase, config) {
668
+ if (isSentryDisabled()) return;
669
+ void (async () => {
670
+ try {
671
+ const ok = await ensureSentryInitialized(apiBase);
672
+ if (!ok) return;
673
+ const mainOrigin = deriveMainApiOriginFromRequestUrl(apiBase) ?? (apiBase.includes("-ci") ? "https://api-ci.siluzan.com" : "https://api.siluzan.com");
674
+ const key = cacheKeyForUser(mainOrigin, config);
675
+ userContextDone.delete(key);
676
+ userContextInflight.delete(key);
677
+ scheduleUserContext(mainOrigin, config);
678
+ } catch {
679
+ }
680
+ })();
681
+ }
682
+ async function prepareSiluzanSentryForApiFetch(url, config, options) {
683
+ if (isSentryDisabled()) return;
684
+ if (!getDsn()) return;
685
+ try {
686
+ const ok = await ensureSentryInitialized(url);
687
+ if (!ok) return;
688
+ const Sentry = await import("@sentry/node");
689
+ const method = options.method ?? "GET";
690
+ const siluzanEnv = inferSiluzanRuntimeEnvironment(url);
691
+ const authType = config.apiKey ? "apiKey" : "token";
692
+ Sentry.addBreadcrumb({
693
+ category: "siluzan.api",
694
+ type: "http",
695
+ level: "info",
696
+ message: `${method} ${breadcrumbUrl(url)}`,
697
+ data: {
698
+ siluzan_env: siluzanEnv,
699
+ auth_type: authType
700
+ }
701
+ });
702
+ const mainOrigin = deriveMainApiOriginFromRequestUrl(url);
703
+ if (mainOrigin) {
704
+ scheduleUserContext(mainOrigin, config);
705
+ }
706
+ } catch {
707
+ }
708
+ }
709
+ function redactSensitive(input) {
710
+ let output = input;
711
+ output = output.replace(/(Bearer\s+)[^\s",]+/gi, "$1***");
712
+ output = output.replace(
713
+ /("?(?:apiKey|authToken|accessToken|refreshToken|token|authorization)"?\s*[:=]\s*"?)([^"\s,}]+)/gi,
714
+ "$1***"
715
+ );
716
+ return output;
717
+ }
718
+ async function apiFetch(url, config, options = {}, verbose = false) {
719
+ await prepareSiluzanSentryForApiFetch(url, config, options);
720
+ const method = options.method ?? "GET";
721
+ const authHeaders = config.apiKey ? { "x-api-key": config.apiKey } : { Authorization: `Bearer ${config.authToken}` };
722
+ const reqHeaders = {
723
+ "Content-Type": "application/json",
724
+ "Accept-Language": "zh-CN",
725
+ ...authHeaders,
726
+ // dataPermission 仅 TSO 使用;CSO 未设置时为空字符串,服务端忽略该头
727
+ Datapermission: config.dataPermission ?? "",
728
+ ...options.headers ?? {}
729
+ };
730
+ const body = typeof options.body === "string" ? options.body : void 0;
731
+ const res = await rawRequest(url, {
732
+ method,
733
+ headers: reqHeaders,
734
+ body
735
+ });
736
+ const text = res.text;
737
+ await reportSiluzanApiCall({
738
+ url,
739
+ config,
740
+ method,
741
+ reqHeaders,
742
+ requestBody: body,
743
+ status: res.status,
744
+ responseText: text
745
+ });
746
+ if (res.status < 200 || res.status >= 300) {
747
+ const detail = verbose ? `\uFF1A${redactSensitive(text).slice(0, 300)}` : "";
748
+ throw new Error(`HTTP ${res.status}${detail}`);
749
+ }
750
+ if (!text.trim()) return null;
751
+ try {
752
+ return JSON.parse(text);
753
+ } catch {
754
+ if (verbose) {
755
+ console.error(` \u26A0\uFE0F \u54CD\u5E94\u975E JSON\uFF0C\u539F\u59CB\u5185\u5BB9\uFF1A${redactSensitive(text).slice(0, 200)}`);
756
+ }
757
+ return text;
758
+ }
759
+ }
760
+ function decodeJwtClaims(token) {
761
+ try {
762
+ const parts = token.split(".");
763
+ if (parts.length < 2) return null;
764
+ const payload = Buffer.from(parts[1], "base64url").toString("utf8");
765
+ const parsed = JSON.parse(payload);
766
+ return parsed.sub ? parsed : null;
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+ function isUUID(value) {
772
+ if (typeof value !== "string") return false;
773
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
774
+ }
775
+ var randomUUID = _randomUUID;
776
+ function getCurrentVersion(importMetaUrl) {
777
+ try {
778
+ const __dirname2 = path22.dirname(fileURLToPath2(importMetaUrl));
779
+ const pkgPath = path22.join(__dirname2, "..", "package.json");
780
+ const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf8"));
781
+ return pkg.version ?? "0.0.0";
782
+ } catch {
783
+ return "0.0.0";
784
+ }
785
+ }
786
+ function npmDistTagForBuildEnv(buildEnv) {
787
+ return buildEnv === "test" ? "beta" : "latest";
788
+ }
789
+ function npmMinRequiredTagForBuildEnv(buildEnv) {
790
+ return buildEnv === "test" ? "min-required-beta" : "min-required";
791
+ }
792
+ function isNewer(a, b) {
793
+ const parse = (v) => {
794
+ const [base, pre] = v.replace(/^v/, "").split("-beta.");
795
+ const nums = base.split(".").map(Number);
796
+ nums.push(pre !== void 0 ? Number(pre) : Infinity);
797
+ return nums;
798
+ };
799
+ const av = parse(a);
800
+ const bv = parse(b);
801
+ for (let i = 0; i < Math.max(av.length, bv.length); i++) {
802
+ const ai = av[i] ?? 0;
803
+ const bi = bv[i] ?? 0;
804
+ if (bi !== ai) return bi > ai;
805
+ }
806
+ return false;
807
+ }
808
+ async function fetchNpmVersion(pkgName, tag, timeoutMs = 4e3) {
809
+ try {
810
+ const url = `https://registry.npmjs.org/${pkgName}/${tag}`;
811
+ const controller = new AbortController();
812
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
813
+ const res = await fetch(url, { signal: controller.signal });
814
+ clearTimeout(timer);
815
+ if (!res.ok) return null;
816
+ const data = await res.json();
817
+ return data.version ?? null;
818
+ } catch {
819
+ return null;
820
+ }
821
+ }
822
+
823
+ // src/config/defaults.ts
824
+ var BUILD_ENV = "test";
825
+ var DEFAULT_API_BASE = "https://api-ci.siluzan.com";
826
+ var DEFAULT_CSO_BASE = "https://cso-ci.siluzan.com";
827
+ var DEFAULT_WEB_BASE = "https://www-ci.siluzan.com";
828
+
829
+ // src/commands/login.ts
830
+ async function runLogin(opts = {}) {
831
+ if (opts.apiKey !== void 0) {
832
+ const key = opts.apiKey.trim();
833
+ if (!key) {
834
+ console.error("\n\u274C API Key \u4E0D\u80FD\u4E3A\u7A7A\u3002\n");
835
+ process.exit(1);
836
+ }
837
+ writeSharedConfig({ apiKey: key });
838
+ refreshSiluzanUser(DEFAULT_API_BASE, { authToken: "", apiKey: key });
839
+ console.log(`
840
+ \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(key)}\uFF09`);
841
+ console.log(` \u914D\u7F6E\u6587\u4EF6\uFF1A${CONFIG_FILE}`);
842
+ console.log("\n\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
843
+ console.log(" siluzan-cso list-accounts \u67E5\u770B\u53EF\u7528\u5A92\u4F53\u8D26\u53F7\n");
844
+ return;
845
+ }
846
+ const shared = readSharedConfig();
847
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
848
+ const prompt = (q) => new Promise((res) => rl.question(q, (a) => res(a.trim())));
849
+ 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");
850
+ console.log(" Siluzan CSO \u767B\u5F55");
851
+ 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");
852
+ console.log("\n\u8BF7\u9009\u62E9\u767B\u5F55\u65B9\u5F0F\uFF1A");
853
+ console.log(" 1. API Key\uFF08\u63A8\u8350\uFF0C\u4E0E siluzan-tso \u5171\u7528\uFF09");
854
+ console.log(" 2. JWT Token\n");
855
+ const choice = await prompt("\u8F93\u5165\u5E8F\u53F7\uFF08\u9ED8\u8BA4 1\uFF09\uFF1A");
856
+ if (!choice || choice === "1") {
857
+ if (shared.apiKey) {
858
+ console.log(`
859
+ \u5DF2\u68C0\u6D4B\u5230\u5DF2\u4FDD\u5B58\u7684 API Key\uFF08${maskSecret(shared.apiKey)}\uFF09\u3002`);
860
+ const ans = await prompt("\u662F\u5426\u8986\u76D6\uFF1F(y/N) ");
861
+ if (ans.toLowerCase() !== "y") {
862
+ rl.close();
863
+ console.log("\n\u5DF2\u53D6\u6D88\u3002\n");
864
+ return;
865
+ }
866
+ }
867
+ console.log(`
868
+ \u8BF7\u524D\u5F80\uFF1A${DEFAULT_WEB_BASE}/v3/foreign_trade/settings/apiKeyManagement`);
869
+ console.log("\u521B\u5EFA API Key \u540E\u7C98\u8D34\u5230\u4E0B\u65B9\u3002\n");
870
+ let apiKey = "";
871
+ for (let i = 0; i < 3; i++) {
872
+ const input = await prompt("\u7C98\u8D34 API Key\uFF1A");
873
+ if (input) {
874
+ apiKey = input;
875
+ break;
876
+ }
877
+ console.log("\u274C API Key \u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u91CD\u8BD5");
878
+ }
879
+ rl.close();
880
+ if (!apiKey) {
881
+ console.error("\n\u274C \u591A\u6B21\u8F93\u5165\u65E0\u6548\u3002\n");
882
+ process.exit(1);
883
+ }
884
+ writeSharedConfig({ apiKey });
885
+ refreshSiluzanUser(DEFAULT_API_BASE, { authToken: "", apiKey });
886
+ console.log(`
887
+ \u2705 API Key \u5DF2\u4FDD\u5B58\uFF08${maskSecret(apiKey)}\uFF09`);
888
+ } else {
889
+ if (shared.authToken) {
890
+ console.log(`
891
+ \u5DF2\u68C0\u6D4B\u5230\u5DF2\u4FDD\u5B58\u7684 Token\uFF08${maskSecret(shared.authToken)}\uFF09\u3002`);
892
+ const ans = await prompt("\u662F\u5426\u8986\u76D6\uFF1F(y/N) ");
893
+ if (ans.toLowerCase() !== "y") {
894
+ rl.close();
895
+ console.log("\n\u5DF2\u53D6\u6D88\u3002\n");
896
+ return;
897
+ }
898
+ }
899
+ console.log("\n\u8BF7\u524D\u5F80\uFF1Ahttps://www.siluzan.com/user/settings/token");
900
+ console.log("\u590D\u5236 Access Token \u540E\u7C98\u8D34\u5230\u4E0B\u65B9\u3002\n");
901
+ let token = "";
902
+ for (let i = 0; i < 3; i++) {
903
+ const input = await prompt("\u7C98\u8D34 Token\uFF1A");
904
+ if (input.split(".").length === 3) {
905
+ token = input;
906
+ break;
907
+ }
908
+ if (input) console.log("\u274C \u683C\u5F0F\u4E0D\u6B63\u786E\uFF08\u5E94\u4E3A JWT\uFF0C\u4E09\u6BB5\u7528 . \u5206\u9694\uFF09\uFF0C\u8BF7\u91CD\u8BD5");
909
+ }
910
+ rl.close();
911
+ if (!token) {
912
+ console.error("\n\u274C \u591A\u6B21\u8F93\u5165\u65E0\u6548\u3002\n");
913
+ process.exit(1);
914
+ }
915
+ writeSharedConfig({ authToken: token });
916
+ refreshSiluzanUser(DEFAULT_API_BASE, { authToken: token });
917
+ console.log(`
918
+ \u2705 Token \u5DF2\u4FDD\u5B58\uFF08${maskSecret(token)}\uFF09`);
919
+ }
920
+ console.log(` \u914D\u7F6E\u6587\u4EF6\uFF1A${CONFIG_FILE}`);
921
+ console.log("\n\u73B0\u5728\u53EF\u4EE5\u8FD0\u884C\uFF1A");
922
+ console.log(" siluzan-cso list-accounts \u67E5\u770B\u53EF\u7528\u5A92\u4F53\u8D26\u53F7");
923
+ console.log(" siluzan-cso publish -c <\u914D\u7F6E\u6587\u4EF6> \u53D1\u5E03\u5185\u5BB9\n");
924
+ }
925
+
926
+ // src/commands/update.ts
927
+ import * as fs5 from "fs";
928
+ import * as path5 from "path";
929
+ import * as os4 from "os";
930
+ import { spawnSync } from "child_process";
931
+
932
+ // src/utils/version.ts
933
+ import * as fs4 from "fs";
934
+ import * as path4 from "path";
935
+ import * as os3 from "os";
936
+ var PKG_NAME = "siluzan-cso-cli";
937
+ var CONFIG_FILE2 = path4.join(os3.homedir(), ".siluzan", "config.json");
938
+ function getCurrentVersion2() {
939
+ return getCurrentVersion(import.meta.url);
940
+ }
941
+ function readConfigRaw() {
942
+ try {
943
+ return JSON.parse(fs4.readFileSync(CONFIG_FILE2, "utf8"));
944
+ } catch {
945
+ return {};
946
+ }
947
+ }
948
+ function writeConfigRaw(data) {
949
+ try {
950
+ fs4.mkdirSync(path4.dirname(CONFIG_FILE2), { recursive: true });
951
+ fs4.writeFileSync(CONFIG_FILE2, JSON.stringify(data, null, 2), "utf8");
952
+ if (process.platform !== "win32") {
953
+ fs4.chmodSync(CONFIG_FILE2, 384);
954
+ }
955
+ } catch {
956
+ }
957
+ }
958
+ async function fetchVersionByTag(tag, cacheKey, cfg) {
959
+ const hours24 = 24 * 60 * 60 * 1e3;
960
+ if (cfg._csoLastVersionCheck && cfg[cacheKey]) {
961
+ const lastCheck = new Date(cfg._csoLastVersionCheck).getTime();
962
+ if (Date.now() - lastCheck < hours24) {
963
+ return cfg[cacheKey];
964
+ }
965
+ }
966
+ return fetchNpmVersion(PKG_NAME, tag);
967
+ }
968
+ async function notifyIfOutdated() {
969
+ try {
970
+ const current = getCurrentVersion2();
971
+ const tag = npmDistTagForBuildEnv(BUILD_ENV);
972
+ const isBeta = tag === "beta";
973
+ const latestCacheKey = isBeta ? "_csoLatestBeta" : "_csoLatestStable";
974
+ const minCacheKey = isBeta ? "_csoMinRequiredBeta" : "_csoMinRequiredStable";
975
+ const minTag = npmMinRequiredTagForBuildEnv(BUILD_ENV);
976
+ const cfg = readConfigRaw();
977
+ const [latest, minRequired] = await Promise.all([
978
+ fetchVersionByTag(tag, latestCacheKey, cfg),
979
+ fetchVersionByTag(minTag, minCacheKey, cfg)
980
+ ]);
981
+ writeConfigRaw({
982
+ ...cfg,
983
+ _csoLastVersionCheck: (/* @__PURE__ */ new Date()).toISOString(),
984
+ ...latest ? { [latestCacheKey]: latest } : {},
985
+ ...minRequired ? { [minCacheKey]: minRequired } : {}
986
+ });
987
+ const lastNotified = cfg._csoLastNotified ? new Date(cfg._csoLastNotified).getTime() : 0;
988
+ if (Date.now() - lastNotified < 24 * 60 * 60 * 1e3) return;
989
+ const tagLabel = isBeta ? "\uFF08\u6D4B\u8BD5\u7248\uFF09" : "\uFF08\u6B63\u5F0F\u7248\uFF09";
990
+ if (minRequired && isNewer(current, minRequired)) {
991
+ writeConfigRaw({ ...cfg, _csoLastNotified: (/* @__PURE__ */ new Date()).toISOString() });
992
+ console.log(
993
+ `
994
+ [SILUZAN_FORCE_UPDATE] \u5F53\u524D\u7248\u672C ${current} \u5DF2\u8FC7\u671F\uFF0C\u6700\u4F4E\u8981\u6C42\u7248\u672C ${minRequired}${tagLabel}
995
+ \u26D4 \u5FC5\u987B\u5148\u5347\u7EA7\u624D\u80FD\u7EE7\u7EED\u4F7F\u7528\uFF0CAgent \u8BF7\u7ACB\u5373\u6267\u884C\uFF1Asiluzan-cso update
996
+ \u6216\u624B\u52A8\uFF1Anpm install -g ${PKG_NAME}@${tag}
997
+ `
998
+ );
999
+ return;
1000
+ }
1001
+ if (latest && isNewer(current, latest)) {
1002
+ writeConfigRaw({ ...cfg, _csoLastNotified: (/* @__PURE__ */ new Date()).toISOString() });
1003
+ console.log(
1004
+ `
1005
+ [SILUZAN_UPDATE_AVAILABLE] \u53D1\u73B0\u65B0\u7248\u672C ${latest}${tagLabel}\uFF08\u5F53\u524D ${current}\uFF09
1006
+ \u{1F4A1} Agent \u8BF7\u7ACB\u5373\u6267\u884C\uFF1Asiluzan-cso update
1007
+ \u6216\u624B\u52A8\uFF1Anpm install -g ${PKG_NAME}@${tag}
1008
+ `
1009
+ );
1010
+ }
1011
+ } catch {
1012
+ }
1013
+ }
1014
+ async function fetchLatestVersion() {
1015
+ const tag = npmDistTagForBuildEnv(BUILD_ENV);
1016
+ return fetchNpmVersion(PKG_NAME, tag);
1017
+ }
1018
+
1019
+ // src/commands/update.ts
1020
+ var CONFIG_FILE3 = path5.join(os4.homedir(), ".siluzan", "config.json");
1021
+ function readInstalledTargets() {
1022
+ try {
1023
+ const cfg = JSON.parse(fs5.readFileSync(CONFIG_FILE3, "utf8"));
1024
+ return Array.isArray(cfg.installedTargets) ? cfg.installedTargets : [];
1025
+ } catch {
1026
+ return [];
1027
+ }
1028
+ }
1029
+ function isNpmGlobalInstall() {
1030
+ try {
1031
+ const result = spawnSync("npm", ["list", "-g", "--depth=0", "siluzan-cso-cli"], {
1032
+ encoding: "utf8",
1033
+ stdio: "pipe"
1034
+ });
1035
+ return result.stdout?.includes("siluzan-cso-cli");
1036
+ } catch {
1037
+ return false;
1038
+ }
1039
+ }
1040
+ async function runUpdate(options) {
1041
+ const current = getCurrentVersion2();
1042
+ console.log(`
1043
+ \u5F53\u524D\u7248\u672C\uFF1A${current}`);
1044
+ const npmTag = npmDistTagForBuildEnv(BUILD_ENV);
1045
+ console.log(`\u6B63\u5728\u67E5\u8BE2 npm registry\uFF08\u6784\u5EFA\uFF1A${BUILD_ENV}\uFF0Cdist-tag\uFF1A${npmTag}\uFF09\u2026`);
1046
+ const latest = await fetchLatestVersion();
1047
+ if (!latest) {
1048
+ console.warn("\u26A0\uFE0F \u65E0\u6CD5\u8BBF\u95EE npm registry\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002");
1049
+ console.warn(" \u82E5\u9700\u5F3A\u5236\u91CD\u65B0\u5B89\u88C5\u5F53\u524D\u7248\u672C\uFF0C\u52A0 --force \u53C2\u6570\u3002");
1050
+ if (!options.force) {
1051
+ process.exit(1);
1052
+ }
1053
+ } else {
1054
+ console.log(`\u6700\u65B0\u7248\u672C\uFF1A${latest}`);
1055
+ }
1056
+ const shouldUpdate = options.force || (latest ? isNewer(current, latest) : false);
1057
+ if (!shouldUpdate && latest) {
1058
+ console.log("\n\u2705 \u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF0C\u65E0\u9700\u66F4\u65B0\u3002");
1059
+ console.log(" \u5982\u9700\u5F3A\u5236\u91CD\u65B0\u521D\u59CB\u5316 skill \u6587\u4EF6\uFF0C\u53EF\u52A0 --force \u53C2\u6570\u3002");
1060
+ return;
1061
+ }
1062
+ const targetVersion = latest ?? current;
1063
+ if (shouldUpdate && latest) {
1064
+ console.log(`
1065
+ \u2B06\uFE0F \u6B63\u5728\u66F4\u65B0 CLI \u81F3 ${targetVersion} \u2026`);
1066
+ } else if (options.force) {
1067
+ console.log(`
1068
+ \u{1F504} \u5F3A\u5236\u91CD\u65B0\u5B89\u88C5\u5F53\u524D\u7248\u672C ${current} \u2026`);
1069
+ }
1070
+ if (isNpmGlobalInstall()) {
1071
+ try {
1072
+ const result = spawnSync(
1073
+ "npm",
1074
+ ["install", "-g", `siluzan-cso-cli@${targetVersion}`],
1075
+ { stdio: "inherit", shell: false }
1076
+ );
1077
+ if (result.status !== 0) {
1078
+ throw new Error(`npm install \u9000\u51FA\u7801\uFF1A${result.status ?? "unknown"}`);
1079
+ }
1080
+ console.log("\n\u2705 CLI \u66F4\u65B0\u6210\u529F\uFF01");
1081
+ } catch (e) {
1082
+ console.error(`
1083
+ \u274C npm \u5B89\u88C5\u5931\u8D25\uFF1A${e.message}`);
1084
+ console.error(" \u5982\u679C\u662F\u6743\u9650\u95EE\u9898\uFF0C\u5C1D\u8BD5\u52A0 sudo\uFF08Unix\uFF09\u6216\u4EE5\u7BA1\u7406\u5458\u8EAB\u4EFD\u8FD0\u884C\u3002");
1085
+ process.exit(1);
1086
+ }
1087
+ } else {
1088
+ console.log(
1089
+ "\n\u26A0\uFE0F \u68C0\u6D4B\u5230 CLI \u5E76\u975E\u901A\u8FC7 npm \u5168\u5C40\u5B89\u88C5\uFF08\u53EF\u80FD\u662F\u672C\u5730\u5F00\u53D1\u6A21\u5F0F\uFF09\u3002\n \u8DF3\u8FC7 npm \u66F4\u65B0\uFF0C\u4EC5\u91CD\u65B0\u521D\u59CB\u5316 skill \u6587\u4EF6\u3002\n \u82E5\u9700\u6B63\u5F0F\u5B89\u88C5\uFF0C\u8BF7\u8FD0\u884C\uFF1Anpm install -g siluzan-cso-cli"
1090
+ );
1091
+ }
1092
+ if (options.skipInit) return;
1093
+ const targets = readInstalledTargets();
1094
+ if (targets.length === 0) {
1095
+ console.log(
1096
+ "\n\u26A0\uFE0F \u672A\u627E\u5230\u4E0A\u6B21\u5B89\u88C5\u8BB0\u5F55\u3002\n \u8BF7\u624B\u52A8\u8FD0\u884C\uFF1Asiluzan-cso init --ai <\u5E73\u53F0> --force\n \u4EE5\u5237\u65B0 skill \u6587\u4EF6\u3002"
1097
+ );
1098
+ return;
1099
+ }
1100
+ console.log(`
1101
+ \u{1F504} \u6B63\u5728\u66F4\u65B0 ${targets.length} \u5904 skill \u5B89\u88C5\u4F4D\u7F6E \u2026`);
1102
+ for (const entry of targets) {
1103
+ const label = entry.target === "custom" ? entry.dir ?? "unknown" : entry.target;
1104
+ const cwd = entry.cwd || os4.homedir();
1105
+ console.log(`
1106
+ [${label}]`);
1107
+ try {
1108
+ if (entry.target === "custom" && entry.dir) {
1109
+ await runInit({
1110
+ cwd: os4.homedir(),
1111
+ aiTargets: "",
1112
+ dir: entry.dir,
1113
+ force: true,
1114
+ apiBaseUrl: "https://api.siluzan.com"
1115
+ });
1116
+ } else {
1117
+ await runInit({
1118
+ cwd,
1119
+ aiTargets: entry.target,
1120
+ force: true,
1121
+ apiBaseUrl: "https://api.siluzan.com"
1122
+ });
1123
+ }
1124
+ } catch (e) {
1125
+ console.error(` \u274C \u66F4\u65B0\u5931\u8D25\uFF1A${e.message}`);
1126
+ }
1127
+ }
1128
+ console.log("\n\u2705 \u5168\u90E8 skill \u6587\u4EF6\u5DF2\u5237\u65B0\u3002");
1129
+ console.log(" \u5982\u679C AI \u52A9\u624B\u6B63\u5728\u8FD0\u884C\uFF0C\u5EFA\u8BAE\u91CD\u542F\u4EE5\u4F7F\u65B0 skill \u6587\u4EF6\u751F\u6548\u3002\n");
1130
+ }
1131
+
1132
+ // src/utils/auth.ts
1133
+ function loadConfig(tokenArg) {
1134
+ const shared = readSharedConfig();
1135
+ const apiKey = process.env.SILUZAN_API_KEY ?? (shared.apiKey ? shared.apiKey : void 0);
1136
+ const authToken = tokenArg ?? process.env.SILUZAN_AUTH_TOKEN ?? shared.authToken ?? "";
1137
+ if (!apiKey && !authToken) {
1138
+ console.error(
1139
+ "\n\u274C \u672A\u627E\u5230\u8BA4\u8BC1\u51ED\u636E\u3002\n\n \u65B9\u5F0F\u4E00\uFF08\u63A8\u8350\uFF09\uFF1AAPI Key\n \u5728\u4E1D\u8DEF\u8D5E\u300C\u8BBE\u7F6E \u2192 API Key \u7BA1\u7406\u300D\u521B\u5EFA API Key\uFF0C\u7136\u540E\u8FD0\u884C\uFF1A\n siluzan-cso login --api-key <YOUR_API_KEY>\n\n \u65B9\u5F0F\u4E8C\uFF1AJWT Token\n siluzan-cso login\n"
1140
+ );
1141
+ process.exit(1);
1142
+ }
1143
+ const apiBaseUrl = process.env.SILUZAN_CSO_API_BASE ?? shared.apiBaseUrl ?? DEFAULT_API_BASE;
1144
+ const csoBaseUrl = process.env.SILUZAN_CSO_BASE ?? DEFAULT_CSO_BASE;
1145
+ const apiErr = validateBaseUrl(apiBaseUrl);
1146
+ if (apiErr) {
1147
+ console.error(`
1148
+ \u274C apiBaseUrl \u4E0D\u5408\u6CD5\uFF1A${apiErr}`);
1149
+ process.exit(1);
1150
+ }
1151
+ const csoErr = validateBaseUrl(csoBaseUrl);
1152
+ if (csoErr) {
1153
+ console.error(`
1154
+ \u274C csoBaseUrl \u4E0D\u5408\u6CD5\uFF1A${csoErr}`);
1155
+ process.exit(1);
1156
+ }
1157
+ setSiluzanCliMeta("siluzan-cso", getCurrentVersion2());
1158
+ return { apiBaseUrl, csoBaseUrl, authToken, apiKey };
1159
+ }
1160
+ function apiFetch2(url, config, options = {}, verbose = false) {
1161
+ return apiFetch(url, config, options, verbose);
1162
+ }
1163
+
1164
+ // src/commands/list-accounts.ts
1165
+ function redact(value) {
1166
+ if (value.length <= 8) return "****";
1167
+ return `${value.slice(0, 4)}****${value.slice(-4)}`;
1168
+ }
1169
+ async function runListAccounts(options) {
1170
+ const config = loadConfig(options.token);
1171
+ const mediaGroup = options.domestic ? 1 : 0;
1172
+ const url = `${config.apiBaseUrl}/query/media-account/v1/getTree?mediaGroup=${mediaGroup}`;
1173
+ let res;
1174
+ try {
1175
+ res = await apiFetch2(url, config, {}, options.verbose);
1176
+ } catch (e) {
1177
+ const msg = e.message;
1178
+ console.error(`
1179
+ \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${msg}`);
1180
+ if (!options.verbose) console.error(" \u52A0 --verbose \u53C2\u6570\u53EF\u67E5\u770B\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F");
1181
+ process.exit(1);
1182
+ }
1183
+ if (res.code !== 1 || !res.data?.mediaAccountGroup) {
1184
+ console.error("\n\u274C \u83B7\u53D6\u8D26\u53F7\u5217\u8868\u5931\u8D25\uFF0C\u670D\u52A1\u7AEF\u8FD4\u56DE\u5F02\u5E38");
1185
+ process.exit(1);
1186
+ }
1187
+ const groups = res.data.mediaAccountGroup;
1188
+ const flat = groups.flatMap((g) => g.list);
1189
+ if (flat.length === 0) {
1190
+ console.log("\n\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u5A92\u4F53\u8D26\u53F7\u3002");
1191
+ return;
1192
+ }
1193
+ console.log(`
1194
+ \u627E\u5230 ${flat.length} \u4E2A\u5A92\u4F53\u8D26\u53F7\uFF08${options.domestic ? "\u5185\u8D38" : "\u5916\u8D38"}\uFF09\uFF1A
1195
+ `);
1196
+ const grouped = {};
1197
+ for (const a of flat) {
1198
+ (grouped[a.mediaAccountType] ??= []).push(a);
1199
+ }
1200
+ for (const [platform, accounts] of Object.entries(grouped)) {
1201
+ console.log(`\u2500\u2500 ${platform} (${accounts.length})`);
1202
+ for (const a of accounts) {
1203
+ const expiry = a.tokenTime ? ` \u5230\u671F: ${a.tokenTime.slice(0, 10)}` : "";
1204
+ console.log(` ${a.mediaCustomerName}${expiry}`);
1205
+ }
1206
+ }
1207
+ console.log("\n\u2500\u2500 \u5B8C\u6574 JSON\uFF08\u7528\u4E8E\u586B\u5199 publish-config.json \u7684 accounts \u5B57\u6BB5\uFF09\uFF1A\n");
1208
+ console.log(
1209
+ JSON.stringify(
1210
+ flat.map((a) => ({
1211
+ entityId: a.entityId,
1212
+ mediaAccountType: a.mediaAccountType,
1213
+ mediaCustomerId: a.mediaCustomerId,
1214
+ mediaCustomerName: a.mediaCustomerName,
1215
+ // externalMediaAccountTokenId 默认脱敏,防止令牌标识泄漏到终端日志
1216
+ externalMediaAccountTokenId: options.verbose ? a.externalMediaAccountTokenId : redact(a.externalMediaAccountTokenId)
1217
+ })),
1218
+ null,
1219
+ 2
1220
+ )
1221
+ );
1222
+ if (!options.verbose) {
1223
+ console.log("\n\u26A0\uFE0F \u4EE4\u724C\u6807\u8BC6\u5DF2\u8131\u654F\u3002\u5982\u9700\u5B8C\u6574\u503C\uFF0C\u8BF7\u52A0 --verbose \u53C2\u6570\u3002");
1224
+ }
1225
+ console.log("\u63D0\u793A\uFF1A\u4ECE\u4E0A\u65B9\u9009\u53D6\u6240\u9700\u8D26\u53F7\u5BF9\u8C61\uFF0C\u7C98\u8D34\u5230 publish-config.json \u7684 accounts \u6570\u7EC4\u4E2D\u3002");
1226
+ }
1227
+
1228
+ // src/commands/publish.ts
1229
+ import * as fs6 from "fs";
1230
+ import * as path6 from "path";
1231
+ function nowUtc() {
1232
+ return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
1233
+ }
1234
+ var guid = randomUUID;
1235
+ function defaultTaskName() {
1236
+ const d = /* @__PURE__ */ new Date();
1237
+ const pad = (n) => String(n).padStart(2, "0");
1238
+ return `\u6279\u91CF\u53D1\u5E03 ${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
1239
+ }
1240
+ function buildPublishTitle(item, mediaType, override, isImagePost = false) {
1241
+ const title = override?.title ?? item.title ?? "";
1242
+ const topics = override?.topics ?? item.topics ?? [];
1243
+ const rawDesc = override?.description ?? item.description ?? "";
1244
+ let description;
1245
+ if (isImagePost) {
1246
+ description = rawDesc;
1247
+ } else {
1248
+ description = ["YouTube", "Facebook"].includes(mediaType) ? rawDesc || title : title;
1249
+ }
1250
+ const topicPosition = ["YouTube", "Facebook"].includes(mediaType) ? 4 : 2;
1251
+ return { title, description, topics, topicPosition };
1252
+ }
1253
+ function buildImagePayload(img, currentUserId) {
1254
+ return {
1255
+ imageUrl: img.imageUrl,
1256
+ imageName: img.imageName,
1257
+ sourceImageId: img.sourceImageId ?? void 0,
1258
+ imageCreateUserId: img.imageCreateUserId ?? currentUserId ?? void 0,
1259
+ imageSourceType: img.imageSourceType,
1260
+ belongTo: -1,
1261
+ belongToId: null,
1262
+ replica: false,
1263
+ size: img.size ?? 0,
1264
+ width: img.width ?? 0,
1265
+ height: img.height ?? 0
1266
+ };
1267
+ }
1268
+ function buildCoverPayload(cover, videoId, currentUserId) {
1269
+ return {
1270
+ imageUrl: cover.imageUrl,
1271
+ imageName: cover.imageName,
1272
+ imageSourceType: cover.imageSourceType,
1273
+ sourceImageId: cover.sourceImageId ?? videoId,
1274
+ imageCreateUserId: cover.imageCreateUserId ?? currentUserId ?? void 0,
1275
+ // belongTo / belongToId:CLI 无法获取组织信息,统一与前端素材库视频行为保持一致
1276
+ belongTo: -1,
1277
+ belongToId: null,
1278
+ replica: false,
1279
+ coverTsp: 0,
1280
+ compositeCover: false,
1281
+ size: cover.size ?? 0,
1282
+ width: cover.width ?? 0,
1283
+ height: cover.height ?? 0
1284
+ };
1285
+ }
1286
+ function toSendType(mode) {
1287
+ return mode === "scheduled" ? 2 : 1;
1288
+ }
1289
+ function finalizeChannels(channels, sentKey) {
1290
+ for (const ch of Object.values(channels)) {
1291
+ let chSendType = 1;
1292
+ let chPublishAt = null;
1293
+ for (const acc of ch.mediaAccountInfos) {
1294
+ const items = acc[sentKey] ?? [];
1295
+ for (const v of items) {
1296
+ if (v.sendType <= 1) continue;
1297
+ if (v.sendType > chSendType) {
1298
+ chSendType = v.sendType;
1299
+ chPublishAt = v.publishAt;
1300
+ } else if (v.sendType === chSendType && v.publishAt && (!chPublishAt || new Date(v.publishAt) < new Date(chPublishAt))) {
1301
+ chPublishAt = v.publishAt;
1302
+ }
1303
+ }
1304
+ }
1305
+ ch.videoType = chSendType > 1 ? 2 : 1;
1306
+ ch.publishAts = chSendType > 1 && chPublishAt ? [chPublishAt] : [];
1307
+ }
1308
+ }
1309
+ function buildVideoChannels(videos, currentUserId) {
1310
+ const channels = {};
1311
+ const contents = [];
1312
+ const errorList = [];
1313
+ let globalSendType = 1;
1314
+ for (const item of videos) {
1315
+ if (!item.accounts?.length) errorList.push(`\u89C6\u9891 "${item.videoName}" \u672A\u8BBE\u7F6E\u8D26\u53F7`);
1316
+ if (!item.title) errorList.push(`\u89C6\u9891 "${item.videoName}" \u672A\u586B\u5199\u6807\u9898`);
1317
+ if (!item.cover?.imageUrl) errorList.push(`\u89C6\u9891 "${item.videoName}" \u5C01\u9762\u53C2\u6570\u7F3A\u5931`);
1318
+ let itemSendType = 1;
1319
+ for (const account of item.accounts ?? []) {
1320
+ const mediaType = account.mediaAccountType;
1321
+ const sendType = toSendType(item.publishMode);
1322
+ const publishAtUtc = item.publishAt ? new Date(item.publishAt).toISOString() : null;
1323
+ if (item.publishMode === "scheduled" && !item.publishAt) {
1324
+ errorList.push(`\u89C6\u9891 "${item.videoName}" \u8BBE\u7F6E\u4E86\u5B9A\u65F6\u53D1\u5E03\u4F46\u672A\u586B\u5199 publishAt`);
1325
+ }
1326
+ if (sendType > itemSendType) itemSendType = sendType;
1327
+ const override = item.platformOverrides?.[mediaType];
1328
+ const publishTitle = buildPublishTitle(item, mediaType, override);
1329
+ const youTubeVideoStatus = mediaType === "YouTube" ? {
1330
+ privacyStatus: item.youtubePrivacy ?? "public",
1331
+ instantPremiere: false
1332
+ } : null;
1333
+ const toBeSent = {
1334
+ videoId: item.videoId,
1335
+ sendType,
1336
+ publishAt: publishAtUtc,
1337
+ publishTitle,
1338
+ publishCover: buildCoverPayload(item.cover, item.videoId, currentUserId),
1339
+ youTubeVideoStatus
1340
+ };
1341
+ const accountInfo = {
1342
+ mediaAccountId: account.entityId,
1343
+ mediaCustomerId: account.mediaCustomerId,
1344
+ mediaCustomerName: account.mediaCustomerName,
1345
+ externalMediaAccountTokenId: account.externalMediaAccountTokenId
1346
+ };
1347
+ if (channels[mediaType]) {
1348
+ const existing = channels[mediaType].mediaAccountInfos.find(
1349
+ (i) => i.mediaAccountId === account.entityId
1350
+ );
1351
+ if (existing) {
1352
+ existing.videoToBeSent.push(toBeSent);
1353
+ } else {
1354
+ channels[mediaType].mediaAccountInfos.push({ ...accountInfo, videoToBeSent: [toBeSent] });
1355
+ }
1356
+ } else {
1357
+ channels[mediaType] = {
1358
+ mediaType,
1359
+ mediaAccountInfos: [{ ...accountInfo, videoToBeSent: [toBeSent] }]
1360
+ };
1361
+ }
1362
+ }
1363
+ if (itemSendType > globalSendType) globalSendType = itemSendType;
1364
+ contents.push({
1365
+ videoId: item.videoId,
1366
+ // 素材库视频:sourceVideoId 与 videoId 相同;非素材库:null
1367
+ sourceVideoId: item.isMaterialCenter ? item.videoId : null,
1368
+ videoUrl: item.videoUrl,
1369
+ videoName: item.videoName,
1370
+ width: item.width ?? 0,
1371
+ height: item.height ?? 0,
1372
+ videoSize: item.videoSize ?? 0,
1373
+ duration: item.duration ?? 0,
1374
+ // 实际请求中 videoMd5 为 null(非空字符串)
1375
+ videoMd5: item.videoMd5 ?? null,
1376
+ isMaterialCenter: item.isMaterialCenter ?? false,
1377
+ // videoCreateUserId:优先取配置里的值,回落到当前 token 的 sub(上传者 ID)
1378
+ videoCreateUserId: item.videoCreateUserId ?? currentUserId,
1379
+ // belongTo/-ToId:CLI 无组织上下文,统一 -1/null(与前端素材库视频行为一致)
1380
+ belongTo: -1,
1381
+ belongToId: null,
1382
+ replica: false,
1383
+ // mediaAccountIds:后端用于建立视频与账号的关联,缺少此字段会导致发布失败
1384
+ mediaAccountIds: (item.accounts ?? []).map((a) => a.entityId),
1385
+ // POI 地理位置:默认 poiWay=3(不使用 POI)
1386
+ poiWay: 3,
1387
+ poiId: "",
1388
+ poiName: "",
1389
+ defaultTitle: {
1390
+ title: item.title,
1391
+ description: item.description ?? "",
1392
+ topicPosition: 2,
1393
+ topics: item.topics ?? []
1394
+ },
1395
+ // defaultCover 补全所有后端字段(与 publishCover 保持结构一致)
1396
+ defaultCover: buildCoverPayload(item.cover, item.videoId, currentUserId)
1397
+ });
1398
+ }
1399
+ finalizeChannels(channels, "videoToBeSent");
1400
+ return { channels, videoType: globalSendType, contents, errorList };
1401
+ }
1402
+ function buildImageChannels(posts, currentUserId) {
1403
+ const channels = {};
1404
+ const contents = [];
1405
+ const errorList = [];
1406
+ let globalSendType = 1;
1407
+ const validPosts = posts.filter(
1408
+ (item) => item.images?.some((img) => img.imageUrl) || (item.title ?? "").trim()
1409
+ );
1410
+ if (validPosts.length === 0) {
1411
+ errorList.push("\u6CA1\u6709\u53EF\u53D1\u5E03\u7684\u5E16\u5B50\uFF0C\u8BF7\u81F3\u5C11\u4E3A\u6BCF\u4E2A\u5E16\u5B50\u6DFB\u52A0\u56FE\u7247\u6216\u6587\u6848");
1412
+ return { channels, videoType: 1, contents, errorList };
1413
+ }
1414
+ for (const item of validPosts) {
1415
+ const vGuid = item.vGuid ?? guid();
1416
+ if (!item.accounts?.length) errorList.push("\u56FE\u6587\u5E16\u5B50\u672A\u8BBE\u7F6E\u8D26\u53F7");
1417
+ let itemSendType = 1;
1418
+ for (const account of item.accounts ?? []) {
1419
+ const mediaType = account.mediaAccountType;
1420
+ const sendType = toSendType(item.publishMode);
1421
+ const publishAtUtc = item.publishAt ? new Date(item.publishAt).toISOString() : null;
1422
+ if (item.publishMode === "scheduled" && !item.publishAt) {
1423
+ errorList.push("\u56FE\u6587\u5E16\u5B50\u8BBE\u7F6E\u4E86\u5B9A\u65F6\u53D1\u5E03\u4F46\u672A\u586B\u5199 publishAt");
1424
+ }
1425
+ if (sendType > itemSendType) itemSendType = sendType;
1426
+ const override = item.platformOverrides?.[mediaType];
1427
+ const publishTitle = buildPublishTitle(item, mediaType, override, true);
1428
+ const toBeSent = {
1429
+ imageGroupId: vGuid,
1430
+ sendType,
1431
+ publishAt: publishAtUtc,
1432
+ publishTitle
1433
+ };
1434
+ const accountInfo = {
1435
+ mediaAccountId: account.entityId,
1436
+ mediaCustomerId: account.mediaCustomerId,
1437
+ mediaCustomerName: account.mediaCustomerName,
1438
+ externalMediaAccountTokenId: account.externalMediaAccountTokenId
1439
+ };
1440
+ if (channels[mediaType]) {
1441
+ const existing = channels[mediaType].mediaAccountInfos.find(
1442
+ (i) => i.mediaAccountId === account.entityId
1443
+ );
1444
+ if (existing) {
1445
+ existing.imageTextToBeSent.push(toBeSent);
1446
+ } else {
1447
+ channels[mediaType].mediaAccountInfos.push({ ...accountInfo, imageTextToBeSent: [toBeSent] });
1448
+ }
1449
+ } else {
1450
+ channels[mediaType] = {
1451
+ mediaType,
1452
+ mediaAccountInfos: [{ ...accountInfo, imageTextToBeSent: [toBeSent] }]
1453
+ };
1454
+ }
1455
+ }
1456
+ if (itemSendType > globalSendType) globalSendType = itemSendType;
1457
+ contents.push({
1458
+ imageGroupId: vGuid,
1459
+ // 补全每张图片的后端必填字段(belongTo、replica、imageCreateUserId 等)
1460
+ images: (item.images ?? []).map((img) => buildImagePayload(img, currentUserId)),
1461
+ mediaAccountIds: (item.accounts ?? []).map((a) => a.entityId)
1462
+ });
1463
+ }
1464
+ finalizeChannels(channels, "imageTextToBeSent");
1465
+ return { channels, videoType: globalSendType, contents, errorList };
1466
+ }
1467
+ async function runPublish(options) {
1468
+ const configPath = path6.resolve(options.config);
1469
+ if (!fs6.existsSync(configPath)) {
1470
+ console.error(`
1471
+ \u274C \u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${configPath}`);
1472
+ process.exit(1);
1473
+ }
1474
+ let input;
1475
+ try {
1476
+ input = JSON.parse(fs6.readFileSync(configPath, "utf8"));
1477
+ } catch (e) {
1478
+ console.error(`
1479
+ \u274C \u914D\u7F6E\u6587\u4EF6\u89E3\u6790\u5931\u8D25\uFF1A${e.message}`);
1480
+ process.exit(1);
1481
+ }
1482
+ const config = loadConfig(options.token);
1483
+ const jwtClaims = decodeJwtClaims(config.authToken);
1484
+ const currentUserId = isUUID(jwtClaims?.sub) ? jwtClaims.sub : null;
1485
+ const currentDid = isUUID(jwtClaims?.did) ? jwtClaims.did : null;
1486
+ const now = nowUtc();
1487
+ const uniqueActionId = guid();
1488
+ const taskName = input.taskName ?? defaultTaskName();
1489
+ const isDomestic = input.isDomesticTrade ?? false;
1490
+ const csoPlatformType = isDomestic ? 1 : 2;
1491
+ let channels;
1492
+ let videoType;
1493
+ let contents;
1494
+ let errorList;
1495
+ if (input.contentType === 1) {
1496
+ if (!input.videos?.length) {
1497
+ console.error("\n\u274C contentType=1\uFF08\u89C6\u9891\uFF09\u65F6\uFF0Cvideos \u6570\u7EC4\u4E0D\u80FD\u4E3A\u7A7A");
1498
+ process.exit(1);
1499
+ }
1500
+ ({ channels, videoType, contents, errorList } = buildVideoChannels(input.videos, currentUserId));
1501
+ } else {
1502
+ if (!input.posts?.length) {
1503
+ console.error("\n\u274C contentType=2\uFF08\u56FE\u6587\uFF09\u65F6\uFF0Cposts \u6570\u7EC4\u4E0D\u80FD\u4E3A\u7A7A");
1504
+ process.exit(1);
1505
+ }
1506
+ ({ channels, videoType, contents, errorList } = buildImageChannels(input.posts, currentUserId));
1507
+ }
1508
+ if (errorList.length > 0) {
1509
+ console.error("\n\u274C \u53C2\u6570\u6821\u9A8C\u5931\u8D25\uFF0C\u8BF7\u4FEE\u6B63\u4EE5\u4E0B\u95EE\u9898\u540E\u91CD\u8BD5\uFF1A");
1510
+ errorList.forEach((e) => console.error(` \xB7 ${e}`));
1511
+ process.exit(1);
1512
+ }
1513
+ const body = {
1514
+ contents: JSON.stringify(contents),
1515
+ channels: Object.values(channels),
1516
+ id: null,
1517
+ draftsId: null,
1518
+ uniqueActionId,
1519
+ taskName,
1520
+ failureNotification: input.failureNotification ?? false,
1521
+ contentType: input.contentType,
1522
+ videoType,
1523
+ csoPlatformType,
1524
+ publishTimeSetting: {},
1525
+ actionStartTime: now,
1526
+ actionStopTime: now,
1527
+ version: "v3"
1528
+ };
1529
+ if (options.dryRun) {
1530
+ console.log("\n\u2705 [dry-run] \u53D1\u5E03\u53C2\u6570\u9884\u89C8\uFF08\u672A\u5B9E\u9645\u63D0\u4EA4\uFF09\uFF1A\n");
1531
+ if (options.verbose) {
1532
+ console.log(JSON.stringify(body, null, 2));
1533
+ } else {
1534
+ const redacted = {
1535
+ ...body,
1536
+ uniqueActionId: "[\u5DF2\u9690\u85CF\uFF0C\u52A0 --verbose \u67E5\u770B]",
1537
+ channels: body.channels.map((ch) => ({
1538
+ ...ch,
1539
+ mediaAccountInfos: ch.mediaAccountInfos.map((acc) => ({
1540
+ ...acc,
1541
+ externalMediaAccountTokenId: `${acc.externalMediaAccountTokenId.slice(0, 4)}****`
1542
+ }))
1543
+ }))
1544
+ };
1545
+ console.log(JSON.stringify(redacted, null, 2));
1546
+ console.log("\n\u26A0\uFE0F \u90E8\u5206\u5B57\u6BB5\u5DF2\u8131\u654F\uFF0C\u52A0 --verbose \u53C2\u6570\u67E5\u770B\u5B8C\u6574\u8BF7\u6C42\u4F53\u3002");
1547
+ }
1548
+ return;
1549
+ }
1550
+ console.log(`
1551
+ \u{1F4E4} \u6B63\u5728\u63D0\u4EA4\u53D1\u5E03\u4EFB\u52A1\uFF1A"${taskName}" \u2026`);
1552
+ let res;
1553
+ try {
1554
+ res = await apiFetch2(
1555
+ `${config.csoBaseUrl}/cso/v1/video/create`,
1556
+ config,
1557
+ { method: "POST", body: JSON.stringify(body) },
1558
+ options.verbose
1559
+ );
1560
+ } catch (e) {
1561
+ const msg = e.message;
1562
+ console.error(`
1563
+ \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${msg}`);
1564
+ if (!options.verbose) console.error(" \u52A0 --verbose \u53C2\u6570\u53EF\u67E5\u770B\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F");
1565
+ process.exit(1);
1566
+ }
1567
+ if (res.code === 1) {
1568
+ console.log(`
1569
+ \u2705 \u53D1\u5E03\u4EFB\u52A1\u63D0\u4EA4\u6210\u529F\uFF01`);
1570
+ console.log(` \u4EFB\u52A1\u540D\uFF1A${taskName}`);
1571
+ console.log(` uniqueActionId\uFF1A${uniqueActionId}`);
1572
+ console.log(` \u53EF\u5728 CSO \u4EFB\u52A1\u5217\u8868\uFF08/task\uFF09\u67E5\u770B\u8FDB\u5EA6\u3002`);
1573
+ } else {
1574
+ console.error(`
1575
+ \u274C \u53D1\u5E03\u5931\u8D25\uFF1A${res.message ?? JSON.stringify(res)}`);
1576
+ process.exit(1);
1577
+ }
1578
+ }
1579
+
1580
+ // src/commands/upload.ts
1581
+ import * as fs7 from "fs";
1582
+ import * as os5 from "os";
1583
+ import * as path7 from "path";
1584
+ import { imageSize } from "image-size";
1585
+ import { ContainerClient } from "@azure/storage-blob";
1586
+ var SENSITIVE_PATH_PREFIXES = [
1587
+ path7.join(os5.homedir(), ".siluzan"),
1588
+ // 本工具配置(含 authToken)
1589
+ path7.join(os5.homedir(), ".ssh"),
1590
+ // SSH 私钥
1591
+ path7.join(os5.homedir(), ".aws"),
1592
+ // AWS 凭证
1593
+ path7.join(os5.homedir(), ".config"),
1594
+ // 各类应用配置
1595
+ path7.join(os5.homedir(), ".gnupg"),
1596
+ // GPG 私钥
1597
+ path7.join(os5.homedir(), ".npmrc"),
1598
+ // npm Token
1599
+ path7.join(os5.homedir(), ".docker"),
1600
+ // Docker 凭证
1601
+ path7.join(os5.homedir(), ".kube"),
1602
+ // Kubernetes 配置
1603
+ path7.join(os5.homedir(), ".pypirc"),
1604
+ // PyPI Token
1605
+ path7.join(os5.homedir(), ".gitconfig"),
1606
+ path7.join(os5.homedir(), ".env")
1607
+ // 常用本地环境变量文件
1608
+ ];
1609
+ var SENSITIVE_ABSOLUTE_PATHS = [
1610
+ "/etc/passwd",
1611
+ "/etc/shadow",
1612
+ "/etc/hosts"
1613
+ ];
1614
+ function validateUploadPath(resolved) {
1615
+ const normalized = resolved.toLowerCase();
1616
+ for (const prefix of SENSITIVE_PATH_PREFIXES) {
1617
+ if (normalized === prefix.toLowerCase() || normalized.startsWith(prefix.toLowerCase() + path7.sep)) {
1618
+ return `\u7981\u6B62\u4E0A\u4F20\u654F\u611F\u76EE\u5F55\u4E0B\u7684\u6587\u4EF6\uFF1A${prefix}`;
1619
+ }
1620
+ }
1621
+ for (const abs of SENSITIVE_ABSOLUTE_PATHS) {
1622
+ if (normalized === abs.toLowerCase()) {
1623
+ return `\u7981\u6B62\u4E0A\u4F20\u7CFB\u7EDF\u654F\u611F\u6587\u4EF6\uFF1A${abs}`;
1624
+ }
1625
+ }
1626
+ const base = path7.basename(resolved).toLowerCase();
1627
+ if (base === ".env" || base.startsWith(".env.")) {
1628
+ return `\u7981\u6B62\u4E0A\u4F20\u73AF\u5883\u53D8\u91CF\u6587\u4EF6\uFF08${path7.basename(resolved)}\uFF09\uFF0C\u53EF\u80FD\u5305\u542B\u5BC6\u94A5`;
1629
+ }
1630
+ return null;
1631
+ }
1632
+ var DEFAULT_FOLDER_ID = "77777777-7777-7777-7777-777777777777";
1633
+ var BLOCK_SIZE = 2 * 1024 * 1024;
1634
+ var VIDEO_EXTS = [
1635
+ ".mp4",
1636
+ ".mov",
1637
+ ".avi",
1638
+ ".mkv",
1639
+ ".flv",
1640
+ ".f4v",
1641
+ ".rmvb",
1642
+ ".rm",
1643
+ ".vob",
1644
+ ".wmv",
1645
+ ".3gp",
1646
+ ".asf",
1647
+ ".mpeg",
1648
+ ".m4v",
1649
+ ".mpg"
1650
+ ];
1651
+ var IMAGE_EXTS = [".jpg", ".jpeg", ".png", ".gif", ".arw"];
1652
+ function detectKind(filePath) {
1653
+ const ext = path7.extname(filePath).toLowerCase();
1654
+ if (VIDEO_EXTS.includes(ext)) return "video";
1655
+ if (IMAGE_EXTS.includes(ext)) return "image";
1656
+ throw new Error(
1657
+ `\u65E0\u6CD5\u8BC6\u522B\u6587\u4EF6\u7C7B\u578B\uFF1A${ext}\u3002
1658
+ \u89C6\u9891\uFF1A${VIDEO_EXTS.join(", ")}
1659
+ \u56FE\u7247\uFF1A${IMAGE_EXTS.join(", ")}`
1660
+ );
1661
+ }
1662
+ function getMimeType(filePath) {
1663
+ const map = {
1664
+ ".mp4": "video/mp4",
1665
+ ".mov": "video/quicktime",
1666
+ ".avi": "video/avi",
1667
+ ".mkv": "video/x-matroska",
1668
+ ".flv": "video/x-flv",
1669
+ ".wmv": "video/x-ms-wmv",
1670
+ ".3gp": "video/3gpp",
1671
+ ".jpg": "image/jpeg",
1672
+ ".jpeg": "image/jpeg",
1673
+ ".png": "image/png",
1674
+ ".gif": "image/gif"
1675
+ };
1676
+ return map[path7.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1677
+ }
1678
+ var guid2 = randomUUID;
1679
+ async function generateSas(config, verbose = false) {
1680
+ const url = `${config.apiBaseUrl}/cutapi/v1/material/generateuploadsas?account=cutapi&expirationMinutes=120`;
1681
+ const res = await apiFetch2(url, config, {}, verbose);
1682
+ if (res.code !== 1 || !res.data) {
1683
+ throw new Error("\u7533\u8BF7\u4E0A\u4F20\u51ED\u8BC1\u5931\u8D25\uFF0C\u670D\u52A1\u7AEF\u8FD4\u56DE\u5F02\u5E38");
1684
+ }
1685
+ return res.data;
1686
+ }
1687
+ async function uploadToAzure(filePath, sas, onProgress) {
1688
+ const ext = path7.extname(filePath);
1689
+ const blobName = `${guid2()}${ext}`;
1690
+ const containerClient = new ContainerClient(sas.sasUri);
1691
+ const blockBlobClient = containerClient.getBlockBlobClient(blobName);
1692
+ const fileSize = fs7.statSync(filePath).size;
1693
+ await blockBlobClient.uploadFile(filePath, {
1694
+ blockSize: BLOCK_SIZE,
1695
+ blobHTTPHeaders: { blobContentType: getMimeType(filePath) },
1696
+ onProgress: (ev) => {
1697
+ if (onProgress && fileSize > 0) {
1698
+ onProgress(Math.round(ev.loadedBytes / fileSize * 100));
1699
+ }
1700
+ }
1701
+ });
1702
+ const resourceUrl = `${sas.containerUri}/${blobName}`;
1703
+ return { resourceUrl, blobName };
1704
+ }
1705
+ async function notifyUploaded(config, body, verbose = false) {
1706
+ const url = `${config.csoBaseUrl}/cutapi/v1/material/uploadednotify`;
1707
+ const res = await apiFetch2(url, config, {
1708
+ method: "POST",
1709
+ body: JSON.stringify(body)
1710
+ }, verbose);
1711
+ if (res.code !== 1 || !res.data?.id) {
1712
+ const detail = verbose ? `\uFF1A${JSON.stringify(res)}` : "";
1713
+ throw new Error(`\u7D20\u6750\u6CE8\u518C\u5931\u8D25${detail}`);
1714
+ }
1715
+ return res.data.id;
1716
+ }
1717
+ async function fetchMaterialInfo(config, id, verbose = false) {
1718
+ try {
1719
+ const url = `${config.csoBaseUrl}/cutapi/v1/material/${id}`;
1720
+ const res = await apiFetch2(url, config, {}, verbose);
1721
+ return res.data ?? {};
1722
+ } catch {
1723
+ return {};
1724
+ }
1725
+ }
1726
+ function readMp4Duration(filePath) {
1727
+ try {
1728
+ const fileSize = fs7.statSync(filePath).size;
1729
+ const chunkSize = Math.min(fileSize, 256 * 1024);
1730
+ const bufEnd = Buffer.alloc(chunkSize);
1731
+ const fdEnd = fs7.openSync(filePath, "r");
1732
+ fs7.readSync(fdEnd, bufEnd, 0, chunkSize, fileSize - chunkSize);
1733
+ fs7.closeSync(fdEnd);
1734
+ const bufStart = Buffer.alloc(chunkSize);
1735
+ const fdStart = fs7.openSync(filePath, "r");
1736
+ fs7.readSync(fdStart, bufStart, 0, chunkSize, 0);
1737
+ fs7.closeSync(fdStart);
1738
+ const durationSec = parseMvhdFromBuffer(bufEnd) ?? parseMvhdFromBuffer(bufStart);
1739
+ if (durationSec === null || durationSec <= 0) return null;
1740
+ const h = Math.floor(durationSec / 3600);
1741
+ const m = Math.floor(durationSec % 3600 / 60);
1742
+ const s = Math.floor(durationSec % 60);
1743
+ const ms = Math.round(durationSec % 1 * 1e3);
1744
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(ms).padStart(3, "0")}`;
1745
+ } catch {
1746
+ return null;
1747
+ }
1748
+ }
1749
+ function parseMvhdFromBuffer(buf) {
1750
+ const marker = Buffer.from("6d766864", "hex");
1751
+ const idx = buf.lastIndexOf(marker);
1752
+ if (idx < 4) return null;
1753
+ const dataStart = idx + 4;
1754
+ const version = buf[dataStart];
1755
+ let timescale;
1756
+ let durationVal;
1757
+ if (version === 0) {
1758
+ timescale = buf.readUInt32BE(dataStart + 1 + 3 + 4 + 4);
1759
+ durationVal = buf.readUInt32BE(dataStart + 1 + 3 + 4 + 4 + 4);
1760
+ } else if (version === 1) {
1761
+ timescale = buf.readUInt32BE(dataStart + 1 + 3 + 8 + 8);
1762
+ durationVal = buf.readUInt32BE(dataStart + 1 + 3 + 8 + 8 + 4 + 4);
1763
+ } else {
1764
+ return null;
1765
+ }
1766
+ if (timescale === 0) return null;
1767
+ return durationVal / timescale;
1768
+ }
1769
+ function readImageDimensions(filePath) {
1770
+ try {
1771
+ const buf = fs7.readFileSync(filePath);
1772
+ const dim = imageSize(buf);
1773
+ return { width: dim.width, height: dim.height, format: (dim.type ?? "").toUpperCase() };
1774
+ } catch {
1775
+ return { width: 0, height: 0 };
1776
+ }
1777
+ }
1778
+ async function uploadImageFile(filePath, config, verbose = false) {
1779
+ const fileName = path7.basename(filePath);
1780
+ const fileSize = fs7.statSync(filePath).size;
1781
+ const format = path7.extname(filePath).replace(".", "").toUpperCase();
1782
+ const sas = await generateSas(config, verbose);
1783
+ const { resourceUrl } = await uploadToAzure(filePath, sas, void 0);
1784
+ const dim = readImageDimensions(filePath);
1785
+ const materialId = await notifyUploaded(config, {
1786
+ folderId: DEFAULT_FOLDER_ID,
1787
+ name: fileName,
1788
+ url: resourceUrl,
1789
+ fileSize,
1790
+ type: 1,
1791
+ source: 1,
1792
+ blobAccount: sas.accountName,
1793
+ blobContainer: sas.containerName,
1794
+ format,
1795
+ image: dim
1796
+ }, verbose);
1797
+ const matInfo = await fetchMaterialInfo(config, materialId, verbose);
1798
+ return {
1799
+ resourceUrl,
1800
+ materialId,
1801
+ belongToId: matInfo?.belongToId,
1802
+ belongTo: matInfo?.belongTo,
1803
+ userId: matInfo?.userId
1804
+ };
1805
+ }
1806
+ async function runUpload(options) {
1807
+ const filePath = path7.resolve(options.file);
1808
+ if (!fs7.existsSync(filePath)) {
1809
+ console.error(`
1810
+ \u274C \u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}`);
1811
+ process.exit(1);
1812
+ }
1813
+ const pathErr = validateUploadPath(filePath);
1814
+ if (pathErr) {
1815
+ console.error(`
1816
+ \u274C ${pathErr}`);
1817
+ process.exit(1);
1818
+ }
1819
+ const kind = options.kind ?? detectKind(filePath);
1820
+ const config = loadConfig(options.token);
1821
+ const fileName = path7.basename(filePath);
1822
+ const fileSize = fs7.statSync(filePath).size;
1823
+ const format = path7.extname(filePath).replace(".", "").toUpperCase();
1824
+ if (kind === "video" && !options.cover) {
1825
+ console.error("\n\u274C \u4E0A\u4F20\u89C6\u9891\u65F6\u5FC5\u987B\u901A\u8FC7 --cover <\u56FE\u7247\u8DEF\u5F84> \u6307\u5B9A\u5C01\u9762\u56FE\u7247");
1826
+ process.exit(1);
1827
+ }
1828
+ console.log(`
1829
+ \u{1F4C1} \u51C6\u5907\u4E0A\u4F20${kind === "video" ? "\u89C6\u9891" : "\u56FE\u7247"}\uFF1A${fileName}`);
1830
+ console.log(` \u5927\u5C0F\uFF1A${(fileSize / 1024 / 1024).toFixed(2)} MB`);
1831
+ let thumbUrl = "";
1832
+ let coverMaterialId = "";
1833
+ let coverBelongToId;
1834
+ let coverBelongTo;
1835
+ let coverUserId;
1836
+ const { verbose = false } = options;
1837
+ if (kind === "video" && options.cover) {
1838
+ const coverPath = path7.resolve(options.cover);
1839
+ if (!fs7.existsSync(coverPath)) {
1840
+ console.error(`
1841
+ \u274C \u5C01\u9762\u56FE\u7247\u4E0D\u5B58\u5728\uFF1A${coverPath}`);
1842
+ process.exit(1);
1843
+ }
1844
+ const coverPathErr = validateUploadPath(coverPath);
1845
+ if (coverPathErr) {
1846
+ console.error(`
1847
+ \u274C \u5C01\u9762\u8DEF\u5F84\u4E0D\u5408\u6CD5\uFF1A${coverPathErr}`);
1848
+ process.exit(1);
1849
+ }
1850
+ console.log(`
1851
+ 0\uFE0F\u20E3 \u4E0A\u4F20\u5C01\u9762\u56FE\u7247\uFF1A${path7.basename(coverPath)} \u2026`);
1852
+ try {
1853
+ const coverResult = await uploadImageFile(coverPath, config, verbose);
1854
+ thumbUrl = coverResult.resourceUrl;
1855
+ coverMaterialId = coverResult.materialId;
1856
+ coverBelongToId = coverResult.belongToId;
1857
+ coverBelongTo = coverResult.belongTo;
1858
+ coverUserId = coverResult.userId;
1859
+ if (verbose) console.log(` \u5C01\u9762 URL\uFF1A${thumbUrl}`);
1860
+ else console.log(` \u5C01\u9762\u5DF2\u4E0A\u4F20\uFF08\u7D20\u6750 ID\uFF1A${coverMaterialId}\uFF09`);
1861
+ } catch (e) {
1862
+ const msg = e.message;
1863
+ console.error(`
1864
+ \u274C \u5C01\u9762\u4E0A\u4F20\u5931\u8D25\uFF1A${msg}`);
1865
+ if (!verbose) console.error(" \u52A0 --verbose \u53C2\u6570\u53EF\u67E5\u770B\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F");
1866
+ process.exit(1);
1867
+ }
1868
+ }
1869
+ console.log("\n1\uFE0F\u20E3 \u7533\u8BF7\u4E0A\u4F20\u51ED\u8BC1 \u2026");
1870
+ let sas;
1871
+ try {
1872
+ sas = await generateSas(config, verbose);
1873
+ } catch (e) {
1874
+ const msg = e.message;
1875
+ console.error(`
1876
+ \u274C \u83B7\u53D6\u4E0A\u4F20\u51ED\u8BC1\u5931\u8D25\uFF1A${msg}`);
1877
+ if (msg.includes("401") || msg.includes("403")) {
1878
+ console.error(" Token \u65E0\u6548\u6216\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u914D\u7F6E\uFF1A\n siluzan-cso config set --token <\u4F60\u7684 token>");
1879
+ } else if (!verbose) {
1880
+ console.error(" \u52A0 --verbose \u53C2\u6570\u53EF\u67E5\u770B\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F");
1881
+ }
1882
+ process.exit(1);
1883
+ }
1884
+ console.log("2\uFE0F\u20E3 \u4E0A\u4F20\u5230\u4E91\u5B58\u50A8 \u2026");
1885
+ let resourceUrl;
1886
+ let lastPercent = 0;
1887
+ try {
1888
+ ({ resourceUrl } = await uploadToAzure(filePath, sas, (pct) => {
1889
+ if (pct - lastPercent >= 10 || pct === 100) {
1890
+ process.stdout.write(` \u8FDB\u5EA6\uFF1A${pct}%\r`);
1891
+ lastPercent = pct;
1892
+ }
1893
+ }));
1894
+ } catch (e) {
1895
+ console.error(`
1896
+
1897
+ \u274C \u4E0A\u4F20\u5931\u8D25\uFF1A${e.message}`);
1898
+ process.exit(1);
1899
+ }
1900
+ console.log(" \u8FDB\u5EA6\uFF1A100% \u2713 ");
1901
+ console.log("3\uFE0F\u20E3 \u6CE8\u518C\u7D20\u6750 \u2026");
1902
+ const imageDim = kind === "image" ? readImageDimensions(filePath) : void 0;
1903
+ let materialId;
1904
+ try {
1905
+ materialId = await notifyUploaded(config, {
1906
+ folderId: DEFAULT_FOLDER_ID,
1907
+ name: fileName,
1908
+ url: resourceUrl,
1909
+ fileSize,
1910
+ type: kind === "video" ? 3 : 1,
1911
+ // 1=图片 3=视频(与前端 fileType() 一致)
1912
+ source: 1,
1913
+ blobAccount: sas.accountName,
1914
+ blobContainer: sas.containerName,
1915
+ format,
1916
+ ...kind === "image" ? { image: imageDim } : {},
1917
+ ...kind === "video" ? (() => {
1918
+ const dur = readMp4Duration(filePath);
1919
+ if (!dur) throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u65F6\u957F\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u4E3A\u6709\u6548\u7684 MP4 \u683C\u5F0F");
1920
+ return { video: {}, audio: {}, duration: dur, overallBitRate: 0, thumbUrl };
1921
+ })() : {}
1922
+ }, verbose);
1923
+ } catch (e) {
1924
+ const msg = e.message;
1925
+ console.error(`
1926
+ \u274C \u6CE8\u518C\u5931\u8D25\uFF1A${msg}`);
1927
+ if (!verbose) console.error(" \u52A0 --verbose \u53C2\u6570\u53EF\u67E5\u770B\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F");
1928
+ process.exit(1);
1929
+ }
1930
+ console.log("4\uFE0F\u20E3 \u83B7\u53D6\u7D20\u6750\u4FE1\u606F \u2026");
1931
+ const matInfo = await fetchMaterialInfo(config, materialId, verbose);
1932
+ const previewSas = matInfo?.previewSas ?? "";
1933
+ const coverUrl = matInfo?.thumbUrl && previewSas ? `${matInfo.thumbUrl}?${previewSas}` : thumbUrl;
1934
+ const accessUrl = previewSas ? `${resourceUrl}?${previewSas}` : `${resourceUrl}?${sas.sasToken}`;
1935
+ console.log(`
1936
+ \u2705 \u4E0A\u4F20\u6210\u529F\uFF01`);
1937
+ console.log(` \u7D20\u6750 ID : ${materialId}`);
1938
+ if (kind === "video") {
1939
+ const videoConfig = {
1940
+ videoId: materialId,
1941
+ // verbose 模式输出含签名 URL,普通模式只输出不含签名的基础 URL
1942
+ videoUrl: verbose ? accessUrl : resourceUrl,
1943
+ videoName: fileName,
1944
+ isMaterialCenter: true,
1945
+ ...matInfo?.video ? { width: matInfo.video.width, height: matInfo.video.height } : {},
1946
+ ...matInfo?.duration ? { duration: matInfo.duration } : {},
1947
+ cover: {
1948
+ imageUrl: verbose ? coverUrl || "\uFF08\u8BF7\u624B\u52A8\u586B\u5199\u5C01\u9762\u56FE\u7247 URL\uFF09" : thumbUrl,
1949
+ imageName: options.cover ? path7.basename(options.cover) : "cover.jpg",
1950
+ imageSourceType: 1,
1951
+ sourceImageId: coverMaterialId || void 0,
1952
+ imageCreateUserId: coverUserId || void 0,
1953
+ belongTo: coverBelongTo !== void 0 ? coverBelongTo : -1,
1954
+ belongToId: coverBelongToId || null
1955
+ }
1956
+ };
1957
+ console.log("\n\u2500\u2500 \u5C06\u4EE5\u4E0B\u5185\u5BB9\u7C98\u8D34\u5230 publish-config.json \u7684 videos[] \u6761\u76EE\uFF1A\n");
1958
+ console.log(JSON.stringify(videoConfig, null, 2));
1959
+ } else {
1960
+ const imageConfig = {
1961
+ // 必须使用带 SAS 签名的 URL,后端用此地址拉取图片内容
1962
+ imageUrl: accessUrl,
1963
+ imageName: fileName,
1964
+ // 素材中心图片固定为 2(与前端 createImageByMaterial 保持一致)
1965
+ imageSourceType: 2,
1966
+ sourceImageId: materialId,
1967
+ size: fileSize,
1968
+ ...matInfo?.image ? { width: matInfo.image.width, height: matInfo.image.height } : {}
1969
+ };
1970
+ console.log("\n\u2500\u2500 \u5C06\u4EE5\u4E0B\u5185\u5BB9\u7C98\u8D34\u5230 publish-config.json \u7684 posts[].images[] \u6216 cover\uFF1A\n");
1971
+ console.log(JSON.stringify(imageConfig, null, 2));
1972
+ }
1973
+ }
1974
+
1975
+ // src/commands/config.ts
1976
+ import * as fs8 from "fs";
1977
+ function cmdConfigShow() {
1978
+ const shared = readSharedConfig();
1979
+ if (!shared.authToken) {
1980
+ console.log(
1981
+ "\n\u5C1A\u672A\u914D\u7F6E Token\u3002\n\n \u5982\u679C\u4F60\u8FD8\u6CA1\u6709 Siluzan \u8D26\u53F7\uFF0C\u8BF7\u5148\u6CE8\u518C\uFF1A\n \u{1F449} https://www.siluzan.com\n\n \u6CE8\u518C\u767B\u5F55\u540E\u8FDB\u5165\u300C\u4E2A\u4EBA\u8BBE\u7F6E \u2192 API Token\u300D\u590D\u5236\u4F60\u7684 Token\uFF0C\u7136\u540E\u8FD0\u884C\uFF1A\n siluzan-cso login\n"
1982
+ );
1983
+ return;
1984
+ }
1985
+ const apiBaseUrl = shared.apiBaseUrl ?? DEFAULT_API_BASE;
1986
+ const csoBaseUrl = DEFAULT_CSO_BASE;
1987
+ const apiKey = shared.apiKey ?? "";
1988
+ console.log("\n\u5F53\u524D\u914D\u7F6E\uFF08~/.siluzan/config.json\uFF09\uFF1A");
1989
+ console.log(` \u6784\u5EFA\u73AF\u5883 : ${BUILD_ENV}`);
1990
+ console.log(` apiBaseUrl : ${apiBaseUrl}`);
1991
+ console.log(` csoBaseUrl : ${csoBaseUrl}`);
1992
+ if (apiKey) {
1993
+ console.log(` apiKey : ${maskSecret(apiKey)} \u2190 \u5F53\u524D\u751F\u6548\uFF08X-Api-Key \u9274\u6743\uFF09`);
1994
+ }
1995
+ if (shared.authToken) {
1996
+ const note = apiKey ? " \uFF08\u5DF2\u88AB apiKey \u8986\u76D6\uFF09" : " \u2190 \u5F53\u524D\u751F\u6548\uFF08Bearer \u9274\u6743\uFF09";
1997
+ console.log(` authToken : ${maskSecret(shared.authToken)}${note}`);
1998
+ }
1999
+ console.log(`
2000
+ \u914D\u7F6E\u6587\u4EF6\u4F4D\u7F6E\uFF1A${CONFIG_FILE}`);
2001
+ if (process.platform !== "win32") {
2002
+ console.log(" \u26A0\uFE0F Token \u660E\u6587\u5B58\u50A8\uFF0C\u8BF7\u786E\u4FDD\u6587\u4EF6\u6743\u9650\u4E3A 600\uFF0C\u4E14\u4E0D\u8981\u63D0\u4EA4\u81F3\u4EE3\u7801\u4ED3\u5E93\u6216\u7F51\u76D8\u3002");
2003
+ }
2004
+ console.log();
2005
+ }
2006
+ function cmdConfigSet(opts) {
2007
+ if (!opts.apiKey && !opts.token && !opts.apiBase) {
2008
+ console.error("\n\u274C \u8BF7\u81F3\u5C11\u63D0\u4F9B\u4E00\u4E2A\u8981\u66F4\u65B0\u7684\u914D\u7F6E\u9879\uFF08--api-key\u3001--token \u6216 --api-base\uFF09\n");
2009
+ process.exit(1);
2010
+ }
2011
+ if (opts.apiBase) {
2012
+ const err = validateBaseUrl(opts.apiBase);
2013
+ if (err) {
2014
+ console.error(`
2015
+ \u274C --api-base \u4E0D\u5408\u6CD5\uFF1A${err}
2016
+ `);
2017
+ process.exit(1);
2018
+ }
2019
+ writeSharedConfig({ apiBaseUrl: opts.apiBase });
2020
+ }
2021
+ if (opts.apiKey) writeSharedConfig({ apiKey: opts.apiKey });
2022
+ if (opts.token) writeSharedConfig({ authToken: opts.token });
2023
+ console.log(`
2024
+ \u2705 \u914D\u7F6E\u5DF2\u4FDD\u5B58\u5230 ${CONFIG_FILE}`);
2025
+ if (opts.apiKey) console.log(` apiKey: ${maskSecret(opts.apiKey)}`);
2026
+ if (opts.token) console.log(` authToken: ${maskSecret(opts.token)}`);
2027
+ if (opts.apiBase) console.log(` apiBaseUrl: ${opts.apiBase}`);
2028
+ console.log("\n\u540E\u7EED\u6240\u6709\u547D\u4EE4\u81EA\u52A8\u8BFB\u53D6\u6B64\u914D\u7F6E\u3002\n");
2029
+ }
2030
+ function cmdConfigClear() {
2031
+ clearSharedConfig();
2032
+ console.log(`
2033
+ \u2705 authToken \u5DF2\u6E05\u7A7A\uFF08${CONFIG_FILE}\uFF09
2034
+ `);
2035
+ }
2036
+
2037
+ // src/commands/letter.ts
2038
+ var PAGE_SIZE = 20;
2039
+ var AUTO_PAGINATE_LIMIT = 500;
2040
+ function parseContent(raw) {
2041
+ try {
2042
+ return JSON.parse(raw);
2043
+ } catch {
2044
+ return null;
2045
+ }
2046
+ }
2047
+ function formatUtcTime(utcStr) {
2048
+ try {
2049
+ const dateStr = utcStr.endsWith("Z") ? utcStr : utcStr + "Z";
2050
+ return new Date(dateStr).toLocaleString("zh-CN");
2051
+ } catch {
2052
+ return utcStr;
2053
+ }
2054
+ }
2055
+ function contentToText(content) {
2056
+ if (!content) return "[\u89E3\u6790\u5931\u8D25]";
2057
+ if (content.message_type === "text") return content.text ?? "";
2058
+ if (content.message_type === "image") return "[\u56FE\u7247\u6D88\u606F]";
2059
+ if (content.message_type === "video") return "[\u89C6\u9891\u6D88\u606F]";
2060
+ return "[\u5176\u4ED6\u6D88\u606F\uFF0C\u8BF7\u5728 CSO \u79C1\u4FE1\u9875\u9762\u67E5\u770B]";
2061
+ }
2062
+ async function postFormData(url, config, data) {
2063
+ const form = new FormData();
2064
+ for (const [key, value] of Object.entries(data)) {
2065
+ form.append(key, value);
2066
+ }
2067
+ const res = await fetch(url, {
2068
+ method: "POST",
2069
+ headers: {
2070
+ Authorization: `Bearer ${config.authToken}`
2071
+ // 不手动设 Content-Type,fetch 会为 FormData 自动添加带 boundary 的 multipart 头
2072
+ },
2073
+ body: form
2074
+ });
2075
+ if (!res.ok) {
2076
+ throw new Error(`HTTP ${res.status}`);
2077
+ }
2078
+ }
2079
+ async function fetchSessionPage(config, csoPlatformType, mediaCustomerId, cursor, unreadOnly) {
2080
+ const params = new URLSearchParams({
2081
+ mediaCustomerId,
2082
+ size: String(PAGE_SIZE)
2083
+ });
2084
+ if (cursor) params.set("cursor", cursor);
2085
+ if (unreadOnly) params.set("isRead", "false");
2086
+ return apiFetch2(
2087
+ `${config.csoBaseUrl}/cso/v1/im/userSessionList/${csoPlatformType}?${params}`,
2088
+ config
2089
+ );
2090
+ }
2091
+ async function runLetterSessions(options) {
2092
+ const config = loadConfig(options.token);
2093
+ const csoPlatformType = options.domestic ? 1 : 2;
2094
+ let sessions = [];
2095
+ let nextCursor = null;
2096
+ let hasMore = false;
2097
+ if (options.unreadOnly) {
2098
+ let cursor = options.cursor ?? null;
2099
+ let pagesFetched = 0;
2100
+ console.log("\u6B63\u5728\u62C9\u53D6\u672A\u8BFB\u4F1A\u8BDD\u2026");
2101
+ while (true) {
2102
+ let res;
2103
+ try {
2104
+ res = await fetchSessionPage(config, csoPlatformType, options.account, cursor, true);
2105
+ } catch (e) {
2106
+ console.error(`
2107
+ \u274C \u83B7\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${e.message}`);
2108
+ process.exit(1);
2109
+ }
2110
+ if (res.code !== 1) {
2111
+ console.error("\n\u274C \u670D\u52A1\u7AEF\u8FD4\u56DE\u5F02\u5E38\uFF0C\u83B7\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25");
2112
+ process.exit(1);
2113
+ }
2114
+ const page = res.data?.list ?? [];
2115
+ sessions.push(...page);
2116
+ pagesFetched++;
2117
+ cursor = res.data?.cursor ?? null;
2118
+ hasMore = Boolean(res.data?.hasMore);
2119
+ if (page.length > 0) {
2120
+ console.log(` \u7B2C ${pagesFetched} \u9875\uFF1A${page.length} \u6761\uFF0C\u7D2F\u8BA1 ${sessions.length} \u6761`);
2121
+ }
2122
+ if (sessions.length >= AUTO_PAGINATE_LIMIT) {
2123
+ console.log(
2124
+ `
2125
+ \u26A0\uFE0F \u5DF2\u62C9\u53D6 ${sessions.length} \u6761\u672A\u8BFB\u4F1A\u8BDD\uFF0C\u8FBE\u5230\u5B89\u5168\u4E0A\u9650\uFF08${AUTO_PAGINATE_LIMIT}\uFF09\uFF0C\u505C\u6B62\u7EE7\u7EED\u7FFB\u9875\u3002
2126
+ \u5982\u9700\u5904\u7406\u66F4\u591A\uFF0C\u8BF7\u5904\u7406\u5B8C\u5F53\u524D\u6279\u6B21\u540E\u4F7F\u7528 --cursor ${cursor} \u7EE7\u7EED\u3002`
2127
+ );
2128
+ nextCursor = cursor;
2129
+ hasMore = Boolean(res.data?.hasMore);
2130
+ break;
2131
+ }
2132
+ if (!hasMore || !cursor) break;
2133
+ }
2134
+ } else {
2135
+ let res;
2136
+ try {
2137
+ res = await fetchSessionPage(config, csoPlatformType, options.account, options.cursor ?? null, false);
2138
+ } catch (e) {
2139
+ console.error(`
2140
+ \u274C \u83B7\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25\uFF1A${e.message}`);
2141
+ process.exit(1);
2142
+ }
2143
+ if (res.code !== 1) {
2144
+ console.error("\n\u274C \u670D\u52A1\u7AEF\u8FD4\u56DE\u5F02\u5E38\uFF0C\u83B7\u53D6\u4F1A\u8BDD\u5217\u8868\u5931\u8D25");
2145
+ process.exit(1);
2146
+ }
2147
+ sessions = res.data?.list ?? [];
2148
+ nextCursor = res.data?.cursor ?? null;
2149
+ hasMore = Boolean(res.data?.hasMore);
2150
+ }
2151
+ const modeLabel = options.unreadOnly ? "\u672A\u8BFB" : "\u5168\u90E8";
2152
+ if (sessions.length === 0) {
2153
+ console.log(`
2154
+ \u6682\u65E0${modeLabel}\u4F1A\u8BDD\u3002`);
2155
+ return;
2156
+ }
2157
+ const batchSize = options.batchSize ?? sessions.length;
2158
+ const totalBatches = Math.ceil(sessions.length / batchSize);
2159
+ console.log(
2160
+ `
2161
+ \u627E\u5230 ${sessions.length} \u4E2A${modeLabel}\u4F1A\u8BDD\uFF08\u8D26\u53F7 ${options.account}\uFF09` + (totalBatches > 1 ? `\uFF0C\u5DF2\u5206\u4E3A ${totalBatches} \u6279\uFF08\u6BCF\u6279 ${batchSize} \u4E2A\uFF09` : "") + "\uFF1A\n"
2162
+ );
2163
+ for (let batchIdx = 0; batchIdx < totalBatches; batchIdx++) {
2164
+ const batch = sessions.slice(batchIdx * batchSize, (batchIdx + 1) * batchSize);
2165
+ if (totalBatches > 1) {
2166
+ console.log(`
2167
+ \u2501\u2501\u2501 \u7B2C ${batchIdx + 1} \u6279 / \u5171 ${totalBatches} \u6279 \u2501\u2501\u2501
2168
+ `);
2169
+ }
2170
+ for (const s of batch) {
2171
+ const content = parseContent(s.content);
2172
+ const lastMsg = contentToText(content).slice(0, 60);
2173
+ const unreadMark = s.unread > 0 ? ` \u26A1 \u672A\u8BFB ${s.unread} \u6761` : "";
2174
+ const time = new Date(s.lastContactTime * 1e3).toLocaleString("zh-CN");
2175
+ const name = s.nikeName ?? "(\u672A\u77E5)";
2176
+ console.log(` userId: ${s.userId} \u6635\u79F0: ${name}${unreadMark}`);
2177
+ console.log(` \u6700\u65B0\u6D88\u606F: ${lastMsg || "(\u7A7A)"} \u65F6\u95F4: ${time}
2178
+ `);
2179
+ }
2180
+ console.log(`\u2500\u2500 \u6279\u6B21 ${batchIdx + 1} JSON\uFF1A
2181
+ `);
2182
+ console.log(
2183
+ JSON.stringify(
2184
+ batch.map((s) => {
2185
+ const content = parseContent(s.content);
2186
+ return {
2187
+ userId: s.userId,
2188
+ nikeName: s.nikeName ?? "(\u672A\u77E5)",
2189
+ mediaCustomerId: s.mediaCustomerId,
2190
+ unread: s.unread,
2191
+ lastContactTime: new Date(s.lastContactTime * 1e3).toISOString(),
2192
+ lastMessage: contentToText(content)
2193
+ };
2194
+ }),
2195
+ null,
2196
+ 2
2197
+ )
2198
+ );
2199
+ if (totalBatches > 1 && batchIdx < totalBatches - 1) {
2200
+ console.log(`
2201
+ \u23F8 \u8BF7\u5904\u7406\u5B8C\u4EE5\u4E0A\u7B2C ${batchIdx + 1} \u6279\u4F1A\u8BDD\u540E\uFF0C\u518D\u7EE7\u7EED\u5904\u7406\u4E0B\u4E00\u6279\u3002`);
2202
+ }
2203
+ }
2204
+ if (hasMore && nextCursor) {
2205
+ console.log(
2206
+ `
2207
+ \u{1F4C4} \u8FD8\u6709\u66F4\u591A\u4F1A\u8BDD\u672A\u52A0\u8F7D\u3002\u7EE7\u7EED\u83B7\u53D6\u8BF7\u8FD0\u884C\uFF1A
2208
+ siluzan-cso letter sessions -a ${options.account}` + (options.unreadOnly ? " --unread-only" : "") + (options.domestic ? " --domestic" : "") + ` --cursor ${nextCursor}
2209
+ `
2210
+ );
2211
+ }
2212
+ }
2213
+ async function runLetterMessages(options) {
2214
+ const config = loadConfig(options.token);
2215
+ const csoPlatformType = options.domestic ? 1 : 2;
2216
+ const params = new URLSearchParams({
2217
+ mediaCustomerId: options.account,
2218
+ userId: options.user,
2219
+ size: "20"
2220
+ });
2221
+ const url = `${config.csoBaseUrl}/cso/v1/im/messageRecord/${csoPlatformType}?${params}`;
2222
+ let res;
2223
+ try {
2224
+ res = await apiFetch2(url, config);
2225
+ } catch (e) {
2226
+ console.error(`
2227
+ \u274C \u83B7\u53D6\u6D88\u606F\u8BB0\u5F55\u5931\u8D25\uFF1A${e.message}`);
2228
+ process.exit(1);
2229
+ }
2230
+ if (res.code !== 1) {
2231
+ console.error("\n\u274C \u670D\u52A1\u7AEF\u8FD4\u56DE\u5F02\u5E38\uFF0C\u83B7\u53D6\u6D88\u606F\u8BB0\u5F55\u5931\u8D25");
2232
+ process.exit(1);
2233
+ }
2234
+ const messages = [...res.data?.list ?? []].reverse();
2235
+ if (messages.length === 0) {
2236
+ console.log("\n\u6682\u65E0\u6D88\u606F\u8BB0\u5F55\u3002");
2237
+ return;
2238
+ }
2239
+ console.log(`
2240
+ \u6D88\u606F\u8BB0\u5F55\uFF08\u5171 ${messages.length} \u6761\uFF0C\u65F6\u95F4\u7531\u65E7\u5230\u65B0\uFF09\uFF1A
2241
+ `);
2242
+ for (const m of messages) {
2243
+ const content = parseContent(m.content);
2244
+ const sender = m.isAuthor ? "\u3010\u6211\u65B9\u3011" : "\u3010\u7528\u6237\u3011";
2245
+ const timeStr = formatUtcTime(m.createdDateTime);
2246
+ const statusMark = m.status === 2 ? ` \u274C\u5931\u8D25: ${m.remark}` : m.status === 0 ? " \u23F3\u53D1\u9001\u4E2D" : "";
2247
+ console.log(` ${timeStr} ${sender} ${contentToText(content)}${statusMark}`);
2248
+ }
2249
+ const latestUserMsg = [...messages].reverse().find((m) => !m.isAuthor);
2250
+ const latestContent = latestUserMsg ? parseContent(latestUserMsg.content) : null;
2251
+ const hasContext = latestContent?.server_message_id || latestContent?.conversation_short_id;
2252
+ console.log("\n\u2500\u2500 \u56DE\u590D\u6240\u9700\u53C2\u6570\uFF08\u4F20\u7ED9 letter reply \u547D\u4EE4\uFF09\uFF1A\n");
2253
+ console.log(
2254
+ JSON.stringify(
2255
+ {
2256
+ userId: options.user,
2257
+ mediaCustomerId: options.account,
2258
+ // 抖音平台回复时需要携带这两个字段引用用户最新消息
2259
+ msg_id: latestContent?.server_message_id ?? null,
2260
+ conversation_id: latestContent?.conversation_short_id ?? null,
2261
+ _tip: hasContext ? "\u6296\u97F3\u5E73\u53F0\u56DE\u590D\u65F6\u8BF7\u643A\u5E26 --msg-id \u548C --conv-id \u53C2\u6570\uFF08\u503C\u89C1\u4E0A\u65B9\uFF09" : "\u65E0\u9700 msg_id / conv_id\uFF08\u6216\u4E3A\u975E\u6296\u97F3\u5E73\u53F0\uFF09"
2262
+ },
2263
+ null,
2264
+ 2
2265
+ )
2266
+ );
2267
+ }
2268
+ async function runLetterReply(options) {
2269
+ const config = loadConfig(options.token);
2270
+ const csoPlatformType = options.domestic ? 1 : 2;
2271
+ const scene = options.mediaType === "Douyin" ? "im_replay_msg" : null;
2272
+ const message = {
2273
+ message_type: "text",
2274
+ text: options.text
2275
+ };
2276
+ if (options.msgId) message["msg_id"] = options.msgId;
2277
+ if (options.convId) message["conversation_id"] = options.convId;
2278
+ const body = {
2279
+ mediaCustomerId: options.account,
2280
+ userId: options.user,
2281
+ message,
2282
+ messageChannel: 0,
2283
+ scene
2284
+ };
2285
+ const url = `${config.csoBaseUrl}/cso/v1/im/sendMessage/${csoPlatformType}/${options.mediaType}`;
2286
+ console.log(`
2287
+ \u{1F4E9} \u6B63\u5728\u5411\u7528\u6237 ${options.user} \u53D1\u9001\u79C1\u4FE1\u2026`);
2288
+ let res;
2289
+ try {
2290
+ res = await apiFetch2(url, config, {
2291
+ method: "POST",
2292
+ body: JSON.stringify(body)
2293
+ });
2294
+ } catch (e) {
2295
+ console.error(`
2296
+ \u274C \u8BF7\u6C42\u5931\u8D25\uFF1A${e.message}`);
2297
+ process.exit(1);
2298
+ }
2299
+ if (res.code !== 1) {
2300
+ console.error(`
2301
+ \u274C \u53D1\u9001\u5931\u8D25\uFF1A${res.message ?? JSON.stringify(res)}`);
2302
+ process.exit(1);
2303
+ }
2304
+ if (res.data?.status === 2) {
2305
+ console.error(
2306
+ "\n\u274C \u6D88\u606F\u88AB\u5E73\u53F0\u62D2\u7EDD\uFF08status=2\uFF09\u3002\n \u53EF\u80FD\u539F\u56E0\uFF1A\u7528\u6237\u6700\u540E\u4E00\u6761\u6D88\u606F\u5DF2\u8D85\u8FC7 24 \u5C0F\u65F6\uFF08\u5E73\u53F0\u9650\u5236\uFF09\uFF0C\u6216\u8D26\u53F7\u65E0\u79C1\u4FE1\u53D1\u9001\u6743\u9650\u3002\n \u8BF7\u68C0\u67E5\u7528\u6237\u6700\u8FD1\u6D88\u606F\u65F6\u95F4\u540E\u518D\u91CD\u8BD5\u3002"
2307
+ );
2308
+ process.exit(1);
2309
+ }
2310
+ console.log(`
2311
+ \u2705 \u79C1\u4FE1\u53D1\u9001\u6210\u529F\uFF01\u5185\u5BB9\uFF1A${options.text}`);
2312
+ try {
2313
+ await postFormData(
2314
+ `${config.csoBaseUrl}/cso/v1/im/setToRead/${csoPlatformType}/${options.mediaType}`,
2315
+ config,
2316
+ { mediaCustomerId: options.account, userId: options.user }
2317
+ );
2318
+ console.log(" \u4F1A\u8BDD\u5DF2\u81EA\u52A8\u6807\u8BB0\u4E3A\u5DF2\u8BFB\u3002");
2319
+ } catch {
2320
+ }
2321
+ }
2322
+ async function runLetterMarkRead(options) {
2323
+ const config = loadConfig(options.token);
2324
+ const csoPlatformType = options.domestic ? 1 : 2;
2325
+ const url = `${config.csoBaseUrl}/cso/v1/im/setToRead/${csoPlatformType}/${options.mediaType}`;
2326
+ try {
2327
+ await postFormData(url, config, {
2328
+ mediaCustomerId: options.account,
2329
+ userId: options.user
2330
+ });
2331
+ console.log(`
2332
+ \u2705 \u4F1A\u8BDD ${options.user} \u5DF2\u6807\u8BB0\u4E3A\u5DF2\u8BFB\u3002`);
2333
+ } catch (e) {
2334
+ console.error(`
2335
+ \u274C \u6807\u8BB0\u5DF2\u8BFB\u5931\u8D25\uFF1A${e.message}`);
2336
+ process.exit(1);
2337
+ }
2338
+ }
2339
+
2340
+ // src/index.ts
2341
+ var program = new Command();
2342
+ program.name("siluzan-cso").description("Siluzan \u5E73\u53F0 Skill \u5DE5\u5177\uFF1A\u521D\u59CB\u5316\u3001\u8D26\u53F7\u67E5\u8BE2\u3001\u4E0A\u4F20\u3001\u56FE\u6587/\u89C6\u9891\u53D1\u5E03").version(getCurrentVersion2());
2343
+ program.hook("preAction", () => {
2344
+ setSiluzanCliInvocation(redactCliArgvForSentry(process.argv));
2345
+ });
2346
+ var configCmd = program.command("config").description("\u67E5\u770B\u6216\u8BBE\u7F6E Siluzan \u8BA4\u8BC1\u914D\u7F6E\uFF08~/.siluzan/config.json\uFF09");
2347
+ configCmd.command("show").description("\u5C55\u793A\u5F53\u524D\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\uFF08Token \u8131\u654F\u663E\u793A\uFF09").action(() => cmdConfigShow());
2348
+ configCmd.command("set").description("\u4FDD\u5B58\u914D\u7F6E\u5230 ~/.siluzan/config.json\uFF0C\u540E\u7EED\u547D\u4EE4\u81EA\u52A8\u8BFB\u53D6").option("--api-key <key>", "API Key\uFF08\u63A8\u8350\uFF0C\u4E0E siluzan-tso \u5171\u7528\uFF0C\u4F18\u5148\u7EA7\u9AD8\u4E8E token\uFF09").option("-t, --token <token>", "\u7528\u6237 Auth Token\uFF08JWT\uFF09").option("--api-base <url>", "\u4E3B API \u57FA\u5730\u5740\uFF08\u9ED8\u8BA4 https://api.siluzan.com\uFF09").action((opts) => {
2349
+ cmdConfigSet({ apiKey: opts.apiKey, token: opts.token, apiBase: opts.apiBase });
2350
+ });
2351
+ configCmd.command("clear").description("\u6E05\u7A7A\u5DF2\u4FDD\u5B58\u7684 Token").action(() => cmdConfigClear());
2352
+ program.command("login").description("\u5F15\u5BFC\u5B8C\u6210 Siluzan \u8D26\u53F7\u767B\u5F55\uFF0C\u4FDD\u5B58 API Key \u6216 Token \u5230\u672C\u5730").option("--api-key <key>", "\u76F4\u63A5\u4FDD\u5B58 API Key\uFF08\u8DF3\u8FC7\u4EA4\u4E92\u5F0F\u6D41\u7A0B\uFF0C\u4E0E siluzan-tso \u5171\u7528\uFF09").action(async (opts) => {
2353
+ await runLogin({ apiKey: opts.apiKey });
2354
+ });
2355
+ program.command("update").description("\u68C0\u67E5\u5E76\u66F4\u65B0 siluzan-cso-cli \u81F3\u6700\u65B0\u7248\u672C\uFF0C\u540C\u6B65\u5237\u65B0\u6240\u6709\u5DF2\u5B89\u88C5\u7684 skill \u6587\u4EF6").option("--force", "\u8DF3\u8FC7\u7248\u672C\u6BD4\u8F83\uFF0C\u5F3A\u5236\u91CD\u65B0\u5B89\u88C5\u5E76\u5237\u65B0 skill \u6587\u4EF6", false).option("--skip-init", "\u4EC5\u66F4\u65B0 CLI\uFF0C\u4E0D\u91CD\u65B0\u521D\u59CB\u5316 skill \u6587\u4EF6", false).action(async (opts) => {
2356
+ await runUpdate({ force: opts.force, skipInit: opts.skipInit });
2357
+ });
2358
+ program.command("init").description("\u5C06 Skill \u6587\u4EF6\u5199\u5165\u6307\u5B9A AI \u52A9\u624B\u76EE\u5F55\uFF08--ai \u9009\u62E9\u9884\u8BBE\u5E73\u53F0\uFF0C\u6216 --dir \u6307\u5B9A\u4EFB\u610F\u76EE\u5F55\uFF09").option(
2359
+ "-a, --ai <targets>",
2360
+ "\u76EE\u6807\u5E73\u53F0\uFF0C\u9017\u53F7\u5206\u9694\uFF1Acursor,claude,openclaw-workspace,openclaw-global,workbuddy-workspace,workbuddy-global,all",
2361
+ "cursor"
2362
+ ).option(
2363
+ "-d, --dir <path>",
2364
+ "\u5C06 Skill \u6587\u4EF6\u76F4\u63A5\u5199\u5165\u6307\u5B9A\u76EE\u5F55\uFF08\u7EDD\u5BF9\u6216\u76F8\u5BF9\u8DEF\u5F84\uFF09\uFF0C\u4E0E --ai \u4E92\u65A5\uFF0C\u4F18\u5148\u7EA7\u66F4\u9AD8"
2365
+ ).option("-f, --force", "\u8986\u76D6\u5DF2\u5B58\u5728\u7684 Skill \u6587\u4EF6", false).option("--api-base <url>", "API \u57FA\u5730\u5740\uFF08\u5199\u5165\u914D\u7F6E\u63D0\u793A\uFF09", "https://api.siluzan.com").action(async (opts) => {
2366
+ await runInit({
2367
+ cwd: process.cwd(),
2368
+ aiTargets: opts.ai,
2369
+ dir: opts.dir,
2370
+ force: Boolean(opts.force),
2371
+ apiBaseUrl: opts.apiBase ?? "https://api.siluzan.com"
2372
+ });
2373
+ });
2374
+ program.command("list-accounts").description("\u5217\u51FA\u8D26\u53F7\u6811\u4E2D\u6240\u6709\u53EF\u7528\u5A92\u4F53\u8D26\u53F7\uFF0C\u8F93\u51FA JSON \u4F9B\u586B\u5199\u53D1\u5E03\u914D\u7F6E").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--domestic", "\u5185\u8D38\u6A21\u5F0F\uFF08mediaGroup=1\uFF09", false).option("--verbose", "\u663E\u793A\u5B8C\u6574\u4EE4\u724C\u6807\u8BC6\uFF08\u9ED8\u8BA4\u8131\u654F\uFF09", false).action(async (opts) => {
2375
+ await runListAccounts({ token: opts.token, domestic: opts.domestic, verbose: opts.verbose });
2376
+ });
2377
+ program.command("upload").description("\u4E0A\u4F20\u672C\u5730\u89C6\u9891\u6216\u56FE\u7247\u5230 Siluzan \u7D20\u6750\u5E93\uFF0C\u8F93\u51FA\u53EF\u76F4\u63A5\u586B\u5165 publish-config.json \u7684\u5B57\u6BB5").requiredOption("-f, --file <path>", "\u672C\u5730\u6587\u4EF6\u8DEF\u5F84").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--kind <video|image>", "\u624B\u52A8\u6307\u5B9A\u6587\u4EF6\u7C7B\u578B\uFF08\u9ED8\u8BA4\u6309\u6269\u5C55\u540D\u81EA\u52A8\u5224\u65AD\uFF09").option("--cover <path>", "\u5C01\u9762\u56FE\u7247\u8DEF\u5F84\uFF08\u4E0A\u4F20\u89C6\u9891\u65F6\u5FC5\u987B\u63D0\u4F9B\uFF09").option("--verbose", "\u663E\u793A\u8BE6\u7EC6\u9519\u8BEF\u4FE1\u606F\u53CA\u5B8C\u6574 URL\uFF08\u9ED8\u8BA4\u9690\u85CF\u542B\u7B7E\u540D URL\uFF09", false).action(async (opts) => {
2378
+ await runUpload({ file: opts.file, token: opts.token, kind: opts.kind, cover: opts.cover, verbose: opts.verbose });
2379
+ });
2380
+ program.command("extract-cover").description("\u4ECE\u89C6\u9891\u6587\u4EF6\u622A\u53D6\u5C01\u9762\u5E27\uFF0C\u6309\u5E73\u53F0\u89C4\u683C\uFF08YouTube/TikTok/Twitter \u7B49\uFF09\u88C1\u526A\u7F29\u653E\uFF0C\u53EF\u9009\u81EA\u52A8\u4E0A\u4F20").requiredOption("-f, --file <path>", "\u89C6\u9891\u6587\u4EF6\u8DEF\u5F84\uFF08MP4/MOV/MKV \u7B49\uFF09").option(
2381
+ "-t, --time <seconds...>",
2382
+ "\u622A\u5E27\u65F6\u95F4\u70B9\uFF08\u79D2\uFF0C\u53EF\u591A\u4E2A\uFF09\uFF0C\u5982 -t 0 3 10 \u622A\u53D6\u7B2C 0\u30013\u300110 \u79D2\u5E27\uFF08\u9ED8\u8BA4: 0\uFF09"
2383
+ ).option(
2384
+ "-p, --platform <platform>",
2385
+ "\u76EE\u6807\u5E73\u53F0\uFF08\u9ED8\u8BA4 youtube\uFF09\u3002\u8FD0\u884C --list-platforms \u67E5\u770B\u5168\u90E8\u652F\u6301\u5E73\u53F0",
2386
+ "youtube"
2387
+ ).option("-o, --output <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84\uFF08\u591A\u5E27\u65F6\u4F5C\u4E3A\u6587\u4EF6\u540D\u524D\u7F00\uFF1B\u9ED8\u8BA4\u4FDD\u5B58\u5728\u89C6\u9891\u540C\u76EE\u5F55\uFF09").option("--upload", "\u622A\u53D6\u540E\u81EA\u52A8\u4E0A\u4F20\u5C01\u9762\u5230\u7D20\u6750\u5E93\uFF0C\u5E76\u8F93\u51FA cover JSON \u7247\u6BB5", false).option("--list-platforms", "\u5217\u51FA\u6240\u6709\u652F\u6301\u7684\u5E73\u53F0\u5C01\u9762\u89C4\u683C", false).option("--token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").action(async (opts) => {
2388
+ if (opts.listPlatforms) {
2389
+ const { PLATFORM_COVER_SPECS } = await import("./extract-cover-NFKUUFLS.js");
2390
+ const keys = Object.keys(PLATFORM_COVER_SPECS);
2391
+ console.log("\n\u5404\u5E73\u53F0\u89C6\u9891\u5C01\u9762\u89C4\u683C\uFF1A\n");
2392
+ const maxKeyLen = Math.max(...keys.map((k) => k.length));
2393
+ const maxNameLen = Math.max(...keys.map((k) => PLATFORM_COVER_SPECS[k].displayName.length));
2394
+ console.log(` ${"\u5E73\u53F0 ID".padEnd(maxKeyLen)} ${"\u5E73\u53F0\u540D\u79F0".padEnd(maxNameLen)} ${"\u5C3A\u5BF8".padEnd(12)} ${"\u6BD4\u4F8B".padEnd(6)} \u8BF4\u660E`);
2395
+ console.log(" " + "-".repeat(maxKeyLen + maxNameLen + 55));
2396
+ for (const key of keys) {
2397
+ const spec = PLATFORM_COVER_SPECS[key];
2398
+ const size = `${spec.width}\xD7${spec.height}`;
2399
+ console.log(` ${key.padEnd(maxKeyLen)} ${spec.displayName.padEnd(maxNameLen)} ${size.padEnd(12)} ${spec.ratio.padEnd(6)} ${spec.note}`);
2400
+ }
2401
+ console.log();
2402
+ return;
2403
+ }
2404
+ await runExtractCover({
2405
+ file: opts.file,
2406
+ time: opts.time,
2407
+ platform: opts.platform,
2408
+ output: opts.output,
2409
+ upload: opts.upload,
2410
+ token: opts.token
2411
+ });
2412
+ });
2413
+ program.command("publish").description("\u6309 JSON \u914D\u7F6E\u6587\u4EF6\u63D0\u4EA4\u53D1\u5E03\u4EFB\u52A1\u5230 CSO").requiredOption("-c, --config <path>", "\u53D1\u5E03\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF08JSON\uFF09").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--dry-run", "\u4EC5\u9884\u89C8\u8BF7\u6C42\u4F53\uFF0C\u4E0D\u5B9E\u9645\u63D0\u4EA4", false).option("--verbose", "\u663E\u793A\u5B8C\u6574\u8BF7\u6C42\u4F53\u53CA\u8BE6\u7EC6\u9519\u8BEF\uFF08\u542B\u654F\u611F\u5B57\u6BB5\uFF0C\u9ED8\u8BA4\u8131\u654F\uFF09", false).action(async (opts) => {
2414
+ await runPublish({ config: opts.config, token: opts.token, dryRun: opts.dryRun, verbose: opts.verbose });
2415
+ });
2416
+ var letterCmd = program.command("letter").description("\u79C1\u4FE1\u7BA1\u7406\uFF1A\u67E5\u8BE2\u4F1A\u8BDD\u5217\u8868\u3001\u8BFB\u53D6\u6D88\u606F\u8BB0\u5F55\u3001\u53D1\u9001\u56DE\u590D");
2417
+ letterCmd.command("sessions").description("\u5217\u51FA\u6307\u5B9A\u8D26\u53F7\u7684\u79C1\u4FE1\u4F1A\u8BDD\uFF08\u9ED8\u8BA4\u5168\u90E8\uFF0C--unread-only \u4EC5\u663E\u793A\u672A\u8BFB\u5E76\u81EA\u52A8\u7FFB\u9875\uFF09").requiredOption("-a, --account <mediaCustomerId>", "\u4F01\u4E1A\u5A92\u4F53\u8D26\u53F7 ID\uFF08\u6765\u81EA list-accounts \u7684 mediaCustomerId\uFF09").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--domestic", "\u5185\u8D38\u6A21\u5F0F\uFF08\u6296\u97F3\uFF0CcsoPlatformType=1\uFF09", false).option("--unread-only", "\u4EC5\u663E\u793A\u6709\u672A\u8BFB\u6D88\u606F\u7684\u4F1A\u8BDD\uFF08\u81EA\u52A8\u7FFB\u9875\u805A\u5408\u5168\u90E8\u672A\u8BFB\uFF0C\u4E0A\u9650 500 \u6761\uFF09", false).option("--cursor <cursor>", "\u4ECE\u6307\u5B9A\u6E38\u6807\u4F4D\u7F6E\u7EE7\u7EED\u7FFB\u9875\uFF08\u6765\u81EA\u4E0A\u6B21\u8F93\u51FA\u7684 nextCursor \u503C\uFF09").option("--batch-size <n>", "\u5C06\u7ED3\u679C\u5206\u6279\u8F93\u51FA\uFF0C\u6BCF\u6279 n \u4E2A\u4F1A\u8BDD\uFF08AI \u5E94\u9010\u6279\u5904\u7406\uFF0C\u5EFA\u8BAE 20-30\uFF09", parseInt).action(async (opts) => {
2418
+ await runLetterSessions({
2419
+ account: opts.account,
2420
+ token: opts.token,
2421
+ domestic: opts.domestic,
2422
+ unreadOnly: opts.unreadOnly,
2423
+ cursor: opts.cursor,
2424
+ batchSize: opts.batchSize
2425
+ });
2426
+ });
2427
+ letterCmd.command("messages").description("\u67E5\u770B\u4E0E\u6307\u5B9A\u7528\u6237\u7684\u79C1\u4FE1\u8BB0\u5F55\uFF08\u6700\u65B0 20 \u6761\uFF0C\u4ECE\u65E7\u5230\u65B0\uFF09").requiredOption("-a, --account <mediaCustomerId>", "\u4F01\u4E1A\u5A92\u4F53\u8D26\u53F7 ID").requiredOption("-u, --user <userId>", "\u76EE\u6807\u7528\u6237 ID\uFF08\u6765\u81EA letter sessions \u8F93\u51FA\uFF09").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--domestic", "\u5185\u8D38\u6A21\u5F0F", false).action(async (opts) => {
2428
+ await runLetterMessages({
2429
+ account: opts.account,
2430
+ user: opts.user,
2431
+ token: opts.token,
2432
+ domestic: opts.domestic
2433
+ });
2434
+ });
2435
+ letterCmd.command("reply").description("\u5411\u6307\u5B9A\u7528\u6237\u53D1\u9001\u79C1\u4FE1\u56DE\u590D\uFF08\u7EAF\u6587\u5B57\uFF09\uFF0C\u6210\u529F\u540E\u81EA\u52A8\u6807\u8BB0\u4F1A\u8BDD\u5DF2\u8BFB").requiredOption("-a, --account <mediaCustomerId>", "\u4F01\u4E1A\u5A92\u4F53\u8D26\u53F7 ID").requiredOption("-u, --user <userId>", "\u76EE\u6807\u7528\u6237 ID").requiredOption("-m, --media-type <type>", "\u5E73\u53F0\u7C7B\u578B\uFF1ADouyin | Facebook | Instagram\uFF08\u6765\u81EA list-accounts \u7684 mediaAccountType\uFF09").requiredOption("--text <content>", "\u56DE\u590D\u5185\u5BB9\uFF08\u7EAF\u6587\u5B57\uFF09").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--domestic", "\u5185\u8D38\u6A21\u5F0F", false).option("--msg-id <msgId>", "\u7528\u6237\u6700\u65B0\u6D88\u606F\u7684 server_message_id\uFF08\u6296\u97F3\u5E73\u53F0\u5EFA\u8BAE\u63D0\u4F9B\uFF0C\u7528\u4E8E\u5173\u8054\u56DE\u590D\u4E0A\u4E0B\u6587\uFF09").option("--conv-id <convId>", "\u7528\u6237\u6700\u65B0\u6D88\u606F\u7684 conversation_short_id\uFF08\u6296\u97F3\u5E73\u53F0\u5EFA\u8BAE\u63D0\u4F9B\uFF09").action(async (opts) => {
2436
+ await runLetterReply({
2437
+ account: opts.account,
2438
+ user: opts.user,
2439
+ mediaType: opts.mediaType,
2440
+ text: opts.text,
2441
+ token: opts.token,
2442
+ domestic: opts.domestic,
2443
+ msgId: opts.msgId,
2444
+ convId: opts.convId
2445
+ });
2446
+ });
2447
+ letterCmd.command("mark-read").description("\u5C06\u6307\u5B9A\u7528\u6237\u7684\u4F1A\u8BDD\u6807\u8BB0\u4E3A\u5DF2\u8BFB\uFF08reply \u547D\u4EE4\u5DF2\u81EA\u52A8\u6267\u884C\uFF0C\u901A\u5E38\u65E0\u9700\u5355\u72EC\u8C03\u7528\uFF09").requiredOption("-a, --account <mediaCustomerId>", "\u4F01\u4E1A\u5A92\u4F53\u8D26\u53F7 ID").requiredOption("-u, --user <userId>", "\u76EE\u6807\u7528\u6237 ID").requiredOption("-m, --media-type <type>", "\u5E73\u53F0\u7C7B\u578B\uFF1ADouyin | Facebook | Instagram").option("-t, --token <token>", "Token\uFF08\u53EF\u9009\uFF1B\u4F18\u5148\u4E8E ~/.siluzan/config.json\uFF09").option("--domestic", "\u5185\u8D38\u6A21\u5F0F", false).action(async (opts) => {
2448
+ await runLetterMarkRead({
2449
+ account: opts.account,
2450
+ user: opts.user,
2451
+ mediaType: opts.mediaType,
2452
+ token: opts.token,
2453
+ domestic: opts.domestic
2454
+ });
2455
+ });
2456
+ program.parse();
2457
+ var activeCmd = process.argv[2];
2458
+ if (activeCmd !== "update" && activeCmd !== "login") {
2459
+ notifyIfOutdated().catch(() => {
2460
+ });
2461
+ }