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.
Files changed (54) hide show
  1. package/dist/web/assets/{Analytics-CKycBI0G.js → Analytics-BuBlsbcI.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-DQczxuOX.js} +1 -1
  3. package/dist/web/assets/Home-BvAzzWnB.js +1 -0
  4. package/dist/web/assets/Home-CIua-b-o.css +1 -0
  5. package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-C8gAlaps.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-nqgi6_cR.js} +1 -1
  7. package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-D8AxMYy6.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-BDbNbNkK.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-DGGaGW2c.js} +1 -1
  10. package/dist/web/assets/index-Dsh4Q5Cg.css +1 -0
  11. package/dist/web/assets/index-y-eEayxR.js +2 -0
  12. package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
  13. package/dist/web/index.html +3 -3
  14. package/package.json +1 -1
  15. package/src/commands/stats.js +1 -1
  16. package/src/config/channel-preset-websites.js +2 -1
  17. package/src/config/paths.js +8 -0
  18. package/src/server/api/agents.js +16 -54
  19. package/src/server/api/claude-hooks.js +1 -1
  20. package/src/server/api/claude-statistics.js +57 -0
  21. package/src/server/api/commands.js +16 -54
  22. package/src/server/api/dashboard.js +2 -2
  23. package/src/server/api/gemini-channels.js +2 -0
  24. package/src/server/api/hooks.js +4 -1
  25. package/src/server/api/plugins.js +46 -69
  26. package/src/server/api/skills.js +21 -77
  27. package/src/server/api/validation-errors.js +34 -0
  28. package/src/server/gemini-proxy-server.js +205 -21
  29. package/src/server/index.js +1 -0
  30. package/src/server/services/agents-service.js +166 -75
  31. package/src/server/services/base/base-channel-service.js +15 -0
  32. package/src/server/services/base/response-usage-parser.js +44 -0
  33. package/src/server/services/channel-balance.js +86 -1
  34. package/src/server/services/claude-statistics-service.js +139 -0
  35. package/src/server/services/commands-service.js +203 -121
  36. package/src/server/services/config-artifact-paths.js +116 -0
  37. package/src/server/services/config-export-service.js +7 -6
  38. package/src/server/services/config-registry-service.js +2 -2
  39. package/src/server/services/config-sync-manager.js +45 -11
  40. package/src/server/services/config-templates-service.js +21 -19
  41. package/src/server/services/format-converter.js +42 -4
  42. package/src/server/services/gemini-channels.js +30 -1
  43. package/src/server/services/gemini-sessions.js +347 -61
  44. package/src/server/services/mcp-client.js +236 -49
  45. package/src/server/services/mcp-service.js +51 -64
  46. package/src/server/services/notification-hooks.js +659 -88
  47. package/src/server/services/opencode-channels.js +15 -0
  48. package/src/server/services/plugins-service.js +1137 -86
  49. package/src/server/services/repo-scanner-base.js +6 -29
  50. package/src/server/services/skill-service.js +198 -65
  51. package/dist/web/assets/Home-BeGkC4bb.css +0 -1
  52. package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
  53. package/dist/web/assets/index-0isNzXNY.css +0 -1
  54. 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 getFeishuConfig() {
53
- const uiConfig = loadUIConfig();
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
- enabled: uiConfig.feishuNotification?.enabled === true,
56
- webhookUrl: uiConfig.feishuNotification?.webhookUrl || ''
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(feishu = {}, claudeEnabled) {
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.feishuNotification = {
76
- enabled: feishu.enabled === true,
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(feishu = {}) {
437
- const feishuEnabled = feishu.enabled === true && !!feishu.webhookUrl;
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
- const FEISHU_ENABLED = ${feishuEnabled ? 'true' : 'false'}
448
- const FEISHU_WEBHOOK_URL = ${JSON.stringify(feishuEnabled ? feishu.webhookUrl : '')}
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 sendFeishu(message, source) {
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(FEISHU_WEBHOOK_URL)
643
- const timestamp = new Date().toLocaleString('zh-CN')
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(data)
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, () => resolve())
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(data)
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 sendFeishu(message, source)
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(feishu = {}) {
1305
+ function writeNotifyScript(remoteNotifications = {}) {
932
1306
  ensureParentDir(PATHS.notifyHook);
933
- fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu), { mode: 0o755 });
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
- feishu: getFeishuConfig(),
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 existingFeishu = getFeishuConfig();
1309
- const requestedWebhookUrl = String(input?.feishu?.webhookUrl || '').trim();
1310
- const feishu = {
1311
- enabled: input?.feishu?.enabled === true,
1312
- webhookUrl: requestedWebhookUrl || (input?.feishu?.enabled === true ? existingFeishu.webhookUrl || '' : '')
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(feishu, platforms.claude.enabled);
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(feishu);
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
- feishu: input.feishu !== undefined
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
- feishu: {
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
- feishu: {
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.feishu || {});
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, testFeishu, webhookUrl } = {}) {
1686
- if (testFeishu && webhookUrl) {
1687
- return sendFeishuTest(webhookUrl);
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,