ccjk 2.0.7 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +6 -6
- package/README.ko.md +5 -5
- package/README.md +7 -7
- package/README.zh-CN.md +9 -9
- package/dist/chunks/simple-config.mjs +5 -3
- package/dist/cli.mjs +1246 -8
- package/dist/i18n/locales/en/notification.json +137 -0
- package/dist/i18n/locales/zh-CN/notification.json +137 -0
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import cac from 'cac';
|
|
3
3
|
import ansis from 'ansis';
|
|
4
|
-
import { S as STATUS, ch as i18n, n as runConfigWizard, ci as testApiConnection, m as displayCurrentStatus, cj as quickSetup, ck as format, k as COLORS, cl as getAllPresets, cm as ensureI18nInitialized, cn as readCcrConfig, co as isCcrInstalled, cp as installCcr, cq as configureCcrFeature, cr as promptBoolean, cs as handleExitPromptError, ct as handleGeneralError, cu as COMETIX_COMMAND_NAME, cv as COMETIX_COMMANDS, cw as installCometixLine, cx as addNumbersToChoices, cy as checkAndUpdateTools, aW as runCodexUpdate, cz as resolveCodeType$1, a2 as resolveCodeToolType$1, cA as readZcfConfig, $ as isCodeToolType, W as DEFAULT_CODE_TOOL_TYPE, a_ as switchCodexProvider, aM as listCodexProviders, aR as readCodexConfig, aD as switchToOfficialLogin, a$ as switchToProvider, q as commandExists, y as CLAUDE_DIR, z as SETTINGS_FILE, cB as readJsonConfig, cC as writeJsonConfig, cD as moveToTrash, U as ZCF_CONFIG_FILE, ah as displayBanner, cE as updateZcfConfig, v as version, cF as resolveAiOutputLanguage, cG as updatePromptOnly, cH as selectAndInstallWorkflows, cI as checkClaudeCodeVersionAndPrompt, l as init, ai as displayBannerWithInfo, X as CODE_TOOL_BANNERS, aV as runCodexUninstall, b5 as configureCodexMcp, aE as configureCodexApi, aY as runCodexWorkflowImportWithLanguageSelection, aT as runCodexFullInit, cJ as checkSuperpowersInstalled, cK as getSuperpowersSkills, cL as updateSuperpowers, cM as uninstallSuperpowers, cN as installSuperpowers, cO as installSuperpowersViaGit, _ as CODE_TOOL_INFO, bb as getToolStatus, bc as getAllToolsStatus, V as CODE_TOOL_TYPES, aj as boxify, be as installTool, cP as readZcfConfigAsync, cQ as initI18n, bV as quickSync, cf as checkAllVersions, cg as upgradeAll, i as detectAllConfigs, bB as displayConfigScan, c9 as displayPermissions, cR as selectScriptLanguage, cS as changeLanguage, bU as runOnboarding } from './chunks/simple-config.mjs';
|
|
5
|
-
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
6
|
-
import { homedir } from 'node:os';
|
|
4
|
+
import { S as STATUS, ch as i18n, n as runConfigWizard$1, ci as testApiConnection, m as displayCurrentStatus, cj as quickSetup, ck as format, k as COLORS, cl as getAllPresets, cm as ensureI18nInitialized, cn as readCcrConfig, co as isCcrInstalled, cp as installCcr, cq as configureCcrFeature, cr as promptBoolean, cs as handleExitPromptError, ct as handleGeneralError, cu as COMETIX_COMMAND_NAME, cv as COMETIX_COMMANDS, cw as installCometixLine, cx as addNumbersToChoices, cy as checkAndUpdateTools, aW as runCodexUpdate, cz as resolveCodeType$1, a2 as resolveCodeToolType$1, cA as readZcfConfig, $ as isCodeToolType, W as DEFAULT_CODE_TOOL_TYPE, a_ as switchCodexProvider, aM as listCodexProviders, aR as readCodexConfig, aD as switchToOfficialLogin, a$ as switchToProvider, q as commandExists, y as CLAUDE_DIR, z as SETTINGS_FILE, cB as readJsonConfig, cC as writeJsonConfig, cD as moveToTrash, U as ZCF_CONFIG_FILE, ah as displayBanner, cE as updateZcfConfig, v as version, cF as resolveAiOutputLanguage, cG as updatePromptOnly, cH as selectAndInstallWorkflows, cI as checkClaudeCodeVersionAndPrompt, l as init, ai as displayBannerWithInfo, X as CODE_TOOL_BANNERS, aV as runCodexUninstall, b5 as configureCodexMcp, aE as configureCodexApi, aY as runCodexWorkflowImportWithLanguageSelection, aT as runCodexFullInit, cJ as checkSuperpowersInstalled, cK as getSuperpowersSkills, cL as updateSuperpowers, cM as uninstallSuperpowers, cN as installSuperpowers, cO as installSuperpowersViaGit, _ as CODE_TOOL_INFO, bb as getToolStatus, bc as getAllToolsStatus, V as CODE_TOOL_TYPES, aj as boxify, be as installTool, cP as readZcfConfigAsync, cQ as initI18n, bV as quickSync, cf as checkAllVersions, cg as upgradeAll, i as detectAllConfigs, bB as displayConfigScan, c9 as displayPermissions, cR as selectScriptLanguage, cS as changeLanguage, bU as runOnboarding } from './chunks/simple-config.mjs';
|
|
5
|
+
import nodeFs__default, { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
6
|
+
import os, { homedir } from 'node:os';
|
|
7
7
|
import inquirer from 'inquirer';
|
|
8
8
|
import { join, resolve as resolve$1 } from 'pathe';
|
|
9
9
|
import { runCcrStop, runCcrStart, runCcrRestart, runCcrStatus, runCcrUi } from './chunks/commands.mjs';
|
|
@@ -16,8 +16,10 @@ import { spawn, exec } from 'node:child_process';
|
|
|
16
16
|
import { promisify } from 'node:util';
|
|
17
17
|
import { ClaudeCodeConfigManager } from './chunks/claude-code-config-manager.mjs';
|
|
18
18
|
import { pathExists } from 'fs-extra';
|
|
19
|
-
import { join as join$1, resolve, dirname } from 'node:path';
|
|
20
|
-
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import path, { join as join$1, resolve, dirname } from 'node:path';
|
|
20
|
+
import crypto, { randomUUID } from 'node:crypto';
|
|
21
|
+
import { parse, stringify } from '@iarna/toml';
|
|
22
|
+
import { Buffer } from 'node:buffer';
|
|
21
23
|
import ora from 'ora';
|
|
22
24
|
import 'smol-toml';
|
|
23
25
|
import 'dayjs';
|
|
@@ -65,14 +67,14 @@ function setupApi(providerId, apiKey, _lang = "en") {
|
|
|
65
67
|
console.log(STATUS.error(format(i18n.t("api:configFailed"), { error: result.error || "Unknown error" })));
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
|
-
function showStatus(lang = "en") {
|
|
70
|
+
function showStatus$1(lang = "en") {
|
|
69
71
|
displayCurrentStatus(lang);
|
|
70
72
|
}
|
|
71
73
|
async function testApi(lang = "en") {
|
|
72
74
|
await testApiConnection(lang);
|
|
73
75
|
}
|
|
74
76
|
async function runWizard(lang = "en") {
|
|
75
|
-
await runConfigWizard(lang);
|
|
77
|
+
await runConfigWizard$1(lang);
|
|
76
78
|
}
|
|
77
79
|
async function apiCommand(action = "wizard", args = [], options = {}) {
|
|
78
80
|
const lang = options.lang || "en";
|
|
@@ -95,7 +97,7 @@ async function apiCommand(action = "wizard", args = [], options = {}) {
|
|
|
95
97
|
break;
|
|
96
98
|
case "status":
|
|
97
99
|
case "s":
|
|
98
|
-
showStatus(lang);
|
|
100
|
+
showStatus$1(lang);
|
|
99
101
|
break;
|
|
100
102
|
case "test":
|
|
101
103
|
case "t":
|
|
@@ -6309,6 +6311,1239 @@ async function mcpMarket(action, args, options = {}) {
|
|
|
6309
6311
|
}
|
|
6310
6312
|
}
|
|
6311
6313
|
|
|
6314
|
+
const TOKEN_PREFIX = "ccjk_";
|
|
6315
|
+
const TOKEN_LENGTH = 64;
|
|
6316
|
+
const TOKEN_VERSION = 1;
|
|
6317
|
+
function generateDeviceToken() {
|
|
6318
|
+
const randomBytes = crypto.randomBytes(TOKEN_LENGTH / 2);
|
|
6319
|
+
const randomHex = randomBytes.toString("hex");
|
|
6320
|
+
return `${TOKEN_PREFIX}${TOKEN_VERSION}${randomHex}`;
|
|
6321
|
+
}
|
|
6322
|
+
function isValidTokenFormat(token) {
|
|
6323
|
+
if (!token || typeof token !== "string") {
|
|
6324
|
+
return false;
|
|
6325
|
+
}
|
|
6326
|
+
if (!token.startsWith(TOKEN_PREFIX)) {
|
|
6327
|
+
return false;
|
|
6328
|
+
}
|
|
6329
|
+
const expectedLength = TOKEN_PREFIX.length + 1 + TOKEN_LENGTH;
|
|
6330
|
+
if (token.length !== expectedLength) {
|
|
6331
|
+
return false;
|
|
6332
|
+
}
|
|
6333
|
+
const version = token[TOKEN_PREFIX.length];
|
|
6334
|
+
if (!/^\d$/.test(version)) {
|
|
6335
|
+
return false;
|
|
6336
|
+
}
|
|
6337
|
+
const hexPart = token.slice(TOKEN_PREFIX.length + 1);
|
|
6338
|
+
if (!/^[a-f0-9]+$/i.test(hexPart)) {
|
|
6339
|
+
return false;
|
|
6340
|
+
}
|
|
6341
|
+
return true;
|
|
6342
|
+
}
|
|
6343
|
+
function getDeviceInfo() {
|
|
6344
|
+
return {
|
|
6345
|
+
name: os.hostname(),
|
|
6346
|
+
platform: os.platform(),
|
|
6347
|
+
osVersion: os.release(),
|
|
6348
|
+
arch: os.arch(),
|
|
6349
|
+
username: os.userInfo().username,
|
|
6350
|
+
machineId: generateMachineId()
|
|
6351
|
+
};
|
|
6352
|
+
}
|
|
6353
|
+
function generateMachineId() {
|
|
6354
|
+
const components = [
|
|
6355
|
+
os.hostname(),
|
|
6356
|
+
os.platform(),
|
|
6357
|
+
os.arch(),
|
|
6358
|
+
os.cpus()[0]?.model || "unknown",
|
|
6359
|
+
os.userInfo().username,
|
|
6360
|
+
// Add network interface MAC addresses for uniqueness
|
|
6361
|
+
...Object.values(os.networkInterfaces()).flat().filter((iface) => iface && !iface.internal && iface.mac !== "00:00:00:00:00:00").map((iface) => iface?.mac).filter(Boolean).slice(0, 3)
|
|
6362
|
+
// Limit to first 3 MACs
|
|
6363
|
+
];
|
|
6364
|
+
const combined = components.join("|");
|
|
6365
|
+
return crypto.createHash("sha256").update(combined).digest("hex").slice(0, 32);
|
|
6366
|
+
}
|
|
6367
|
+
function deriveEncryptionKey() {
|
|
6368
|
+
const machineId = generateMachineId();
|
|
6369
|
+
const salt = "ccjk-notification-token-v1";
|
|
6370
|
+
return crypto.pbkdf2Sync(machineId, salt, 1e5, 32, "sha256");
|
|
6371
|
+
}
|
|
6372
|
+
function encryptToken(token) {
|
|
6373
|
+
const key = deriveEncryptionKey();
|
|
6374
|
+
const iv = crypto.randomBytes(16);
|
|
6375
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
6376
|
+
let encrypted = cipher.update(token, "utf8", "hex");
|
|
6377
|
+
encrypted += cipher.final("hex");
|
|
6378
|
+
const authTag = cipher.getAuthTag();
|
|
6379
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
6380
|
+
}
|
|
6381
|
+
function decryptToken(encryptedToken) {
|
|
6382
|
+
try {
|
|
6383
|
+
const parts = encryptedToken.split(":");
|
|
6384
|
+
if (parts.length !== 3) {
|
|
6385
|
+
return null;
|
|
6386
|
+
}
|
|
6387
|
+
const [ivHex, authTagHex, encrypted] = parts;
|
|
6388
|
+
const key = deriveEncryptionKey();
|
|
6389
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
6390
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
6391
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
6392
|
+
decipher.setAuthTag(authTag);
|
|
6393
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
6394
|
+
decrypted += decipher.final("utf8");
|
|
6395
|
+
return decrypted;
|
|
6396
|
+
} catch {
|
|
6397
|
+
return null;
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
function maskToken(token) {
|
|
6401
|
+
if (!token || token.length < 12) {
|
|
6402
|
+
return "***";
|
|
6403
|
+
}
|
|
6404
|
+
const prefix = token.slice(0, TOKEN_PREFIX.length + 1);
|
|
6405
|
+
const suffix = token.slice(-4);
|
|
6406
|
+
return `${prefix}***...***${suffix}`;
|
|
6407
|
+
}
|
|
6408
|
+
|
|
6409
|
+
const DEFAULT_NOTIFICATION_CONFIG = {
|
|
6410
|
+
enabled: false,
|
|
6411
|
+
deviceToken: "",
|
|
6412
|
+
threshold: 10,
|
|
6413
|
+
// 10 minutes
|
|
6414
|
+
cloudEndpoint: "https://api.claudehome.cn",
|
|
6415
|
+
channels: {},
|
|
6416
|
+
quietHours: {
|
|
6417
|
+
enabled: false,
|
|
6418
|
+
startHour: 22,
|
|
6419
|
+
endHour: 8
|
|
6420
|
+
}
|
|
6421
|
+
};
|
|
6422
|
+
function validateNotificationConfig(config) {
|
|
6423
|
+
const errors = [];
|
|
6424
|
+
const warnings = [];
|
|
6425
|
+
if (config.threshold !== void 0) {
|
|
6426
|
+
if (config.threshold < 1) {
|
|
6427
|
+
errors.push({
|
|
6428
|
+
field: "threshold",
|
|
6429
|
+
message: "Threshold must be at least 1 minute",
|
|
6430
|
+
code: "THRESHOLD_TOO_LOW"
|
|
6431
|
+
});
|
|
6432
|
+
}
|
|
6433
|
+
if (config.threshold > 1440) {
|
|
6434
|
+
warnings.push("Threshold is set to more than 24 hours");
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
if (config.channels?.feishu?.enabled) {
|
|
6438
|
+
if (!config.channels.feishu.webhookUrl) {
|
|
6439
|
+
errors.push({
|
|
6440
|
+
field: "channels.feishu.webhookUrl",
|
|
6441
|
+
message: "Feishu webhook URL is required when enabled",
|
|
6442
|
+
code: "FEISHU_WEBHOOK_REQUIRED"
|
|
6443
|
+
});
|
|
6444
|
+
} else if (!config.channels.feishu.webhookUrl.startsWith("https://")) {
|
|
6445
|
+
errors.push({
|
|
6446
|
+
field: "channels.feishu.webhookUrl",
|
|
6447
|
+
message: "Feishu webhook URL must use HTTPS",
|
|
6448
|
+
code: "FEISHU_WEBHOOK_INVALID"
|
|
6449
|
+
});
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
if (config.channels?.wechat?.enabled) {
|
|
6453
|
+
if (!config.channels.wechat.corpId) {
|
|
6454
|
+
errors.push({
|
|
6455
|
+
field: "channels.wechat.corpId",
|
|
6456
|
+
message: "WeChat Work Corp ID is required when enabled",
|
|
6457
|
+
code: "WECHAT_CORPID_REQUIRED"
|
|
6458
|
+
});
|
|
6459
|
+
}
|
|
6460
|
+
if (!config.channels.wechat.agentId) {
|
|
6461
|
+
errors.push({
|
|
6462
|
+
field: "channels.wechat.agentId",
|
|
6463
|
+
message: "WeChat Work Agent ID is required when enabled",
|
|
6464
|
+
code: "WECHAT_AGENTID_REQUIRED"
|
|
6465
|
+
});
|
|
6466
|
+
}
|
|
6467
|
+
if (!config.channels.wechat.secret) {
|
|
6468
|
+
errors.push({
|
|
6469
|
+
field: "channels.wechat.secret",
|
|
6470
|
+
message: "WeChat Work Secret is required when enabled",
|
|
6471
|
+
code: "WECHAT_SECRET_REQUIRED"
|
|
6472
|
+
});
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
if (config.channels?.email?.enabled) {
|
|
6476
|
+
if (!config.channels.email.address) {
|
|
6477
|
+
errors.push({
|
|
6478
|
+
field: "channels.email.address",
|
|
6479
|
+
message: "Email address is required when enabled",
|
|
6480
|
+
code: "EMAIL_ADDRESS_REQUIRED"
|
|
6481
|
+
});
|
|
6482
|
+
} else if (!/^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/.test(config.channels.email.address)) {
|
|
6483
|
+
errors.push({
|
|
6484
|
+
field: "channels.email.address",
|
|
6485
|
+
message: "Invalid email address format",
|
|
6486
|
+
code: "EMAIL_ADDRESS_INVALID"
|
|
6487
|
+
});
|
|
6488
|
+
}
|
|
6489
|
+
}
|
|
6490
|
+
if (config.channels?.sms?.enabled) {
|
|
6491
|
+
if (!config.channels.sms.phone) {
|
|
6492
|
+
errors.push({
|
|
6493
|
+
field: "channels.sms.phone",
|
|
6494
|
+
message: "Phone number is required when enabled",
|
|
6495
|
+
code: "SMS_PHONE_REQUIRED"
|
|
6496
|
+
});
|
|
6497
|
+
} else if (!/^\d{10,15}$/.test(config.channels.sms.phone.replace(/\D/g, ""))) {
|
|
6498
|
+
errors.push({
|
|
6499
|
+
field: "channels.sms.phone",
|
|
6500
|
+
message: "Invalid phone number format",
|
|
6501
|
+
code: "SMS_PHONE_INVALID"
|
|
6502
|
+
});
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
if (config.quietHours?.enabled) {
|
|
6506
|
+
if (config.quietHours.startHour < 0 || config.quietHours.startHour > 23) {
|
|
6507
|
+
errors.push({
|
|
6508
|
+
field: "quietHours.startHour",
|
|
6509
|
+
message: "Start hour must be between 0 and 23",
|
|
6510
|
+
code: "QUIET_HOURS_INVALID"
|
|
6511
|
+
});
|
|
6512
|
+
}
|
|
6513
|
+
if (config.quietHours.endHour < 0 || config.quietHours.endHour > 23) {
|
|
6514
|
+
errors.push({
|
|
6515
|
+
field: "quietHours.endHour",
|
|
6516
|
+
message: "End hour must be between 0 and 23",
|
|
6517
|
+
code: "QUIET_HOURS_INVALID"
|
|
6518
|
+
});
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
return {
|
|
6522
|
+
valid: errors.length === 0,
|
|
6523
|
+
errors,
|
|
6524
|
+
warnings
|
|
6525
|
+
};
|
|
6526
|
+
}
|
|
6527
|
+
|
|
6528
|
+
const CCJK_CONFIG_DIR = path.join(os.homedir(), ".ccjk");
|
|
6529
|
+
const CONFIG_FILE_PATH = path.join(CCJK_CONFIG_DIR, "config.toml");
|
|
6530
|
+
const SECRETS_FILE_PATH = path.join(CCJK_CONFIG_DIR, ".notification-secrets");
|
|
6531
|
+
async function loadNotificationConfig() {
|
|
6532
|
+
try {
|
|
6533
|
+
if (!nodeFs__default.existsSync(CCJK_CONFIG_DIR)) {
|
|
6534
|
+
nodeFs__default.mkdirSync(CCJK_CONFIG_DIR, { recursive: true });
|
|
6535
|
+
}
|
|
6536
|
+
if (!nodeFs__default.existsSync(CONFIG_FILE_PATH)) {
|
|
6537
|
+
return { ...DEFAULT_NOTIFICATION_CONFIG };
|
|
6538
|
+
}
|
|
6539
|
+
const configContent = nodeFs__default.readFileSync(CONFIG_FILE_PATH, "utf-8");
|
|
6540
|
+
const config = parse(configContent);
|
|
6541
|
+
const notificationConfig = config.notification;
|
|
6542
|
+
if (!notificationConfig) {
|
|
6543
|
+
return { ...DEFAULT_NOTIFICATION_CONFIG };
|
|
6544
|
+
}
|
|
6545
|
+
const deviceToken = await loadDeviceToken();
|
|
6546
|
+
const quietHoursConfig = notificationConfig.quietHours || {};
|
|
6547
|
+
const defaultQuietHours = DEFAULT_NOTIFICATION_CONFIG.quietHours || {
|
|
6548
|
+
enabled: false,
|
|
6549
|
+
startHour: 22,
|
|
6550
|
+
endHour: 8,
|
|
6551
|
+
timezone: "local"
|
|
6552
|
+
};
|
|
6553
|
+
return {
|
|
6554
|
+
...DEFAULT_NOTIFICATION_CONFIG,
|
|
6555
|
+
...notificationConfig,
|
|
6556
|
+
deviceToken: deviceToken || notificationConfig.deviceToken || "",
|
|
6557
|
+
channels: {
|
|
6558
|
+
...DEFAULT_NOTIFICATION_CONFIG.channels,
|
|
6559
|
+
...notificationConfig.channels
|
|
6560
|
+
},
|
|
6561
|
+
quietHours: {
|
|
6562
|
+
enabled: quietHoursConfig.enabled ?? defaultQuietHours.enabled,
|
|
6563
|
+
startHour: quietHoursConfig.startHour ?? defaultQuietHours.startHour,
|
|
6564
|
+
endHour: quietHoursConfig.endHour ?? defaultQuietHours.endHour,
|
|
6565
|
+
timezone: quietHoursConfig.timezone ?? defaultQuietHours.timezone
|
|
6566
|
+
}
|
|
6567
|
+
};
|
|
6568
|
+
} catch (error) {
|
|
6569
|
+
console.error("Failed to load notification config:", error);
|
|
6570
|
+
return { ...DEFAULT_NOTIFICATION_CONFIG };
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
async function loadDeviceToken() {
|
|
6574
|
+
try {
|
|
6575
|
+
if (!nodeFs__default.existsSync(SECRETS_FILE_PATH)) {
|
|
6576
|
+
return null;
|
|
6577
|
+
}
|
|
6578
|
+
const secretsContent = nodeFs__default.readFileSync(SECRETS_FILE_PATH, "utf-8");
|
|
6579
|
+
const secrets = JSON.parse(secretsContent);
|
|
6580
|
+
if (!secrets.deviceToken) {
|
|
6581
|
+
return null;
|
|
6582
|
+
}
|
|
6583
|
+
const decryptedToken = decryptToken(secrets.deviceToken);
|
|
6584
|
+
return decryptedToken;
|
|
6585
|
+
} catch {
|
|
6586
|
+
return null;
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
async function saveNotificationConfig(config) {
|
|
6590
|
+
try {
|
|
6591
|
+
if (!nodeFs__default.existsSync(CCJK_CONFIG_DIR)) {
|
|
6592
|
+
nodeFs__default.mkdirSync(CCJK_CONFIG_DIR, { recursive: true });
|
|
6593
|
+
}
|
|
6594
|
+
let existingConfig = {};
|
|
6595
|
+
if (nodeFs__default.existsSync(CONFIG_FILE_PATH)) {
|
|
6596
|
+
const configContent = nodeFs__default.readFileSync(CONFIG_FILE_PATH, "utf-8");
|
|
6597
|
+
existingConfig = parse(configContent);
|
|
6598
|
+
}
|
|
6599
|
+
const notificationConfig = { ...config };
|
|
6600
|
+
if (notificationConfig.deviceToken) {
|
|
6601
|
+
await saveDeviceToken(notificationConfig.deviceToken);
|
|
6602
|
+
delete notificationConfig.deviceToken;
|
|
6603
|
+
}
|
|
6604
|
+
existingConfig.notification = {
|
|
6605
|
+
...existingConfig.notification || {},
|
|
6606
|
+
...notificationConfig
|
|
6607
|
+
};
|
|
6608
|
+
const tomlContent = stringify(existingConfig);
|
|
6609
|
+
nodeFs__default.writeFileSync(CONFIG_FILE_PATH, tomlContent, "utf-8");
|
|
6610
|
+
} catch (error) {
|
|
6611
|
+
throw new Error(`Failed to save notification config: ${error}`);
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
async function saveDeviceToken(token) {
|
|
6615
|
+
try {
|
|
6616
|
+
const encryptedToken = encryptToken(token);
|
|
6617
|
+
let secrets = {};
|
|
6618
|
+
if (nodeFs__default.existsSync(SECRETS_FILE_PATH)) {
|
|
6619
|
+
const secretsContent = nodeFs__default.readFileSync(SECRETS_FILE_PATH, "utf-8");
|
|
6620
|
+
secrets = JSON.parse(secretsContent);
|
|
6621
|
+
}
|
|
6622
|
+
secrets.deviceToken = encryptedToken;
|
|
6623
|
+
secrets.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6624
|
+
nodeFs__default.writeFileSync(SECRETS_FILE_PATH, JSON.stringify(secrets, null, 2), {
|
|
6625
|
+
encoding: "utf-8",
|
|
6626
|
+
mode: 384
|
|
6627
|
+
// Owner read/write only
|
|
6628
|
+
});
|
|
6629
|
+
} catch (error) {
|
|
6630
|
+
throw new Error(`Failed to save device token: ${error}`);
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
async function initializeNotificationConfig() {
|
|
6634
|
+
const existingConfig = await loadNotificationConfig();
|
|
6635
|
+
if (!existingConfig.deviceToken || !isValidTokenFormat(existingConfig.deviceToken)) {
|
|
6636
|
+
existingConfig.deviceToken = generateDeviceToken();
|
|
6637
|
+
await saveNotificationConfig(existingConfig);
|
|
6638
|
+
}
|
|
6639
|
+
return existingConfig;
|
|
6640
|
+
}
|
|
6641
|
+
async function updateNotificationConfig(updates) {
|
|
6642
|
+
const currentConfig = await loadNotificationConfig();
|
|
6643
|
+
const updatesQuietHours = updates.quietHours || {};
|
|
6644
|
+
const currentQuietHours = currentConfig.quietHours || {
|
|
6645
|
+
enabled: false,
|
|
6646
|
+
startHour: 22,
|
|
6647
|
+
endHour: 8,
|
|
6648
|
+
timezone: "local"
|
|
6649
|
+
};
|
|
6650
|
+
const newConfig = {
|
|
6651
|
+
...currentConfig,
|
|
6652
|
+
...updates,
|
|
6653
|
+
channels: {
|
|
6654
|
+
...currentConfig.channels,
|
|
6655
|
+
...updates.channels
|
|
6656
|
+
},
|
|
6657
|
+
quietHours: {
|
|
6658
|
+
enabled: updatesQuietHours.enabled ?? currentQuietHours.enabled,
|
|
6659
|
+
startHour: updatesQuietHours.startHour ?? currentQuietHours.startHour,
|
|
6660
|
+
endHour: updatesQuietHours.endHour ?? currentQuietHours.endHour,
|
|
6661
|
+
timezone: updatesQuietHours.timezone ?? currentQuietHours.timezone
|
|
6662
|
+
}
|
|
6663
|
+
};
|
|
6664
|
+
await saveNotificationConfig(newConfig);
|
|
6665
|
+
return newConfig;
|
|
6666
|
+
}
|
|
6667
|
+
async function enableChannel(channel, config) {
|
|
6668
|
+
const currentConfig = await loadNotificationConfig();
|
|
6669
|
+
currentConfig.channels[channel] = {
|
|
6670
|
+
...config,
|
|
6671
|
+
enabled: true
|
|
6672
|
+
};
|
|
6673
|
+
await saveNotificationConfig(currentConfig);
|
|
6674
|
+
}
|
|
6675
|
+
async function disableChannel(channel) {
|
|
6676
|
+
const currentConfig = await loadNotificationConfig();
|
|
6677
|
+
if (currentConfig.channels[channel]) {
|
|
6678
|
+
currentConfig.channels[channel].enabled = false;
|
|
6679
|
+
await saveNotificationConfig(currentConfig);
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
async function getEnabledChannels() {
|
|
6683
|
+
const config = await loadNotificationConfig();
|
|
6684
|
+
const enabledChannels = [];
|
|
6685
|
+
if (config.channels.feishu?.enabled) {
|
|
6686
|
+
enabledChannels.push("feishu");
|
|
6687
|
+
}
|
|
6688
|
+
if (config.channels.wechat?.enabled) {
|
|
6689
|
+
enabledChannels.push("wechat");
|
|
6690
|
+
}
|
|
6691
|
+
if (config.channels.email?.enabled) {
|
|
6692
|
+
enabledChannels.push("email");
|
|
6693
|
+
}
|
|
6694
|
+
if (config.channels.sms?.enabled) {
|
|
6695
|
+
enabledChannels.push("sms");
|
|
6696
|
+
}
|
|
6697
|
+
return enabledChannels;
|
|
6698
|
+
}
|
|
6699
|
+
async function validateCurrentConfig() {
|
|
6700
|
+
const config = await loadNotificationConfig();
|
|
6701
|
+
return validateNotificationConfig(config);
|
|
6702
|
+
}
|
|
6703
|
+
async function getConfigSummary() {
|
|
6704
|
+
const config = await loadNotificationConfig();
|
|
6705
|
+
const enabledChannels = await getEnabledChannels();
|
|
6706
|
+
return {
|
|
6707
|
+
enabled: config.enabled,
|
|
6708
|
+
deviceToken: maskToken(config.deviceToken),
|
|
6709
|
+
threshold: config.threshold,
|
|
6710
|
+
enabledChannels,
|
|
6711
|
+
quietHours: {
|
|
6712
|
+
enabled: config.quietHours?.enabled || false,
|
|
6713
|
+
hours: config.quietHours?.enabled ? `${config.quietHours.startHour}:00 - ${config.quietHours.endHour}:00` : void 0
|
|
6714
|
+
}
|
|
6715
|
+
};
|
|
6716
|
+
}
|
|
6717
|
+
const THRESHOLD_OPTIONS = [
|
|
6718
|
+
{ value: 5, label: "5 minutes" },
|
|
6719
|
+
{ value: 10, label: "10 minutes" },
|
|
6720
|
+
{ value: 15, label: "15 minutes" },
|
|
6721
|
+
{ value: 30, label: "30 minutes" },
|
|
6722
|
+
{ value: 60, label: "1 hour" }
|
|
6723
|
+
];
|
|
6724
|
+
async function setThreshold(minutes) {
|
|
6725
|
+
if (minutes < 1) {
|
|
6726
|
+
throw new Error("Threshold must be at least 1 minute");
|
|
6727
|
+
}
|
|
6728
|
+
await updateNotificationConfig({ threshold: minutes });
|
|
6729
|
+
}
|
|
6730
|
+
async function enableNotifications() {
|
|
6731
|
+
await updateNotificationConfig({ enabled: true });
|
|
6732
|
+
}
|
|
6733
|
+
async function disableNotifications() {
|
|
6734
|
+
await updateNotificationConfig({ enabled: false });
|
|
6735
|
+
}
|
|
6736
|
+
|
|
6737
|
+
const DEFAULT_CLOUD_ENDPOINT = "https://api.claudehome.cn";
|
|
6738
|
+
const REQUEST_TIMEOUT = 3e4;
|
|
6739
|
+
const POLL_TIMEOUT = 6e4;
|
|
6740
|
+
class CloudClient {
|
|
6741
|
+
static instance = null;
|
|
6742
|
+
endpoint = DEFAULT_CLOUD_ENDPOINT;
|
|
6743
|
+
deviceToken = "";
|
|
6744
|
+
isPolling = false;
|
|
6745
|
+
pollAbortController = null;
|
|
6746
|
+
constructor() {
|
|
6747
|
+
}
|
|
6748
|
+
/**
|
|
6749
|
+
* Get the singleton instance
|
|
6750
|
+
*/
|
|
6751
|
+
static getInstance() {
|
|
6752
|
+
if (!CloudClient.instance) {
|
|
6753
|
+
CloudClient.instance = new CloudClient();
|
|
6754
|
+
}
|
|
6755
|
+
return CloudClient.instance;
|
|
6756
|
+
}
|
|
6757
|
+
/**
|
|
6758
|
+
* Initialize the cloud client
|
|
6759
|
+
*/
|
|
6760
|
+
async initialize() {
|
|
6761
|
+
const config = await loadNotificationConfig();
|
|
6762
|
+
this.endpoint = config.cloudEndpoint || DEFAULT_CLOUD_ENDPOINT;
|
|
6763
|
+
this.deviceToken = config.deviceToken;
|
|
6764
|
+
}
|
|
6765
|
+
/**
|
|
6766
|
+
* Set the cloud endpoint
|
|
6767
|
+
*/
|
|
6768
|
+
setEndpoint(endpoint) {
|
|
6769
|
+
this.endpoint = endpoint;
|
|
6770
|
+
}
|
|
6771
|
+
/**
|
|
6772
|
+
* Set the device token
|
|
6773
|
+
*/
|
|
6774
|
+
setDeviceToken(token) {
|
|
6775
|
+
this.deviceToken = token;
|
|
6776
|
+
}
|
|
6777
|
+
// ==========================================================================
|
|
6778
|
+
// Device Registration
|
|
6779
|
+
// ==========================================================================
|
|
6780
|
+
/**
|
|
6781
|
+
* Register this device with the cloud service
|
|
6782
|
+
*/
|
|
6783
|
+
async registerDevice(name) {
|
|
6784
|
+
const deviceInfo = getDeviceInfo();
|
|
6785
|
+
const config = await loadNotificationConfig();
|
|
6786
|
+
const channelsArray = this.convertChannelsToArray(config.channels);
|
|
6787
|
+
const request = {
|
|
6788
|
+
name: name || deviceInfo.name,
|
|
6789
|
+
platform: deviceInfo.platform,
|
|
6790
|
+
version: "1.0.0",
|
|
6791
|
+
// TODO: Get from package.json
|
|
6792
|
+
config: {
|
|
6793
|
+
channels: channelsArray,
|
|
6794
|
+
threshold: config.threshold
|
|
6795
|
+
}
|
|
6796
|
+
};
|
|
6797
|
+
const response = await this.request(
|
|
6798
|
+
"/device/register",
|
|
6799
|
+
{
|
|
6800
|
+
method: "POST",
|
|
6801
|
+
body: JSON.stringify(request)
|
|
6802
|
+
}
|
|
6803
|
+
);
|
|
6804
|
+
if (response.success && response.data) {
|
|
6805
|
+
await updateNotificationConfig({
|
|
6806
|
+
deviceToken: response.data.token
|
|
6807
|
+
});
|
|
6808
|
+
this.deviceToken = response.data.token;
|
|
6809
|
+
return response.data;
|
|
6810
|
+
}
|
|
6811
|
+
throw new Error(response.error || "Failed to register device");
|
|
6812
|
+
}
|
|
6813
|
+
/**
|
|
6814
|
+
* Get device info from cloud service
|
|
6815
|
+
*/
|
|
6816
|
+
async getDeviceInfo() {
|
|
6817
|
+
const response = await this.request(
|
|
6818
|
+
"/device/info",
|
|
6819
|
+
{ method: "GET" }
|
|
6820
|
+
);
|
|
6821
|
+
if (response.success && response.data) {
|
|
6822
|
+
return response.data;
|
|
6823
|
+
}
|
|
6824
|
+
throw new Error(response.error || "Failed to get device info");
|
|
6825
|
+
}
|
|
6826
|
+
/**
|
|
6827
|
+
* Update device channels on cloud service
|
|
6828
|
+
*/
|
|
6829
|
+
async updateChannels(channels) {
|
|
6830
|
+
const channelsArray = this.convertChannelsToArray(channels);
|
|
6831
|
+
const response = await this.request(
|
|
6832
|
+
"/device/channels",
|
|
6833
|
+
{
|
|
6834
|
+
method: "PUT",
|
|
6835
|
+
body: JSON.stringify({ channels: channelsArray })
|
|
6836
|
+
}
|
|
6837
|
+
);
|
|
6838
|
+
if (!response.success) {
|
|
6839
|
+
throw new Error(response.error || "Failed to update channels");
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
// ==========================================================================
|
|
6843
|
+
// Notification Sending
|
|
6844
|
+
// ==========================================================================
|
|
6845
|
+
/**
|
|
6846
|
+
* Send a notification through the cloud service
|
|
6847
|
+
*/
|
|
6848
|
+
async sendNotification(message, channels) {
|
|
6849
|
+
const config = await loadNotificationConfig();
|
|
6850
|
+
const targetChannels = channels || this.getEnabledChannelsFromConfig(config.channels);
|
|
6851
|
+
if (targetChannels.length === 0) {
|
|
6852
|
+
return [];
|
|
6853
|
+
}
|
|
6854
|
+
const title = message.title || this.generateTitle(message.type);
|
|
6855
|
+
const body = this.generateBody(message);
|
|
6856
|
+
const response = await this.request(
|
|
6857
|
+
"/notify",
|
|
6858
|
+
{
|
|
6859
|
+
method: "POST",
|
|
6860
|
+
body: JSON.stringify({
|
|
6861
|
+
type: message.type,
|
|
6862
|
+
title,
|
|
6863
|
+
body,
|
|
6864
|
+
task: message.task,
|
|
6865
|
+
channels: targetChannels,
|
|
6866
|
+
actions: message.actions,
|
|
6867
|
+
priority: message.priority
|
|
6868
|
+
})
|
|
6869
|
+
}
|
|
6870
|
+
);
|
|
6871
|
+
if (response.success && response.data) {
|
|
6872
|
+
return response.data.results;
|
|
6873
|
+
}
|
|
6874
|
+
return targetChannels.map((channel) => ({
|
|
6875
|
+
success: false,
|
|
6876
|
+
channel,
|
|
6877
|
+
sentAt: /* @__PURE__ */ new Date(),
|
|
6878
|
+
error: response.error || "Failed to send notification"
|
|
6879
|
+
}));
|
|
6880
|
+
}
|
|
6881
|
+
/**
|
|
6882
|
+
* Send a test notification
|
|
6883
|
+
*/
|
|
6884
|
+
async sendTestNotification() {
|
|
6885
|
+
const response = await this.request(
|
|
6886
|
+
"/notify/test",
|
|
6887
|
+
{ method: "POST" }
|
|
6888
|
+
);
|
|
6889
|
+
if (response.success && response.data) {
|
|
6890
|
+
return response.data.results;
|
|
6891
|
+
}
|
|
6892
|
+
throw new Error(response.error || "Failed to send test notification");
|
|
6893
|
+
}
|
|
6894
|
+
// ==========================================================================
|
|
6895
|
+
// Reply Polling
|
|
6896
|
+
// ==========================================================================
|
|
6897
|
+
/**
|
|
6898
|
+
* Start polling for replies
|
|
6899
|
+
*
|
|
6900
|
+
* @param onReply - Callback when a reply is received
|
|
6901
|
+
* @param onError - Callback when an error occurs
|
|
6902
|
+
*/
|
|
6903
|
+
startPolling(onReply, onError) {
|
|
6904
|
+
if (this.isPolling) {
|
|
6905
|
+
return;
|
|
6906
|
+
}
|
|
6907
|
+
this.isPolling = true;
|
|
6908
|
+
this.pollLoop(onReply, onError);
|
|
6909
|
+
}
|
|
6910
|
+
/**
|
|
6911
|
+
* Stop polling for replies
|
|
6912
|
+
*/
|
|
6913
|
+
stopPolling() {
|
|
6914
|
+
this.isPolling = false;
|
|
6915
|
+
if (this.pollAbortController) {
|
|
6916
|
+
this.pollAbortController.abort();
|
|
6917
|
+
this.pollAbortController = null;
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
/**
|
|
6921
|
+
* Poll loop for replies
|
|
6922
|
+
*/
|
|
6923
|
+
async pollLoop(onReply, onError) {
|
|
6924
|
+
while (this.isPolling) {
|
|
6925
|
+
try {
|
|
6926
|
+
const reply = await this.pollForReply();
|
|
6927
|
+
if (reply) {
|
|
6928
|
+
onReply(reply);
|
|
6929
|
+
}
|
|
6930
|
+
} catch (error) {
|
|
6931
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
6932
|
+
break;
|
|
6933
|
+
}
|
|
6934
|
+
onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
6935
|
+
await this.sleep(5e3);
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6939
|
+
/**
|
|
6940
|
+
* Poll for a single reply (long-polling)
|
|
6941
|
+
*/
|
|
6942
|
+
async pollForReply() {
|
|
6943
|
+
this.pollAbortController = new AbortController();
|
|
6944
|
+
try {
|
|
6945
|
+
const response = await this.request(
|
|
6946
|
+
"/reply/poll",
|
|
6947
|
+
{
|
|
6948
|
+
method: "GET",
|
|
6949
|
+
signal: this.pollAbortController.signal,
|
|
6950
|
+
timeout: POLL_TIMEOUT
|
|
6951
|
+
}
|
|
6952
|
+
);
|
|
6953
|
+
if (response.success && response.data?.reply) {
|
|
6954
|
+
return {
|
|
6955
|
+
...response.data.reply,
|
|
6956
|
+
timestamp: new Date(response.data.reply.timestamp)
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
return null;
|
|
6960
|
+
} finally {
|
|
6961
|
+
this.pollAbortController = null;
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
/**
|
|
6965
|
+
* Get reply for a specific notification
|
|
6966
|
+
*/
|
|
6967
|
+
async getReply(notificationId) {
|
|
6968
|
+
const response = await this.request(
|
|
6969
|
+
`/reply/${notificationId}`,
|
|
6970
|
+
{ method: "GET" }
|
|
6971
|
+
);
|
|
6972
|
+
if (response.success && response.data?.reply) {
|
|
6973
|
+
return {
|
|
6974
|
+
...response.data.reply,
|
|
6975
|
+
timestamp: new Date(response.data.reply.timestamp)
|
|
6976
|
+
};
|
|
6977
|
+
}
|
|
6978
|
+
return null;
|
|
6979
|
+
}
|
|
6980
|
+
// ==========================================================================
|
|
6981
|
+
// HTTP Request Helper
|
|
6982
|
+
// ==========================================================================
|
|
6983
|
+
/**
|
|
6984
|
+
* Make an HTTP request to the cloud service
|
|
6985
|
+
*/
|
|
6986
|
+
async request(path, options) {
|
|
6987
|
+
const url = `${this.endpoint}${path}`;
|
|
6988
|
+
const timeout = options.timeout || REQUEST_TIMEOUT;
|
|
6989
|
+
const timeoutController = new AbortController();
|
|
6990
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
|
6991
|
+
try {
|
|
6992
|
+
const response = await fetch(url, {
|
|
6993
|
+
method: options.method,
|
|
6994
|
+
headers: {
|
|
6995
|
+
"Content-Type": "application/json",
|
|
6996
|
+
"X-Device-Token": this.deviceToken
|
|
6997
|
+
},
|
|
6998
|
+
body: options.body,
|
|
6999
|
+
signal: options.signal || timeoutController.signal
|
|
7000
|
+
});
|
|
7001
|
+
clearTimeout(timeoutId);
|
|
7002
|
+
const data = await response.json();
|
|
7003
|
+
if (!response.ok) {
|
|
7004
|
+
return {
|
|
7005
|
+
success: false,
|
|
7006
|
+
error: data.error || `HTTP ${response.status}: ${response.statusText}`,
|
|
7007
|
+
code: data.code
|
|
7008
|
+
};
|
|
7009
|
+
}
|
|
7010
|
+
return data;
|
|
7011
|
+
} catch (error) {
|
|
7012
|
+
clearTimeout(timeoutId);
|
|
7013
|
+
if (error instanceof Error) {
|
|
7014
|
+
if (error.name === "AbortError") {
|
|
7015
|
+
throw error;
|
|
7016
|
+
}
|
|
7017
|
+
return {
|
|
7018
|
+
success: false,
|
|
7019
|
+
error: error.message,
|
|
7020
|
+
code: "NETWORK_ERROR"
|
|
7021
|
+
};
|
|
7022
|
+
}
|
|
7023
|
+
return {
|
|
7024
|
+
success: false,
|
|
7025
|
+
error: String(error),
|
|
7026
|
+
code: "UNKNOWN_ERROR"
|
|
7027
|
+
};
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
// ==========================================================================
|
|
7031
|
+
// Utility Methods
|
|
7032
|
+
// ==========================================================================
|
|
7033
|
+
/**
|
|
7034
|
+
* Get enabled channels from config
|
|
7035
|
+
*/
|
|
7036
|
+
getEnabledChannelsFromConfig(channels) {
|
|
7037
|
+
const enabledChannels = [];
|
|
7038
|
+
for (const [name, channelConfig] of Object.entries(channels)) {
|
|
7039
|
+
if (channelConfig?.enabled) {
|
|
7040
|
+
enabledChannels.push(name);
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
return enabledChannels;
|
|
7044
|
+
}
|
|
7045
|
+
/**
|
|
7046
|
+
* Sleep for a specified duration
|
|
7047
|
+
*/
|
|
7048
|
+
sleep(ms) {
|
|
7049
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7050
|
+
}
|
|
7051
|
+
/**
|
|
7052
|
+
* Convert channels from object format to array format
|
|
7053
|
+
*
|
|
7054
|
+
* Converts from: { feishu: { enabled: true, webhookUrl: "..." } }
|
|
7055
|
+
* To: [{ type: "feishu", enabled: true, config: { webhookUrl: "..." } }]
|
|
7056
|
+
*/
|
|
7057
|
+
convertChannelsToArray(channels) {
|
|
7058
|
+
const result = [];
|
|
7059
|
+
for (const [channelType, channelData] of Object.entries(channels)) {
|
|
7060
|
+
if (channelData && typeof channelData === "object") {
|
|
7061
|
+
const { enabled, ...config } = channelData;
|
|
7062
|
+
result.push({
|
|
7063
|
+
type: channelType,
|
|
7064
|
+
enabled: Boolean(enabled),
|
|
7065
|
+
config
|
|
7066
|
+
});
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
7069
|
+
return result;
|
|
7070
|
+
}
|
|
7071
|
+
/**
|
|
7072
|
+
* Generate notification title based on type
|
|
7073
|
+
*/
|
|
7074
|
+
generateTitle(type) {
|
|
7075
|
+
const titles = {
|
|
7076
|
+
task_started: "Task Started",
|
|
7077
|
+
task_progress: "Task Progress",
|
|
7078
|
+
task_completed: "Task Completed",
|
|
7079
|
+
task_failed: "Task Failed",
|
|
7080
|
+
task_cancelled: "Task Cancelled",
|
|
7081
|
+
system: "System Notification"
|
|
7082
|
+
};
|
|
7083
|
+
return titles[type] || "Notification";
|
|
7084
|
+
}
|
|
7085
|
+
/**
|
|
7086
|
+
* Generate notification body from message
|
|
7087
|
+
*/
|
|
7088
|
+
generateBody(message) {
|
|
7089
|
+
const { task } = message;
|
|
7090
|
+
const lines = [];
|
|
7091
|
+
lines.push(`**Task**: ${task.description}`);
|
|
7092
|
+
lines.push(`**Status**: ${task.status}`);
|
|
7093
|
+
if (task.duration) {
|
|
7094
|
+
const minutes = Math.floor(task.duration / 6e4);
|
|
7095
|
+
const seconds = Math.floor(task.duration % 6e4 / 1e3);
|
|
7096
|
+
lines.push(`**Duration**: ${minutes}m ${seconds}s`);
|
|
7097
|
+
}
|
|
7098
|
+
if (task.result) {
|
|
7099
|
+
lines.push(`**Result**: ${task.result}`);
|
|
7100
|
+
}
|
|
7101
|
+
if (task.error) {
|
|
7102
|
+
lines.push(`**Error**: ${task.error}`);
|
|
7103
|
+
}
|
|
7104
|
+
return lines.join("\n");
|
|
7105
|
+
}
|
|
7106
|
+
/**
|
|
7107
|
+
* Reset the singleton instance (for testing)
|
|
7108
|
+
*/
|
|
7109
|
+
static resetInstance() {
|
|
7110
|
+
if (CloudClient.instance) {
|
|
7111
|
+
CloudClient.instance.stopPolling();
|
|
7112
|
+
CloudClient.instance = null;
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
|
|
7117
|
+
async function notificationCommand(action = "menu") {
|
|
7118
|
+
switch (action) {
|
|
7119
|
+
case "config":
|
|
7120
|
+
case "configure":
|
|
7121
|
+
await runConfigWizard();
|
|
7122
|
+
break;
|
|
7123
|
+
case "status":
|
|
7124
|
+
await showStatus();
|
|
7125
|
+
break;
|
|
7126
|
+
case "test":
|
|
7127
|
+
await sendTestNotification();
|
|
7128
|
+
break;
|
|
7129
|
+
case "enable":
|
|
7130
|
+
await enableNotifications();
|
|
7131
|
+
console.log(ansis.green(`\u2705 ${i18n.t("notification:status.enabled")}`));
|
|
7132
|
+
break;
|
|
7133
|
+
case "disable":
|
|
7134
|
+
await disableNotifications();
|
|
7135
|
+
console.log(ansis.yellow(`\u23F8\uFE0F ${i18n.t("notification:status.disabled")}`));
|
|
7136
|
+
break;
|
|
7137
|
+
case "channels":
|
|
7138
|
+
await manageChannels();
|
|
7139
|
+
break;
|
|
7140
|
+
case "threshold":
|
|
7141
|
+
await configureThreshold();
|
|
7142
|
+
break;
|
|
7143
|
+
case "menu":
|
|
7144
|
+
default:
|
|
7145
|
+
await showNotificationMenu();
|
|
7146
|
+
break;
|
|
7147
|
+
}
|
|
7148
|
+
}
|
|
7149
|
+
async function showNotificationMenu() {
|
|
7150
|
+
const config = await loadNotificationConfig();
|
|
7151
|
+
const enabledChannels = await getEnabledChannels();
|
|
7152
|
+
console.log("");
|
|
7153
|
+
console.log(ansis.bold.cyan(i18n.t("notification:menu.title")));
|
|
7154
|
+
console.log("");
|
|
7155
|
+
const statusText = config.enabled ? ansis.green(i18n.t("notification:status.enabled")) : ansis.yellow(i18n.t("notification:status.disabled"));
|
|
7156
|
+
console.log(` ${ansis.dim("\u72B6\u6001:")} ${statusText}`);
|
|
7157
|
+
if (enabledChannels.length > 0) {
|
|
7158
|
+
const channelNames = enabledChannels.map((ch) => i18n.t(`notification:channels.${ch}`)).join(", ");
|
|
7159
|
+
console.log(` ${ansis.dim("\u6E20\u9053:")} ${channelNames}`);
|
|
7160
|
+
} else {
|
|
7161
|
+
console.log(` ${ansis.dim("\u6E20\u9053:")} ${ansis.yellow(i18n.t("notification:channels.noChannels"))}`);
|
|
7162
|
+
}
|
|
7163
|
+
console.log(` ${ansis.dim("\u9608\u503C:")} ${config.threshold} ${i18n.t("notification:config.threshold.minutes", { count: config.threshold })}`);
|
|
7164
|
+
console.log("");
|
|
7165
|
+
const { action } = await inquirer.prompt([{
|
|
7166
|
+
type: "list",
|
|
7167
|
+
name: "action",
|
|
7168
|
+
message: "\u9009\u62E9\u64CD\u4F5C:",
|
|
7169
|
+
choices: [
|
|
7170
|
+
{
|
|
7171
|
+
name: config.enabled ? "\u23F8\uFE0F \u7981\u7528\u901A\u77E5" : "\u25B6\uFE0F \u542F\u7528\u901A\u77E5",
|
|
7172
|
+
value: config.enabled ? "disable" : "enable"
|
|
7173
|
+
},
|
|
7174
|
+
{ name: "\u2699\uFE0F \u914D\u7F6E\u5411\u5BFC", value: "config" },
|
|
7175
|
+
{ name: "\u{1F4F1} \u7BA1\u7406\u6E20\u9053", value: "channels" },
|
|
7176
|
+
{ name: "\u23F1\uFE0F \u8BBE\u7F6E\u9608\u503C", value: "threshold" },
|
|
7177
|
+
{ name: "\u{1F4CA} \u67E5\u770B\u72B6\u6001", value: "status" },
|
|
7178
|
+
{ name: "\u{1F9EA} \u53D1\u9001\u6D4B\u8BD5", value: "test" },
|
|
7179
|
+
{ name: "\u2190 \u8FD4\u56DE", value: "back" }
|
|
7180
|
+
]
|
|
7181
|
+
}]);
|
|
7182
|
+
if (action === "back") {
|
|
7183
|
+
return;
|
|
7184
|
+
}
|
|
7185
|
+
await notificationCommand(action);
|
|
7186
|
+
}
|
|
7187
|
+
async function runConfigWizard() {
|
|
7188
|
+
console.log("");
|
|
7189
|
+
console.log(ansis.bold.cyan(i18n.t("notification:config.wizard.title")));
|
|
7190
|
+
console.log(ansis.dim(i18n.t("notification:config.wizard.welcome")));
|
|
7191
|
+
console.log("");
|
|
7192
|
+
console.log(ansis.yellow(i18n.t("notification:config.wizard.step1")));
|
|
7193
|
+
const config = await initializeNotificationConfig();
|
|
7194
|
+
console.log(ansis.green(i18n.t("notification:config.wizard.tokenGenerated", { token: maskToken(config.deviceToken) })));
|
|
7195
|
+
console.log("");
|
|
7196
|
+
console.log(ansis.yellow(i18n.t("notification:config.wizard.step2")));
|
|
7197
|
+
const channels = await selectChannels();
|
|
7198
|
+
console.log("");
|
|
7199
|
+
if (channels.length > 0) {
|
|
7200
|
+
console.log(ansis.yellow(i18n.t("notification:config.wizard.step3")));
|
|
7201
|
+
for (const channel of channels) {
|
|
7202
|
+
await configureChannel(channel);
|
|
7203
|
+
}
|
|
7204
|
+
console.log("");
|
|
7205
|
+
}
|
|
7206
|
+
console.log(ansis.yellow(i18n.t("notification:config.wizard.step4")));
|
|
7207
|
+
await configureThreshold();
|
|
7208
|
+
console.log("");
|
|
7209
|
+
console.log(ansis.yellow(i18n.t("notification:config.wizard.step5")));
|
|
7210
|
+
await updateNotificationConfig({ enabled: true });
|
|
7211
|
+
const { shouldTest } = await inquirer.prompt([{
|
|
7212
|
+
type: "confirm",
|
|
7213
|
+
name: "shouldTest",
|
|
7214
|
+
message: "\u662F\u5426\u53D1\u9001\u6D4B\u8BD5\u901A\u77E5?",
|
|
7215
|
+
default: true
|
|
7216
|
+
}]);
|
|
7217
|
+
if (shouldTest) {
|
|
7218
|
+
await sendTestNotification();
|
|
7219
|
+
}
|
|
7220
|
+
console.log("");
|
|
7221
|
+
console.log(ansis.bold.green(i18n.t("notification:config.wizard.complete")));
|
|
7222
|
+
console.log("");
|
|
7223
|
+
}
|
|
7224
|
+
async function selectChannels() {
|
|
7225
|
+
const choices = [
|
|
7226
|
+
{ name: `\u{1F4F1} ${i18n.t("notification:channels.feishu")}`, value: "feishu" },
|
|
7227
|
+
{ name: `\u{1F4AC} ${i18n.t("notification:channels.wechat")}`, value: "wechat" },
|
|
7228
|
+
{ name: `\u{1F4E7} ${i18n.t("notification:channels.email")}`, value: "email" },
|
|
7229
|
+
{ name: `\u{1F4F2} ${i18n.t("notification:channels.sms")}`, value: "sms" }
|
|
7230
|
+
];
|
|
7231
|
+
const selected = [];
|
|
7232
|
+
for (const choice of choices) {
|
|
7233
|
+
const { enable } = await inquirer.prompt([{
|
|
7234
|
+
type: "confirm",
|
|
7235
|
+
name: "enable",
|
|
7236
|
+
message: `\u542F\u7528 ${choice.name}?`,
|
|
7237
|
+
default: false
|
|
7238
|
+
}]);
|
|
7239
|
+
if (enable) {
|
|
7240
|
+
selected.push(choice.value);
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
return selected;
|
|
7244
|
+
}
|
|
7245
|
+
async function configureChannel(channel) {
|
|
7246
|
+
console.log("");
|
|
7247
|
+
console.log(ansis.cyan(`\u914D\u7F6E ${i18n.t(`notification:channels.${channel}`)}:`));
|
|
7248
|
+
switch (channel) {
|
|
7249
|
+
case "feishu":
|
|
7250
|
+
await configureFeishu();
|
|
7251
|
+
break;
|
|
7252
|
+
case "wechat":
|
|
7253
|
+
await configureWechat();
|
|
7254
|
+
break;
|
|
7255
|
+
case "email":
|
|
7256
|
+
await configureEmail();
|
|
7257
|
+
break;
|
|
7258
|
+
case "sms":
|
|
7259
|
+
await configureSms();
|
|
7260
|
+
break;
|
|
7261
|
+
}
|
|
7262
|
+
}
|
|
7263
|
+
async function configureFeishu() {
|
|
7264
|
+
const { webhookUrl } = await inquirer.prompt([{
|
|
7265
|
+
type: "input",
|
|
7266
|
+
name: "webhookUrl",
|
|
7267
|
+
message: i18n.t("notification:feishu.webhookUrl"),
|
|
7268
|
+
validate: (value) => {
|
|
7269
|
+
if (!value.startsWith("https://")) {
|
|
7270
|
+
return i18n.t("notification:errors.invalidWebhook");
|
|
7271
|
+
}
|
|
7272
|
+
return true;
|
|
7273
|
+
}
|
|
7274
|
+
}]);
|
|
7275
|
+
const { secret } = await inquirer.prompt([{
|
|
7276
|
+
type: "input",
|
|
7277
|
+
name: "secret",
|
|
7278
|
+
message: `${i18n.t("notification:feishu.secret")} (\u53EF\u9009\uFF0C\u76F4\u63A5\u56DE\u8F66\u8DF3\u8FC7):`
|
|
7279
|
+
}]);
|
|
7280
|
+
const config = {
|
|
7281
|
+
enabled: true,
|
|
7282
|
+
webhookUrl,
|
|
7283
|
+
...secret && { secret }
|
|
7284
|
+
};
|
|
7285
|
+
await enableChannel("feishu", config);
|
|
7286
|
+
console.log(ansis.green(`\u2705 ${i18n.t("notification:channels.feishu")} ${i18n.t("notification:status.configured")}`));
|
|
7287
|
+
}
|
|
7288
|
+
async function configureWechat() {
|
|
7289
|
+
const { corpId } = await inquirer.prompt([{
|
|
7290
|
+
type: "input",
|
|
7291
|
+
name: "corpId",
|
|
7292
|
+
message: i18n.t("notification:wechat.corpId"),
|
|
7293
|
+
validate: (value) => !!value || i18n.t("notification:errors.invalidWebhook")
|
|
7294
|
+
}]);
|
|
7295
|
+
const { agentId } = await inquirer.prompt([{
|
|
7296
|
+
type: "input",
|
|
7297
|
+
name: "agentId",
|
|
7298
|
+
message: i18n.t("notification:wechat.agentId"),
|
|
7299
|
+
validate: (value) => !!value || i18n.t("notification:errors.invalidWebhook")
|
|
7300
|
+
}]);
|
|
7301
|
+
const { secret } = await inquirer.prompt([{
|
|
7302
|
+
type: "input",
|
|
7303
|
+
name: "secret",
|
|
7304
|
+
message: i18n.t("notification:wechat.secret"),
|
|
7305
|
+
validate: (value) => !!value || i18n.t("notification:errors.invalidWebhook")
|
|
7306
|
+
}]);
|
|
7307
|
+
const config = {
|
|
7308
|
+
enabled: true,
|
|
7309
|
+
corpId,
|
|
7310
|
+
agentId,
|
|
7311
|
+
secret
|
|
7312
|
+
};
|
|
7313
|
+
await enableChannel("wechat", config);
|
|
7314
|
+
console.log(ansis.green(`\u2705 ${i18n.t("notification:channels.wechat")} ${i18n.t("notification:status.configured")}`));
|
|
7315
|
+
}
|
|
7316
|
+
async function configureEmail() {
|
|
7317
|
+
const { address } = await inquirer.prompt([{
|
|
7318
|
+
type: "input",
|
|
7319
|
+
name: "address",
|
|
7320
|
+
message: i18n.t("notification:email.address"),
|
|
7321
|
+
validate: (value) => {
|
|
7322
|
+
if (!/^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/.test(value)) {
|
|
7323
|
+
return i18n.t("notification:errors.invalidEmail");
|
|
7324
|
+
}
|
|
7325
|
+
return true;
|
|
7326
|
+
}
|
|
7327
|
+
}]);
|
|
7328
|
+
const config = {
|
|
7329
|
+
enabled: true,
|
|
7330
|
+
address
|
|
7331
|
+
};
|
|
7332
|
+
await enableChannel("email", config);
|
|
7333
|
+
console.log(ansis.green(`\u2705 ${i18n.t("notification:channels.email")} ${i18n.t("notification:status.configured")}`));
|
|
7334
|
+
}
|
|
7335
|
+
async function configureSms() {
|
|
7336
|
+
const { phone } = await inquirer.prompt([{
|
|
7337
|
+
type: "input",
|
|
7338
|
+
name: "phone",
|
|
7339
|
+
message: i18n.t("notification:sms.phone"),
|
|
7340
|
+
validate: (value) => {
|
|
7341
|
+
if (!/^\d{10,15}$/.test(value.replace(/\D/g, ""))) {
|
|
7342
|
+
return i18n.t("notification:errors.invalidPhone");
|
|
7343
|
+
}
|
|
7344
|
+
return true;
|
|
7345
|
+
}
|
|
7346
|
+
}]);
|
|
7347
|
+
const { countryCode } = await inquirer.prompt([{
|
|
7348
|
+
type: "input",
|
|
7349
|
+
name: "countryCode",
|
|
7350
|
+
message: i18n.t("notification:sms.countryCode"),
|
|
7351
|
+
default: "+86"
|
|
7352
|
+
}]);
|
|
7353
|
+
const config = {
|
|
7354
|
+
enabled: true,
|
|
7355
|
+
phone,
|
|
7356
|
+
countryCode
|
|
7357
|
+
};
|
|
7358
|
+
await enableChannel("sms", config);
|
|
7359
|
+
console.log(ansis.green(`\u2705 ${i18n.t("notification:channels.sms")} ${i18n.t("notification:status.configured")}`));
|
|
7360
|
+
}
|
|
7361
|
+
async function manageChannels() {
|
|
7362
|
+
const enabledChannels = await getEnabledChannels();
|
|
7363
|
+
console.log("");
|
|
7364
|
+
console.log(ansis.bold.cyan(i18n.t("notification:channels.title")));
|
|
7365
|
+
console.log("");
|
|
7366
|
+
if (enabledChannels.length === 0) {
|
|
7367
|
+
console.log(ansis.yellow(i18n.t("notification:channels.noChannels")));
|
|
7368
|
+
} else {
|
|
7369
|
+
console.log(i18n.t("notification:channels.enabledCount", { count: enabledChannels.length }));
|
|
7370
|
+
for (const channel of enabledChannels) {
|
|
7371
|
+
console.log(` \u2705 ${i18n.t(`notification:channels.${channel}`)}`);
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
console.log("");
|
|
7375
|
+
const { action } = await inquirer.prompt([{
|
|
7376
|
+
type: "list",
|
|
7377
|
+
name: "action",
|
|
7378
|
+
message: "\u9009\u62E9\u64CD\u4F5C:",
|
|
7379
|
+
choices: [
|
|
7380
|
+
{ name: "\u2795 \u6DFB\u52A0\u6E20\u9053", value: "add" },
|
|
7381
|
+
{ name: "\u2796 \u79FB\u9664\u6E20\u9053", value: "remove" },
|
|
7382
|
+
{ name: "\u2190 \u8FD4\u56DE", value: "back" }
|
|
7383
|
+
]
|
|
7384
|
+
}]);
|
|
7385
|
+
if (action === "back") {
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
if (action === "add") {
|
|
7389
|
+
const allChannels = ["feishu", "wechat", "email", "sms"];
|
|
7390
|
+
const availableChannels = allChannels.filter((ch) => !enabledChannels.includes(ch));
|
|
7391
|
+
if (availableChannels.length === 0) {
|
|
7392
|
+
console.log(ansis.yellow("\u6240\u6709\u6E20\u9053\u90FD\u5DF2\u542F\u7528"));
|
|
7393
|
+
return;
|
|
7394
|
+
}
|
|
7395
|
+
const { channel } = await inquirer.prompt([{
|
|
7396
|
+
type: "list",
|
|
7397
|
+
name: "channel",
|
|
7398
|
+
message: "\u9009\u62E9\u8981\u6DFB\u52A0\u7684\u6E20\u9053:",
|
|
7399
|
+
choices: availableChannels.map((ch) => ({
|
|
7400
|
+
name: i18n.t(`notification:channels.${ch}`),
|
|
7401
|
+
value: ch
|
|
7402
|
+
}))
|
|
7403
|
+
}]);
|
|
7404
|
+
await configureChannel(channel);
|
|
7405
|
+
} else if (action === "remove") {
|
|
7406
|
+
if (enabledChannels.length === 0) {
|
|
7407
|
+
console.log(ansis.yellow("\u6CA1\u6709\u5DF2\u542F\u7528\u7684\u6E20\u9053"));
|
|
7408
|
+
return;
|
|
7409
|
+
}
|
|
7410
|
+
const { channel } = await inquirer.prompt([{
|
|
7411
|
+
type: "list",
|
|
7412
|
+
name: "channel",
|
|
7413
|
+
message: "\u9009\u62E9\u8981\u79FB\u9664\u7684\u6E20\u9053:",
|
|
7414
|
+
choices: enabledChannels.map((ch) => ({
|
|
7415
|
+
name: i18n.t(`notification:channels.${ch}`),
|
|
7416
|
+
value: ch
|
|
7417
|
+
}))
|
|
7418
|
+
}]);
|
|
7419
|
+
await disableChannel(channel);
|
|
7420
|
+
console.log(ansis.green(`\u2705 \u5DF2\u79FB\u9664 ${i18n.t(`notification:channels.${channel}`)}`));
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7423
|
+
async function configureThreshold() {
|
|
7424
|
+
const config = await loadNotificationConfig();
|
|
7425
|
+
console.log("");
|
|
7426
|
+
console.log(ansis.dim(i18n.t("notification:config.threshold.description")));
|
|
7427
|
+
console.log("");
|
|
7428
|
+
const { threshold } = await inquirer.prompt([{
|
|
7429
|
+
type: "list",
|
|
7430
|
+
name: "threshold",
|
|
7431
|
+
message: i18n.t("notification:config.threshold.title"),
|
|
7432
|
+
choices: [
|
|
7433
|
+
...THRESHOLD_OPTIONS.map((opt) => ({
|
|
7434
|
+
name: opt.label,
|
|
7435
|
+
value: opt.value
|
|
7436
|
+
})),
|
|
7437
|
+
{ name: i18n.t("notification:config.threshold.custom"), value: -1 }
|
|
7438
|
+
],
|
|
7439
|
+
default: config.threshold
|
|
7440
|
+
}]);
|
|
7441
|
+
let finalThreshold = threshold;
|
|
7442
|
+
if (threshold === -1) {
|
|
7443
|
+
const { customValue } = await inquirer.prompt([{
|
|
7444
|
+
type: "input",
|
|
7445
|
+
name: "customValue",
|
|
7446
|
+
message: "\u8F93\u5165\u81EA\u5B9A\u4E49\u9608\u503C\uFF08\u5206\u949F\uFF09:",
|
|
7447
|
+
validate: (value) => {
|
|
7448
|
+
const num = Number.parseInt(value, 10);
|
|
7449
|
+
if (Number.isNaN(num) || num < 1) {
|
|
7450
|
+
return "\u8BF7\u8F93\u5165\u5927\u4E8E 0 \u7684\u6570\u5B57";
|
|
7451
|
+
}
|
|
7452
|
+
return true;
|
|
7453
|
+
}
|
|
7454
|
+
}]);
|
|
7455
|
+
finalThreshold = Number.parseInt(customValue, 10);
|
|
7456
|
+
}
|
|
7457
|
+
await setThreshold(finalThreshold);
|
|
7458
|
+
console.log(ansis.green(`\u2705 \u9608\u503C\u5DF2\u8BBE\u7F6E\u4E3A ${finalThreshold} \u5206\u949F`));
|
|
7459
|
+
}
|
|
7460
|
+
async function showStatus() {
|
|
7461
|
+
const summary = await getConfigSummary();
|
|
7462
|
+
const validation = await validateCurrentConfig();
|
|
7463
|
+
console.log("");
|
|
7464
|
+
console.log(ansis.bold.cyan("\u{1F4CA} \u901A\u77E5\u7CFB\u7EDF\u72B6\u6001"));
|
|
7465
|
+
console.log("");
|
|
7466
|
+
const statusIcon = summary.enabled ? "\u2705" : "\u23F8\uFE0F";
|
|
7467
|
+
const statusText = summary.enabled ? ansis.green(i18n.t("notification:status.enabled")) : ansis.yellow(i18n.t("notification:status.disabled"));
|
|
7468
|
+
console.log(` ${statusIcon} \u72B6\u6001: ${statusText}`);
|
|
7469
|
+
console.log(` \u{1F511} \u8BBE\u5907\u4EE4\u724C: ${ansis.dim(summary.deviceToken)}`);
|
|
7470
|
+
console.log(` \u23F1\uFE0F \u9608\u503C: ${summary.threshold} \u5206\u949F`);
|
|
7471
|
+
console.log("");
|
|
7472
|
+
console.log(ansis.bold(" \u{1F4F1} \u901A\u77E5\u6E20\u9053:"));
|
|
7473
|
+
if (summary.enabledChannels.length === 0) {
|
|
7474
|
+
console.log(` ${ansis.yellow(i18n.t("notification:channels.noChannels"))}`);
|
|
7475
|
+
} else {
|
|
7476
|
+
for (const channel of summary.enabledChannels) {
|
|
7477
|
+
console.log(` \u2705 ${i18n.t(`notification:channels.${channel}`)}`);
|
|
7478
|
+
}
|
|
7479
|
+
}
|
|
7480
|
+
if (summary.quietHours.enabled) {
|
|
7481
|
+
console.log("");
|
|
7482
|
+
console.log(` \u{1F319} \u514D\u6253\u6270: ${summary.quietHours.hours}`);
|
|
7483
|
+
}
|
|
7484
|
+
if (!validation.valid) {
|
|
7485
|
+
console.log("");
|
|
7486
|
+
console.log(ansis.bold.red(" \u26A0\uFE0F \u914D\u7F6E\u95EE\u9898:"));
|
|
7487
|
+
for (const error of validation.errors) {
|
|
7488
|
+
console.log(` \u274C ${error.message}`);
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
if (validation.warnings.length > 0) {
|
|
7492
|
+
console.log("");
|
|
7493
|
+
console.log(ansis.bold.yellow(" \u26A0\uFE0F \u8B66\u544A:"));
|
|
7494
|
+
for (const warning of validation.warnings) {
|
|
7495
|
+
console.log(` \u26A0\uFE0F ${warning}`);
|
|
7496
|
+
}
|
|
7497
|
+
}
|
|
7498
|
+
console.log("");
|
|
7499
|
+
}
|
|
7500
|
+
async function sendTestNotification() {
|
|
7501
|
+
const enabledChannels = await getEnabledChannels();
|
|
7502
|
+
if (enabledChannels.length === 0) {
|
|
7503
|
+
console.log(ansis.yellow(i18n.t("notification:errors.noChannels")));
|
|
7504
|
+
return;
|
|
7505
|
+
}
|
|
7506
|
+
console.log("");
|
|
7507
|
+
console.log(ansis.cyan(i18n.t("notification:test.sending")));
|
|
7508
|
+
try {
|
|
7509
|
+
const client = CloudClient.getInstance();
|
|
7510
|
+
await client.initialize();
|
|
7511
|
+
const results = await client.sendTestNotification();
|
|
7512
|
+
console.log("");
|
|
7513
|
+
let hasSuccess = false;
|
|
7514
|
+
let hasFailure = false;
|
|
7515
|
+
for (const result of results) {
|
|
7516
|
+
const channelName = i18n.t(`notification:channels.${result.channel}`);
|
|
7517
|
+
if (result.success) {
|
|
7518
|
+
console.log(ansis.green(`\u2705 ${channelName}: ${i18n.t("notification:test.success")}`));
|
|
7519
|
+
hasSuccess = true;
|
|
7520
|
+
} else {
|
|
7521
|
+
console.log(ansis.red(`\u274C ${channelName}: ${result.error || i18n.t("notification:test.failed")}`));
|
|
7522
|
+
hasFailure = true;
|
|
7523
|
+
}
|
|
7524
|
+
}
|
|
7525
|
+
console.log("");
|
|
7526
|
+
if (hasSuccess) {
|
|
7527
|
+
console.log(ansis.dim(i18n.t("notification:test.checkDevice")));
|
|
7528
|
+
}
|
|
7529
|
+
if (hasFailure) {
|
|
7530
|
+
console.log(ansis.yellow(i18n.t("notification:test.partialFailure")));
|
|
7531
|
+
}
|
|
7532
|
+
} catch (error) {
|
|
7533
|
+
console.log("");
|
|
7534
|
+
console.log(ansis.red(`\u274C ${i18n.t("notification:errors.sendFailed")}`));
|
|
7535
|
+
if (error instanceof Error) {
|
|
7536
|
+
console.log(ansis.dim(error.message));
|
|
7537
|
+
}
|
|
7538
|
+
console.log("");
|
|
7539
|
+
console.log(ansis.yellow(i18n.t("notification:test.troubleshooting")));
|
|
7540
|
+
console.log(ansis.dim(` 1. ${i18n.t("notification:test.checkConnection")}`));
|
|
7541
|
+
console.log(ansis.dim(` 2. ${i18n.t("notification:test.checkConfig")}`));
|
|
7542
|
+
console.log(ansis.dim(` 3. ${i18n.t("notification:test.checkToken")}`));
|
|
7543
|
+
}
|
|
7544
|
+
console.log("");
|
|
7545
|
+
}
|
|
7546
|
+
|
|
6312
7547
|
const SESSIONS_DIR = join$1(homedir(), ".ccjk", "sessions");
|
|
6313
7548
|
async function saveSession() {
|
|
6314
7549
|
try {
|
|
@@ -7330,6 +8565,9 @@ async function setupCommands(cli) {
|
|
|
7330
8565
|
cli.command("workflows", "Manage installed workflows").alias("wf").option("--lang, -l <lang>", "Display language (zh-CN, en)").action(await withLanguageResolution(async () => {
|
|
7331
8566
|
await showWorkflows();
|
|
7332
8567
|
}));
|
|
8568
|
+
cli.command("notification [action]", "Manage task completion notifications (config, status, test, enable, disable, channels)").alias("notify").option("--lang, -l <lang>", "Display language (zh-CN, en)").action(await withLanguageResolution(async (action) => {
|
|
8569
|
+
await notificationCommand(action);
|
|
8570
|
+
}));
|
|
7333
8571
|
cli.help((sections) => customizeHelp(sections));
|
|
7334
8572
|
cli.version(version);
|
|
7335
8573
|
}
|