coding-tool-x 3.6.7 → 3.6.9
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/web/assets/{Analytics-CKycBI0G.js → Analytics-BuBlsbcI.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-DQczxuOX.js} +1 -1
- package/dist/web/assets/Home-BvAzzWnB.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-C8gAlaps.js} +1 -1
- package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-nqgi6_cR.js} +1 -1
- package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-D8AxMYy6.js} +1 -1
- package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-BDbNbNkK.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-DGGaGW2c.js} +1 -1
- package/dist/web/assets/index-Dsh4Q5Cg.css +1 -0
- package/dist/web/assets/index-y-eEayxR.js +2 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +46 -69
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +1137 -86
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +198 -65
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-fWgvlZQx.js +0 -2
|
@@ -3,6 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const https = require('https');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { HttpsProxyAgent } = require('https-proxy-agent');
|
|
6
8
|
const { execSync, execFileSync } = require('child_process');
|
|
7
9
|
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
8
10
|
const { loadUIConfig, saveUIConfig } = require('./ui-config');
|
|
@@ -11,11 +13,35 @@ const geminiSettingsManager = require('./gemini-settings-manager');
|
|
|
11
13
|
|
|
12
14
|
const MANAGED_HOOK_NAME = 'coding-tool-notify';
|
|
13
15
|
const MANAGED_OPENCODE_PLUGIN_FILE = 'coding-tool-notify.js';
|
|
16
|
+
const REMOTE_PROVIDER_TYPES = [
|
|
17
|
+
'wechatBot',
|
|
18
|
+
'qqBot',
|
|
19
|
+
'feishuBot',
|
|
20
|
+
'wecomBot',
|
|
21
|
+
'dingtalkBot',
|
|
22
|
+
'telegramBot'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const REMOTE_PROVIDER_LABELS = {
|
|
26
|
+
wechatBot: '微信 Bot',
|
|
27
|
+
qqBot: 'QQ Bot',
|
|
28
|
+
feishuBot: '飞书 Bot',
|
|
29
|
+
wecomBot: '企业微信 Bot',
|
|
30
|
+
dingtalkBot: '钉钉 Bot',
|
|
31
|
+
telegramBot: 'Telegram Bot'
|
|
32
|
+
};
|
|
14
33
|
|
|
15
34
|
function normalizeType(type) {
|
|
16
35
|
return type === 'dialog' || type === 'browser' ? type : 'notification';
|
|
17
36
|
}
|
|
18
37
|
|
|
38
|
+
function createClientId(prefix = 'coding-tool') {
|
|
39
|
+
const randomPart = typeof crypto.randomUUID === 'function'
|
|
40
|
+
? crypto.randomUUID().replace(/-/g, '').slice(0, 16)
|
|
41
|
+
: crypto.randomBytes(8).toString('hex');
|
|
42
|
+
return `${prefix}-${randomPart}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
19
45
|
function ensureParentDir(filePath) {
|
|
20
46
|
const dir = path.dirname(filePath);
|
|
21
47
|
if (!fs.existsSync(dir)) {
|
|
@@ -49,14 +75,94 @@ function writeClaudeSettings(settings) {
|
|
|
49
75
|
writeJsonFile(NATIVE_PATHS.claude.settings, settings);
|
|
50
76
|
}
|
|
51
77
|
|
|
52
|
-
function
|
|
53
|
-
|
|
78
|
+
function trimString(value) {
|
|
79
|
+
return String(value || '').trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeRemoteProviderType(type) {
|
|
83
|
+
const value = trimString(type);
|
|
84
|
+
return REMOTE_PROVIDER_TYPES.includes(value) ? value : '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createRemoteProviderId(type, index = 0) {
|
|
88
|
+
return `${type || 'provider'}-${Date.now().toString(36)}-${index}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeProviderConfig(type, config = {}) {
|
|
92
|
+
const source = config && typeof config === 'object' ? config : {};
|
|
93
|
+
switch (type) {
|
|
94
|
+
case 'wechatBot':
|
|
95
|
+
return {
|
|
96
|
+
tokenFile: trimString(source.tokenFile),
|
|
97
|
+
botToken: trimString(source.botToken),
|
|
98
|
+
targetUserId: trimString(source.targetUserId),
|
|
99
|
+
contextToken: trimString(source.contextToken)
|
|
100
|
+
};
|
|
101
|
+
case 'qqBot':
|
|
102
|
+
return {
|
|
103
|
+
endpoint: trimString(source.endpoint),
|
|
104
|
+
accessToken: trimString(source.accessToken),
|
|
105
|
+
targetType: source.targetType === 'group' ? 'group' : 'private',
|
|
106
|
+
targetId: trimString(source.targetId)
|
|
107
|
+
};
|
|
108
|
+
case 'feishuBot':
|
|
109
|
+
return {
|
|
110
|
+
webhookUrl: trimString(source.webhookUrl)
|
|
111
|
+
};
|
|
112
|
+
case 'wecomBot':
|
|
113
|
+
return {
|
|
114
|
+
webhookUrl: trimString(source.webhookUrl)
|
|
115
|
+
};
|
|
116
|
+
case 'dingtalkBot':
|
|
117
|
+
return {
|
|
118
|
+
mode: source.mode === 'app' ? 'app' : 'webhook',
|
|
119
|
+
webhookUrl: trimString(source.webhookUrl),
|
|
120
|
+
clientId: trimString(source.clientId),
|
|
121
|
+
clientSecret: trimString(source.clientSecret),
|
|
122
|
+
targetType: source.targetType === 'user' ? 'user' : 'group',
|
|
123
|
+
targetId: trimString(source.targetId)
|
|
124
|
+
};
|
|
125
|
+
case 'telegramBot':
|
|
126
|
+
return {
|
|
127
|
+
botToken: trimString(source.botToken),
|
|
128
|
+
chatId: trimString(source.chatId),
|
|
129
|
+
proxy: trimString(source.proxy)
|
|
130
|
+
};
|
|
131
|
+
default:
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeRemoteProvider(provider = {}, index = 0) {
|
|
137
|
+
const type = normalizeRemoteProviderType(provider.type);
|
|
138
|
+
if (!type) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
54
142
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
143
|
+
id: trimString(provider.id) || createRemoteProviderId(type, index),
|
|
144
|
+
type,
|
|
145
|
+
name: trimString(provider.name) || REMOTE_PROVIDER_LABELS[type],
|
|
146
|
+
enabled: provider.enabled === true,
|
|
147
|
+
config: normalizeProviderConfig(type, provider.config)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeRemoteNotificationsConfig(remoteNotifications = {}) {
|
|
152
|
+
const source = remoteNotifications && typeof remoteNotifications === 'object' ? remoteNotifications : {};
|
|
153
|
+
const providers = Array.isArray(source.providers)
|
|
154
|
+
? source.providers.map((provider, index) => normalizeRemoteProvider(provider, index)).filter(Boolean)
|
|
155
|
+
: [];
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
providers
|
|
57
159
|
};
|
|
58
160
|
}
|
|
59
161
|
|
|
162
|
+
function getRemoteNotificationsConfig(uiConfig = loadUIConfig()) {
|
|
163
|
+
return normalizeRemoteNotificationsConfig(uiConfig.remoteNotifications);
|
|
164
|
+
}
|
|
165
|
+
|
|
60
166
|
function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
|
|
61
167
|
const nextConfig = (uiConfig && typeof uiConfig === 'object') ? { ...uiConfig } : {};
|
|
62
168
|
if (claudeEnabled) {
|
|
@@ -67,15 +173,13 @@ function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
|
|
|
67
173
|
return nextConfig;
|
|
68
174
|
}
|
|
69
175
|
|
|
70
|
-
function saveNotificationUiConfig(
|
|
176
|
+
function saveNotificationUiConfig(remoteNotifications = {}, claudeEnabled) {
|
|
71
177
|
let uiConfig = loadUIConfig();
|
|
72
178
|
if (typeof claudeEnabled === 'boolean') {
|
|
73
179
|
uiConfig = applyClaudeDisablePreference(uiConfig, claudeEnabled);
|
|
74
180
|
}
|
|
75
|
-
uiConfig.
|
|
76
|
-
|
|
77
|
-
webhookUrl: feishu.webhookUrl || ''
|
|
78
|
-
};
|
|
181
|
+
uiConfig.remoteNotifications = normalizeRemoteNotificationsConfig(remoteNotifications);
|
|
182
|
+
delete uiConfig.feishuNotification;
|
|
79
183
|
saveUIConfig(uiConfig);
|
|
80
184
|
}
|
|
81
185
|
|
|
@@ -109,6 +213,92 @@ function validateFeishuWebhookUrl(webhookUrl) {
|
|
|
109
213
|
return urlObj;
|
|
110
214
|
}
|
|
111
215
|
|
|
216
|
+
function validateHttpsUrl(value, label, options = {}) {
|
|
217
|
+
const raw = trimString(value);
|
|
218
|
+
if (!raw) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let urlObj;
|
|
223
|
+
try {
|
|
224
|
+
urlObj = new URL(raw);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw createValidationError(`${label} 格式不正确`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (options.requireHttps !== false && urlObj.protocol !== 'https:') {
|
|
230
|
+
throw createValidationError(`${label} 必须使用 HTTPS`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (options.hostname && urlObj.hostname !== options.hostname) {
|
|
234
|
+
throw createValidationError(`${label} 仅支持 ${options.hostname}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return urlObj;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function requireConfigValue(config, key, label) {
|
|
241
|
+
if (!trimString(config?.[key])) {
|
|
242
|
+
throw createValidationError(`请填写${label}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function validateRemoteProviderConfig(provider = {}) {
|
|
247
|
+
const type = normalizeRemoteProviderType(provider.type);
|
|
248
|
+
const config = provider.config || {};
|
|
249
|
+
if (!type) {
|
|
250
|
+
throw createValidationError('远程通知渠道类型不正确');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
switch (type) {
|
|
254
|
+
case 'wechatBot':
|
|
255
|
+
if (!trimString(config.botToken) && !trimString(config.tokenFile)) {
|
|
256
|
+
throw createValidationError('请填写微信 Bot Token 或 token.json 路径');
|
|
257
|
+
}
|
|
258
|
+
requireConfigValue(config, 'targetUserId', '微信接收用户 ID');
|
|
259
|
+
break;
|
|
260
|
+
case 'qqBot':
|
|
261
|
+
requireConfigValue(config, 'endpoint', 'QQ Bot HTTP 地址');
|
|
262
|
+
validateHttpsUrl(config.endpoint, 'QQ Bot HTTP 地址', { requireHttps: false });
|
|
263
|
+
requireConfigValue(config, 'targetId', 'QQ 接收对象 ID');
|
|
264
|
+
break;
|
|
265
|
+
case 'feishuBot':
|
|
266
|
+
validateFeishuWebhookUrl(config.webhookUrl);
|
|
267
|
+
requireConfigValue(config, 'webhookUrl', '飞书 Webhook URL');
|
|
268
|
+
break;
|
|
269
|
+
case 'wecomBot':
|
|
270
|
+
validateHttpsUrl(config.webhookUrl, '企业微信 Webhook URL');
|
|
271
|
+
requireConfigValue(config, 'webhookUrl', '企业微信 Webhook URL');
|
|
272
|
+
break;
|
|
273
|
+
case 'dingtalkBot':
|
|
274
|
+
if (config.mode === 'app') {
|
|
275
|
+
requireConfigValue(config, 'clientId', '钉钉 App Key');
|
|
276
|
+
requireConfigValue(config, 'clientSecret', '钉钉 App Secret');
|
|
277
|
+
requireConfigValue(config, 'targetId', '钉钉接收对象 ID');
|
|
278
|
+
} else {
|
|
279
|
+
validateHttpsUrl(config.webhookUrl, '钉钉 Webhook URL');
|
|
280
|
+
requireConfigValue(config, 'webhookUrl', '钉钉 Webhook URL');
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
case 'telegramBot':
|
|
284
|
+
requireConfigValue(config, 'botToken', 'Telegram Bot Token');
|
|
285
|
+
requireConfigValue(config, 'chatId', 'Telegram Chat ID');
|
|
286
|
+
break;
|
|
287
|
+
default:
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function validateRemoteNotifications(remoteNotifications = {}) {
|
|
293
|
+
const remote = normalizeRemoteNotificationsConfig(remoteNotifications);
|
|
294
|
+
remote.providers.forEach((provider) => {
|
|
295
|
+
if (provider.enabled === true) {
|
|
296
|
+
validateRemoteProviderConfig(provider);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return remote;
|
|
300
|
+
}
|
|
301
|
+
|
|
112
302
|
function removeNotifyScript() {
|
|
113
303
|
if (fs.existsSync(PATHS.notifyHook)) {
|
|
114
304
|
fs.unlinkSync(PATHS.notifyHook);
|
|
@@ -433,8 +623,9 @@ function runWindowsPowerShellCommand(command) {
|
|
|
433
623
|
});
|
|
434
624
|
}
|
|
435
625
|
|
|
436
|
-
function generateNotifyScript(
|
|
437
|
-
const
|
|
626
|
+
function generateNotifyScript(remoteNotifications = {}) {
|
|
627
|
+
const remote = normalizeRemoteNotificationsConfig(remoteNotifications);
|
|
628
|
+
const enabledProviders = remote.providers.filter((provider) => provider.enabled === true);
|
|
438
629
|
|
|
439
630
|
return `#!/usr/bin/env node
|
|
440
631
|
// Coding Tool 通知脚本 - 自动生成,请勿手动修改
|
|
@@ -442,12 +633,28 @@ const fs = require('fs')
|
|
|
442
633
|
const os = require('os')
|
|
443
634
|
const http = require('http')
|
|
444
635
|
const https = require('https')
|
|
636
|
+
const crypto = require('crypto')
|
|
445
637
|
const { execSync, execFileSync } = require('child_process')
|
|
446
638
|
|
|
447
|
-
|
|
448
|
-
|
|
639
|
+
function createHttpsProxyAgent(proxyUrl) {
|
|
640
|
+
try {
|
|
641
|
+
const { HttpsProxyAgent } = require('https-proxy-agent')
|
|
642
|
+
return new HttpsProxyAgent(proxyUrl)
|
|
643
|
+
} catch (error) {
|
|
644
|
+
return null
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const REMOTE_PROVIDERS = ${JSON.stringify(enabledProviders)}
|
|
449
649
|
const CONFIG_FILE = ${JSON.stringify(PATHS.configFile)}
|
|
450
650
|
|
|
651
|
+
function createClientId(prefix = 'coding-tool') {
|
|
652
|
+
const randomPart = typeof crypto.randomUUID === 'function'
|
|
653
|
+
? crypto.randomUUID().replace(/-/g, '').slice(0, 16)
|
|
654
|
+
: crypto.randomBytes(8).toString('hex')
|
|
655
|
+
return prefix + '-' + randomPart
|
|
656
|
+
}
|
|
657
|
+
|
|
451
658
|
function readArg(name) {
|
|
452
659
|
const prefix = \`\${name}=\`
|
|
453
660
|
const matched = process.argv.slice(2).find((arg) => String(arg || '').startsWith(prefix))
|
|
@@ -632,43 +839,11 @@ function notify(mode, message) {
|
|
|
632
839
|
}
|
|
633
840
|
}
|
|
634
841
|
|
|
635
|
-
function
|
|
636
|
-
if (!FEISHU_ENABLED || !FEISHU_WEBHOOK_URL) {
|
|
637
|
-
return Promise.resolve()
|
|
638
|
-
}
|
|
639
|
-
|
|
842
|
+
function postJson(url, data, headers = {}, extraOptions = {}) {
|
|
640
843
|
return new Promise((resolve) => {
|
|
641
844
|
try {
|
|
642
|
-
const urlObj = new URL(
|
|
643
|
-
const
|
|
644
|
-
const data = JSON.stringify({
|
|
645
|
-
msg_type: 'interactive',
|
|
646
|
-
card: {
|
|
647
|
-
header: {
|
|
648
|
-
title: { tag: 'plain_text', content: 'Coding Tool - 通知' },
|
|
649
|
-
template: 'green'
|
|
650
|
-
},
|
|
651
|
-
elements: [
|
|
652
|
-
{
|
|
653
|
-
tag: 'div',
|
|
654
|
-
text: { tag: 'lark_md', content: '**来源**: ' + source }
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
tag: 'div',
|
|
658
|
-
text: { tag: 'lark_md', content: '**状态**: ' + message }
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
tag: 'div',
|
|
662
|
-
text: { tag: 'lark_md', content: '**时间**: ' + timestamp }
|
|
663
|
-
},
|
|
664
|
-
{
|
|
665
|
-
tag: 'div',
|
|
666
|
-
text: { tag: 'lark_md', content: '**设备**: ' + os.hostname() }
|
|
667
|
-
}
|
|
668
|
-
]
|
|
669
|
-
}
|
|
670
|
-
})
|
|
671
|
-
|
|
845
|
+
const urlObj = new URL(url)
|
|
846
|
+
const body = JSON.stringify(data)
|
|
672
847
|
const options = {
|
|
673
848
|
hostname: urlObj.hostname,
|
|
674
849
|
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
@@ -676,19 +851,24 @@ function sendFeishu(message, source) {
|
|
|
676
851
|
method: 'POST',
|
|
677
852
|
headers: {
|
|
678
853
|
'Content-Type': 'application/json',
|
|
679
|
-
'Content-Length': Buffer.byteLength(
|
|
854
|
+
'Content-Length': Buffer.byteLength(body),
|
|
855
|
+
...headers
|
|
680
856
|
},
|
|
857
|
+
...extraOptions,
|
|
681
858
|
timeout: 10000
|
|
682
859
|
}
|
|
683
860
|
|
|
684
861
|
const requestModule = urlObj.protocol === 'https:' ? https : http
|
|
685
|
-
const request = requestModule.request(options, () =>
|
|
862
|
+
const request = requestModule.request(options, (response) => {
|
|
863
|
+
response.resume()
|
|
864
|
+
resolve()
|
|
865
|
+
})
|
|
686
866
|
request.on('error', () => resolve())
|
|
687
867
|
request.on('timeout', () => {
|
|
688
868
|
request.destroy()
|
|
689
869
|
resolve()
|
|
690
870
|
})
|
|
691
|
-
request.write(
|
|
871
|
+
request.write(body)
|
|
692
872
|
request.end()
|
|
693
873
|
} catch (error) {
|
|
694
874
|
resolve()
|
|
@@ -696,6 +876,200 @@ function sendFeishu(message, source) {
|
|
|
696
876
|
})
|
|
697
877
|
}
|
|
698
878
|
|
|
879
|
+
function sendTelegramMessage(config = {}, ctx, send = postJson) {
|
|
880
|
+
const url = 'https://api.telegram.org/bot' + config.botToken + '/sendMessage'
|
|
881
|
+
const payload = {
|
|
882
|
+
chat_id: config.chatId,
|
|
883
|
+
text: formatPlainMessage(ctx)
|
|
884
|
+
}
|
|
885
|
+
const agent = config.proxy ? createHttpsProxyAgent(config.proxy) : null
|
|
886
|
+
return send(url, payload, {}, agent ? { agent } : {})
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function buildNotificationContext(message, source, eventType) {
|
|
890
|
+
const timestamp = new Date().toLocaleString('zh-CN')
|
|
891
|
+
return {
|
|
892
|
+
title: 'Coding Tool - 通知',
|
|
893
|
+
message,
|
|
894
|
+
source,
|
|
895
|
+
eventType,
|
|
896
|
+
timestamp,
|
|
897
|
+
hostname: os.hostname()
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function formatPlainMessage(ctx) {
|
|
902
|
+
return [
|
|
903
|
+
ctx.title,
|
|
904
|
+
'来源: ' + ctx.source,
|
|
905
|
+
'状态: ' + ctx.message,
|
|
906
|
+
'时间: ' + ctx.timestamp,
|
|
907
|
+
'设备: ' + ctx.hostname
|
|
908
|
+
].join('\\n')
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function readWechatToken(config = {}) {
|
|
912
|
+
if (config.botToken) return config.botToken
|
|
913
|
+
if (!config.tokenFile) return ''
|
|
914
|
+
try {
|
|
915
|
+
const raw = fs.readFileSync(String(config.tokenFile), 'utf8')
|
|
916
|
+
const parsed = JSON.parse(raw)
|
|
917
|
+
return parsed.bot_token || parsed.token || ''
|
|
918
|
+
} catch (error) {
|
|
919
|
+
return ''
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function fetchDingTalkAccessToken(config = {}) {
|
|
924
|
+
if (!config.clientId || !config.clientSecret) return Promise.resolve('')
|
|
925
|
+
return new Promise((resolve) => {
|
|
926
|
+
try {
|
|
927
|
+
const data = JSON.stringify({ appKey: config.clientId, appSecret: config.clientSecret })
|
|
928
|
+
const req = https.request({
|
|
929
|
+
hostname: 'api.dingtalk.com',
|
|
930
|
+
path: '/v1.0/oauth2/accessToken',
|
|
931
|
+
method: 'POST',
|
|
932
|
+
timeout: 10000,
|
|
933
|
+
headers: {
|
|
934
|
+
'Content-Type': 'application/json',
|
|
935
|
+
'Content-Length': Buffer.byteLength(data)
|
|
936
|
+
}
|
|
937
|
+
}, (response) => {
|
|
938
|
+
let body = ''
|
|
939
|
+
response.setEncoding('utf8')
|
|
940
|
+
response.on('data', (chunk) => { body += chunk })
|
|
941
|
+
response.on('end', () => {
|
|
942
|
+
try {
|
|
943
|
+
const parsed = JSON.parse(body)
|
|
944
|
+
resolve(parsed.accessToken || '')
|
|
945
|
+
} catch (error) {
|
|
946
|
+
resolve('')
|
|
947
|
+
}
|
|
948
|
+
})
|
|
949
|
+
})
|
|
950
|
+
req.on('error', () => resolve(''))
|
|
951
|
+
req.on('timeout', () => {
|
|
952
|
+
req.destroy()
|
|
953
|
+
resolve('')
|
|
954
|
+
})
|
|
955
|
+
req.write(data)
|
|
956
|
+
req.end()
|
|
957
|
+
} catch (error) {
|
|
958
|
+
resolve('')
|
|
959
|
+
}
|
|
960
|
+
})
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
async function sendDingTalkAppMessage(config = {}, ctx) {
|
|
964
|
+
const token = await fetchDingTalkAccessToken(config)
|
|
965
|
+
if (!token || !config.targetId) return
|
|
966
|
+
const isGroup = config.targetType !== 'user'
|
|
967
|
+
const payload = {
|
|
968
|
+
robotCode: config.clientId,
|
|
969
|
+
msgKey: 'sampleMarkdown',
|
|
970
|
+
msgParam: JSON.stringify({
|
|
971
|
+
title: ctx.title,
|
|
972
|
+
text: '### ' + ctx.title + '\\n\\n- 来源: ' + ctx.source + '\\n- 状态: ' + ctx.message + '\\n- 时间: ' + ctx.timestamp + '\\n- 设备: ' + ctx.hostname
|
|
973
|
+
})
|
|
974
|
+
}
|
|
975
|
+
if (isGroup) {
|
|
976
|
+
payload.openConversationId = config.targetId
|
|
977
|
+
} else {
|
|
978
|
+
payload.userIds = [config.targetId]
|
|
979
|
+
}
|
|
980
|
+
return postJson(
|
|
981
|
+
'https://api.dingtalk.com/v1.0/robot/' + (isGroup ? 'groupMessages/send' : 'oToMessages/batchSend'),
|
|
982
|
+
payload,
|
|
983
|
+
{ 'x-acs-dingtalk-access-token': token }
|
|
984
|
+
)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function sendRemoteProvider(provider, ctx) {
|
|
988
|
+
const config = provider?.config || {}
|
|
989
|
+
switch (provider?.type) {
|
|
990
|
+
case 'wechatBot': {
|
|
991
|
+
const token = readWechatToken(config)
|
|
992
|
+
if (!token || !config.targetUserId) return Promise.resolve()
|
|
993
|
+
return postJson('https://ilinkai.weixin.qq.com/ilink/bot/sendmessage', {
|
|
994
|
+
msg: {
|
|
995
|
+
from_user_id: '',
|
|
996
|
+
to_user_id: config.targetUserId,
|
|
997
|
+
client_id: createClientId('coding-tool'),
|
|
998
|
+
message_type: 2,
|
|
999
|
+
message_state: 2,
|
|
1000
|
+
item_list: [{ type: 1, text_item: { text: formatPlainMessage(ctx) } }],
|
|
1001
|
+
...(config.contextToken ? { context_token: config.contextToken } : {})
|
|
1002
|
+
},
|
|
1003
|
+
base_info: { channel_version: '2.1.8' }
|
|
1004
|
+
}, {
|
|
1005
|
+
AuthorizationType: 'ilink_bot_token',
|
|
1006
|
+
Authorization: 'Bearer ' + token,
|
|
1007
|
+
'X-WECHAT-UIN': Buffer.from(String(Math.floor(Math.random() * 0xffffffff))).toString('base64')
|
|
1008
|
+
})
|
|
1009
|
+
}
|
|
1010
|
+
case 'qqBot': {
|
|
1011
|
+
if (!config.endpoint || !config.targetId) return Promise.resolve()
|
|
1012
|
+
const path = config.targetType === 'group' ? '/send_group_msg' : '/send_private_msg'
|
|
1013
|
+
const base = String(config.endpoint).replace(/\\/$/, '')
|
|
1014
|
+
return postJson(base + path, {
|
|
1015
|
+
[config.targetType === 'group' ? 'group_id' : 'user_id']: config.targetId,
|
|
1016
|
+
message: formatPlainMessage(ctx)
|
|
1017
|
+
}, config.accessToken ? { Authorization: 'Bearer ' + config.accessToken } : {})
|
|
1018
|
+
}
|
|
1019
|
+
case 'feishuBot': {
|
|
1020
|
+
if (!config.webhookUrl) return Promise.resolve()
|
|
1021
|
+
return postJson(config.webhookUrl, {
|
|
1022
|
+
msg_type: 'interactive',
|
|
1023
|
+
card: {
|
|
1024
|
+
header: {
|
|
1025
|
+
title: { tag: 'plain_text', content: ctx.title },
|
|
1026
|
+
template: 'green'
|
|
1027
|
+
},
|
|
1028
|
+
elements: [
|
|
1029
|
+
{ tag: 'div', text: { tag: 'lark_md', content: '**来源**: ' + ctx.source } },
|
|
1030
|
+
{ tag: 'div', text: { tag: 'lark_md', content: '**状态**: ' + ctx.message } },
|
|
1031
|
+
{ tag: 'div', text: { tag: 'lark_md', content: '**时间**: ' + ctx.timestamp } },
|
|
1032
|
+
{ tag: 'div', text: { tag: 'lark_md', content: '**设备**: ' + ctx.hostname } }
|
|
1033
|
+
]
|
|
1034
|
+
}
|
|
1035
|
+
})
|
|
1036
|
+
}
|
|
1037
|
+
case 'wecomBot': {
|
|
1038
|
+
if (!config.webhookUrl) return Promise.resolve()
|
|
1039
|
+
return postJson(config.webhookUrl, {
|
|
1040
|
+
msgtype: 'markdown',
|
|
1041
|
+
markdown: {
|
|
1042
|
+
content: '**' + ctx.title + '**\\n> 来源: ' + ctx.source + '\\n> 状态: ' + ctx.message + '\\n> 时间: ' + ctx.timestamp + '\\n> 设备: ' + ctx.hostname
|
|
1043
|
+
}
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
1046
|
+
case 'dingtalkBot': {
|
|
1047
|
+
if (config.mode === 'app') return sendDingTalkAppMessage(config, ctx)
|
|
1048
|
+
if (!config.webhookUrl) return Promise.resolve()
|
|
1049
|
+
return postJson(config.webhookUrl, {
|
|
1050
|
+
msgtype: 'markdown',
|
|
1051
|
+
markdown: {
|
|
1052
|
+
title: ctx.title,
|
|
1053
|
+
text: '### ' + ctx.title + '\\n\\n- 来源: ' + ctx.source + '\\n- 状态: ' + ctx.message + '\\n- 时间: ' + ctx.timestamp + '\\n- 设备: ' + ctx.hostname
|
|
1054
|
+
}
|
|
1055
|
+
})
|
|
1056
|
+
}
|
|
1057
|
+
case 'telegramBot': {
|
|
1058
|
+
if (!config.botToken || !config.chatId) return Promise.resolve()
|
|
1059
|
+
return sendTelegramMessage(config, ctx)
|
|
1060
|
+
}
|
|
1061
|
+
default:
|
|
1062
|
+
return Promise.resolve()
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function sendRemoteNotifications(message, source, eventType) {
|
|
1067
|
+
const ctx = buildNotificationContext(message, source, eventType)
|
|
1068
|
+
for (const provider of REMOTE_PROVIDERS) {
|
|
1069
|
+
await sendRemoteProvider(provider, ctx)
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
699
1073
|
(async () => {
|
|
700
1074
|
const source = readArg('--source') || 'claude'
|
|
701
1075
|
const mode = readArg('--mode') || readArg('--cc-notify-type') || 'notification'
|
|
@@ -708,7 +1082,7 @@ function sendFeishu(message, source) {
|
|
|
708
1082
|
} else {
|
|
709
1083
|
notify(mode, message)
|
|
710
1084
|
}
|
|
711
|
-
await
|
|
1085
|
+
await sendRemoteNotifications(message, source, eventType)
|
|
712
1086
|
})().catch(() => {
|
|
713
1087
|
process.exit(0)
|
|
714
1088
|
})
|
|
@@ -928,9 +1302,9 @@ function runWindowsPowerShellCommand(command) {
|
|
|
928
1302
|
`;
|
|
929
1303
|
}
|
|
930
1304
|
|
|
931
|
-
function writeNotifyScript(
|
|
1305
|
+
function writeNotifyScript(remoteNotifications = {}) {
|
|
932
1306
|
ensureParentDir(PATHS.notifyHook);
|
|
933
|
-
fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(
|
|
1307
|
+
fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(remoteNotifications), { mode: 0o755 });
|
|
934
1308
|
}
|
|
935
1309
|
|
|
936
1310
|
function getClaudeHookStatus() {
|
|
@@ -1235,10 +1609,16 @@ function saveOpenCodeHook(enabled, type) {
|
|
|
1235
1609
|
}
|
|
1236
1610
|
|
|
1237
1611
|
function getNotificationSettings() {
|
|
1612
|
+
const uiConfig = loadUIConfig();
|
|
1613
|
+
const remoteNotifications = getRemoteNotificationsConfig(uiConfig);
|
|
1238
1614
|
return {
|
|
1239
1615
|
success: true,
|
|
1240
1616
|
platform: os.platform(),
|
|
1241
|
-
|
|
1617
|
+
remoteNotifications,
|
|
1618
|
+
remoteProviderTypes: REMOTE_PROVIDER_TYPES.map((type) => ({
|
|
1619
|
+
type,
|
|
1620
|
+
label: REMOTE_PROVIDER_LABELS[type]
|
|
1621
|
+
})),
|
|
1242
1622
|
platforms: {
|
|
1243
1623
|
claude: getClaudeHookStatus(),
|
|
1244
1624
|
codex: getCodexHookStatus(),
|
|
@@ -1305,15 +1685,11 @@ function emitBrowserNotification(input = {}) {
|
|
|
1305
1685
|
}
|
|
1306
1686
|
|
|
1307
1687
|
function saveNotificationSettings(input = {}) {
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
};
|
|
1314
|
-
if (feishu.enabled && feishu.webhookUrl) {
|
|
1315
|
-
validateFeishuWebhookUrl(feishu.webhookUrl);
|
|
1316
|
-
}
|
|
1688
|
+
const remoteNotifications = validateRemoteNotifications(
|
|
1689
|
+
input.remoteNotifications !== undefined
|
|
1690
|
+
? input.remoteNotifications
|
|
1691
|
+
: {}
|
|
1692
|
+
);
|
|
1317
1693
|
const platforms = {
|
|
1318
1694
|
claude: normalizePlatformInput(input?.platforms?.claude),
|
|
1319
1695
|
codex: normalizePlatformInput(input?.platforms?.codex),
|
|
@@ -1321,11 +1697,11 @@ function saveNotificationSettings(input = {}) {
|
|
|
1321
1697
|
opencode: normalizePlatformInput(input?.platforms?.opencode)
|
|
1322
1698
|
};
|
|
1323
1699
|
|
|
1324
|
-
saveNotificationUiConfig(
|
|
1700
|
+
saveNotificationUiConfig(remoteNotifications, platforms.claude.enabled);
|
|
1325
1701
|
|
|
1326
1702
|
const hasManagedPlatform = Object.values(platforms).some((platform) => platform.enabled);
|
|
1327
1703
|
if (hasManagedPlatform) {
|
|
1328
|
-
writeNotifyScript(
|
|
1704
|
+
writeNotifyScript(remoteNotifications);
|
|
1329
1705
|
}
|
|
1330
1706
|
|
|
1331
1707
|
saveClaudeHook(platforms.claude.enabled, platforms.claude.type);
|
|
@@ -1460,15 +1836,7 @@ function buildLegacyClaudeSaveInput(input = {}, currentSettings = getNotificatio
|
|
|
1460
1836
|
gemini: normalizeSavedPlatformStatus(currentSettings?.platforms?.gemini),
|
|
1461
1837
|
opencode: normalizeSavedPlatformStatus(currentSettings?.platforms?.opencode)
|
|
1462
1838
|
},
|
|
1463
|
-
|
|
1464
|
-
? {
|
|
1465
|
-
enabled: input.feishu?.enabled === true,
|
|
1466
|
-
webhookUrl: input.feishu?.webhookUrl || ''
|
|
1467
|
-
}
|
|
1468
|
-
: {
|
|
1469
|
-
enabled: currentSettings?.feishu?.enabled === true,
|
|
1470
|
-
webhookUrl: currentSettings?.feishu?.webhookUrl || ''
|
|
1471
|
-
}
|
|
1839
|
+
remoteNotifications: currentSettings?.remoteNotifications || { providers: [] }
|
|
1472
1840
|
};
|
|
1473
1841
|
}
|
|
1474
1842
|
|
|
@@ -1476,7 +1844,6 @@ function getLegacyClaudeHookSettings() {
|
|
|
1476
1844
|
return {
|
|
1477
1845
|
success: true,
|
|
1478
1846
|
stopHook: parseStopHookStatus(readClaudeSettings()),
|
|
1479
|
-
feishu: getFeishuConfig(),
|
|
1480
1847
|
platform: os.platform()
|
|
1481
1848
|
};
|
|
1482
1849
|
}
|
|
@@ -1508,10 +1875,7 @@ function initDefaultHooks() {
|
|
|
1508
1875
|
gemini: normalizeSavedPlatformStatus(currentSettings?.platforms?.gemini),
|
|
1509
1876
|
opencode: normalizeSavedPlatformStatus(currentSettings?.platforms?.opencode)
|
|
1510
1877
|
},
|
|
1511
|
-
|
|
1512
|
-
enabled: currentSettings?.feishu?.enabled === true,
|
|
1513
|
-
webhookUrl: currentSettings?.feishu?.webhookUrl || ''
|
|
1514
|
-
}
|
|
1878
|
+
remoteNotifications: currentSettings?.remoteNotifications || { providers: [] }
|
|
1515
1879
|
});
|
|
1516
1880
|
console.log('[Claude Hooks] 检测到旧版 Stop hook 路径,已自动修复');
|
|
1517
1881
|
} else {
|
|
@@ -1528,10 +1892,7 @@ function initDefaultHooks() {
|
|
|
1528
1892
|
gemini: normalizeSavedPlatformStatus(currentSettings?.platforms?.gemini),
|
|
1529
1893
|
opencode: normalizeSavedPlatformStatus(currentSettings?.platforms?.opencode)
|
|
1530
1894
|
},
|
|
1531
|
-
|
|
1532
|
-
enabled: currentSettings?.feishu?.enabled === true,
|
|
1533
|
-
webhookUrl: currentSettings?.feishu?.webhookUrl || ''
|
|
1534
|
-
}
|
|
1895
|
+
remoteNotifications: currentSettings?.remoteNotifications || { providers: [] }
|
|
1535
1896
|
});
|
|
1536
1897
|
console.log('[Claude Hooks] 已自动开启任务完成通知(右上角卡片)');
|
|
1537
1898
|
} catch (error) {
|
|
@@ -1589,6 +1950,212 @@ function sendFeishuTest(webhookUrl) {
|
|
|
1589
1950
|
});
|
|
1590
1951
|
}
|
|
1591
1952
|
|
|
1953
|
+
function requestJson(url, data, headers = {}, extraOptions = {}) {
|
|
1954
|
+
return new Promise((resolve, reject) => {
|
|
1955
|
+
try {
|
|
1956
|
+
const urlObj = new URL(url);
|
|
1957
|
+
const body = JSON.stringify(data);
|
|
1958
|
+
const requestModule = urlObj.protocol === 'https:' ? https : http;
|
|
1959
|
+
const request = requestModule.request({
|
|
1960
|
+
hostname: urlObj.hostname,
|
|
1961
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
1962
|
+
path: urlObj.pathname + urlObj.search,
|
|
1963
|
+
method: 'POST',
|
|
1964
|
+
timeout: 10000,
|
|
1965
|
+
headers: {
|
|
1966
|
+
'Content-Type': 'application/json',
|
|
1967
|
+
'Content-Length': Buffer.byteLength(body),
|
|
1968
|
+
...headers
|
|
1969
|
+
},
|
|
1970
|
+
...extraOptions
|
|
1971
|
+
}, (response) => {
|
|
1972
|
+
response.resume();
|
|
1973
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
1974
|
+
resolve();
|
|
1975
|
+
} else {
|
|
1976
|
+
reject(new Error(`远程通知返回 HTTP ${response.statusCode}`));
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
request.on('error', reject);
|
|
1980
|
+
request.on('timeout', () => {
|
|
1981
|
+
request.destroy(new Error('远程通知测试超时'));
|
|
1982
|
+
});
|
|
1983
|
+
request.write(body);
|
|
1984
|
+
request.end();
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
reject(error);
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function requestDingTalkAccessToken(config = {}) {
|
|
1992
|
+
if (!config.clientId || !config.clientSecret) {
|
|
1993
|
+
return Promise.resolve('');
|
|
1994
|
+
}
|
|
1995
|
+
return new Promise((resolve, reject) => {
|
|
1996
|
+
try {
|
|
1997
|
+
const body = JSON.stringify({ appKey: config.clientId, appSecret: config.clientSecret });
|
|
1998
|
+
const request = https.request({
|
|
1999
|
+
hostname: 'api.dingtalk.com',
|
|
2000
|
+
path: '/v1.0/oauth2/accessToken',
|
|
2001
|
+
method: 'POST',
|
|
2002
|
+
timeout: 10000,
|
|
2003
|
+
headers: {
|
|
2004
|
+
'Content-Type': 'application/json',
|
|
2005
|
+
'Content-Length': Buffer.byteLength(body)
|
|
2006
|
+
}
|
|
2007
|
+
}, (response) => {
|
|
2008
|
+
let responseBody = '';
|
|
2009
|
+
response.setEncoding('utf8');
|
|
2010
|
+
response.on('data', (chunk) => { responseBody += chunk; });
|
|
2011
|
+
response.on('end', () => {
|
|
2012
|
+
try {
|
|
2013
|
+
const parsed = JSON.parse(responseBody);
|
|
2014
|
+
if (response.statusCode >= 200 && response.statusCode < 300 && parsed.accessToken) {
|
|
2015
|
+
resolve(parsed.accessToken);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
reject(new Error(`钉钉 Access Token 获取失败: ${responseBody.slice(0, 300)}`));
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
reject(error);
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
});
|
|
2024
|
+
request.on('error', reject);
|
|
2025
|
+
request.on('timeout', () => {
|
|
2026
|
+
request.destroy(new Error('钉钉 Access Token 获取超时'));
|
|
2027
|
+
});
|
|
2028
|
+
request.write(body);
|
|
2029
|
+
request.end();
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
reject(error);
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function buildTestNotificationContext() {
|
|
2037
|
+
return {
|
|
2038
|
+
title: 'Coding Tool - 测试通知',
|
|
2039
|
+
message: '这是一条测试通知',
|
|
2040
|
+
source: 'test',
|
|
2041
|
+
eventType: 'test',
|
|
2042
|
+
timestamp: new Date().toLocaleString('zh-CN'),
|
|
2043
|
+
hostname: os.hostname()
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function formatProviderText(ctx) {
|
|
2048
|
+
return [
|
|
2049
|
+
ctx.title,
|
|
2050
|
+
`来源: ${ctx.source}`,
|
|
2051
|
+
`状态: ${ctx.message}`,
|
|
2052
|
+
`时间: ${ctx.timestamp}`,
|
|
2053
|
+
`设备: ${ctx.hostname}`
|
|
2054
|
+
].join('\n');
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
async function sendRemoteProviderTest(providerInput = {}) {
|
|
2058
|
+
const provider = normalizeRemoteProvider(providerInput);
|
|
2059
|
+
if (!provider) {
|
|
2060
|
+
throw createValidationError('远程通知渠道类型不正确');
|
|
2061
|
+
}
|
|
2062
|
+
validateRemoteProviderConfig({ ...provider, enabled: true });
|
|
2063
|
+
|
|
2064
|
+
const config = provider.config || {};
|
|
2065
|
+
const ctx = buildTestNotificationContext();
|
|
2066
|
+
switch (provider.type) {
|
|
2067
|
+
case 'feishuBot':
|
|
2068
|
+
return sendFeishuTest(config.webhookUrl);
|
|
2069
|
+
case 'qqBot': {
|
|
2070
|
+
const pathSuffix = config.targetType === 'group' ? '/send_group_msg' : '/send_private_msg';
|
|
2071
|
+
return requestJson(String(config.endpoint).replace(/\/$/, '') + pathSuffix, {
|
|
2072
|
+
[config.targetType === 'group' ? 'group_id' : 'user_id']: config.targetId,
|
|
2073
|
+
message: formatProviderText(ctx)
|
|
2074
|
+
}, config.accessToken ? { Authorization: `Bearer ${config.accessToken}` } : {});
|
|
2075
|
+
}
|
|
2076
|
+
case 'wecomBot':
|
|
2077
|
+
return requestJson(config.webhookUrl, {
|
|
2078
|
+
msgtype: 'markdown',
|
|
2079
|
+
markdown: { content: `**${ctx.title}**\n> 状态: ${ctx.message}\n> 时间: ${ctx.timestamp}` }
|
|
2080
|
+
});
|
|
2081
|
+
case 'dingtalkBot':
|
|
2082
|
+
if (config.mode === 'app') {
|
|
2083
|
+
return sendDingTalkAppProviderTest(config, ctx);
|
|
2084
|
+
}
|
|
2085
|
+
return requestJson(config.webhookUrl, {
|
|
2086
|
+
msgtype: 'markdown',
|
|
2087
|
+
markdown: { title: ctx.title, text: `### ${ctx.title}\n\n- 状态: ${ctx.message}\n- 时间: ${ctx.timestamp}` }
|
|
2088
|
+
});
|
|
2089
|
+
case 'telegramBot':
|
|
2090
|
+
return requestTelegramMessage(config, ctx);
|
|
2091
|
+
case 'wechatBot':
|
|
2092
|
+
if (!config.botToken && config.tokenFile) {
|
|
2093
|
+
try {
|
|
2094
|
+
const raw = fs.readFileSync(config.tokenFile, 'utf8');
|
|
2095
|
+
const parsed = JSON.parse(raw);
|
|
2096
|
+
config.botToken = parsed.bot_token || parsed.token || '';
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
throw createValidationError('无法读取微信 token.json');
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
requireConfigValue(config, 'botToken', '微信 Bot Token');
|
|
2102
|
+
return requestJson('https://ilinkai.weixin.qq.com/ilink/bot/sendmessage', {
|
|
2103
|
+
msg: {
|
|
2104
|
+
from_user_id: '',
|
|
2105
|
+
to_user_id: config.targetUserId,
|
|
2106
|
+
client_id: createClientId('coding-tool'),
|
|
2107
|
+
message_type: 2,
|
|
2108
|
+
message_state: 2,
|
|
2109
|
+
item_list: [{ type: 1, text_item: { text: formatProviderText(ctx) } }],
|
|
2110
|
+
...(config.contextToken ? { context_token: config.contextToken } : {})
|
|
2111
|
+
},
|
|
2112
|
+
base_info: { channel_version: '2.1.8' }
|
|
2113
|
+
}, {
|
|
2114
|
+
AuthorizationType: 'ilink_bot_token',
|
|
2115
|
+
Authorization: `Bearer ${config.botToken}`,
|
|
2116
|
+
'X-WECHAT-UIN': Buffer.from(String(Math.floor(Math.random() * 0xffffffff))).toString('base64')
|
|
2117
|
+
});
|
|
2118
|
+
default:
|
|
2119
|
+
throw createValidationError('不支持的远程通知渠道');
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function requestTelegramMessage(config = {}, ctx) {
|
|
2124
|
+
const url = `https://api.telegram.org/bot${config.botToken}/sendMessage`;
|
|
2125
|
+
const payload = {
|
|
2126
|
+
chat_id: config.chatId,
|
|
2127
|
+
text: formatProviderText(ctx)
|
|
2128
|
+
};
|
|
2129
|
+
const agent = config.proxy ? new HttpsProxyAgent(config.proxy) : null;
|
|
2130
|
+
return requestJson(url, payload, {}, agent ? { agent } : {});
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
async function sendDingTalkAppProviderTest(config = {}, ctx) {
|
|
2134
|
+
const token = await requestDingTalkAccessToken(config);
|
|
2135
|
+
if (!token) {
|
|
2136
|
+
throw createValidationError('钉钉 App Access Token 获取失败');
|
|
2137
|
+
}
|
|
2138
|
+
const isGroup = config.targetType !== 'user';
|
|
2139
|
+
const payload = {
|
|
2140
|
+
robotCode: config.clientId,
|
|
2141
|
+
msgKey: 'sampleMarkdown',
|
|
2142
|
+
msgParam: JSON.stringify({
|
|
2143
|
+
title: ctx.title,
|
|
2144
|
+
text: `### ${ctx.title}\n\n- 状态: ${ctx.message}\n- 时间: ${ctx.timestamp}\n- 设备: ${ctx.hostname}`
|
|
2145
|
+
})
|
|
2146
|
+
};
|
|
2147
|
+
if (isGroup) {
|
|
2148
|
+
payload.openConversationId = config.targetId;
|
|
2149
|
+
} else {
|
|
2150
|
+
payload.userIds = [config.targetId];
|
|
2151
|
+
}
|
|
2152
|
+
return requestJson(
|
|
2153
|
+
'https://api.dingtalk.com/v1.0/robot/' + (isGroup ? 'groupMessages/send' : 'oToMessages/batchSend'),
|
|
2154
|
+
payload,
|
|
2155
|
+
{ 'x-acs-dingtalk-access-token': token }
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1592
2159
|
function generateSystemNotificationCommand(type, message, platformOverride = os.platform()) {
|
|
1593
2160
|
const normalizedType = normalizeType(type);
|
|
1594
2161
|
const title = 'Coding Tool';
|
|
@@ -1674,7 +2241,7 @@ function syncManagedNotificationAssets() {
|
|
|
1674
2241
|
const hasManagedPlatform = Object.values(settings?.platforms || {}).some((platform) => platform?.enabled === true);
|
|
1675
2242
|
|
|
1676
2243
|
if (hasManagedPlatform) {
|
|
1677
|
-
writeNotifyScript(settings.
|
|
2244
|
+
writeNotifyScript(settings.remoteNotifications || {});
|
|
1678
2245
|
} else {
|
|
1679
2246
|
removeNotifyScript();
|
|
1680
2247
|
}
|
|
@@ -1682,9 +2249,9 @@ function syncManagedNotificationAssets() {
|
|
|
1682
2249
|
return settings;
|
|
1683
2250
|
}
|
|
1684
2251
|
|
|
1685
|
-
function testNotification({ type,
|
|
1686
|
-
if (
|
|
1687
|
-
return
|
|
2252
|
+
function testNotification({ type, provider } = {}) {
|
|
2253
|
+
if (provider) {
|
|
2254
|
+
return sendRemoteProviderTest(provider);
|
|
1688
2255
|
}
|
|
1689
2256
|
|
|
1690
2257
|
if (normalizeType(type) === 'browser') {
|
|
@@ -1714,6 +2281,8 @@ module.exports = {
|
|
|
1714
2281
|
buildCodexNotifyCommand,
|
|
1715
2282
|
writeNotifyScript,
|
|
1716
2283
|
generateNotifyScript,
|
|
2284
|
+
normalizeRemoteNotificationsConfig,
|
|
2285
|
+
validateRemoteProviderConfig,
|
|
1717
2286
|
_test: {
|
|
1718
2287
|
applyClaudeDisablePreference,
|
|
1719
2288
|
getManagedCommandType,
|
|
@@ -1727,6 +2296,8 @@ module.exports = {
|
|
|
1727
2296
|
parseGeminiNotificationStatus,
|
|
1728
2297
|
parseOpenCodeNotificationStatus,
|
|
1729
2298
|
validateFeishuWebhookUrl,
|
|
2299
|
+
normalizeRemoteNotificationsConfig,
|
|
2300
|
+
validateRemoteProviderConfig,
|
|
1730
2301
|
buildCodexNotifyCommand,
|
|
1731
2302
|
buildGeminiCommand,
|
|
1732
2303
|
buildStopHookCommand,
|