coding-tool-x 3.4.10 → 3.4.12
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-0PgPv5qO.js → Analytics-Q_QFMM2p.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-pBGoYbCP.js → ConfigTemplates-Ca8C7VtV.js} +1 -1
- package/dist/web/assets/{Home-BRN882om.js → Home-ChIIT4Ew.js} +1 -1
- package/dist/web/assets/{PluginManager-am97Huts.js → PluginManager-C3w7p-sj.js} +1 -1
- package/dist/web/assets/{ProjectList-CXS9KJN1.js → ProjectList-BfJTDXDw.js} +1 -1
- package/dist/web/assets/{SessionList-BZyrzH7J.js → SessionList-a3EoL0hZ.js} +1 -1
- package/dist/web/assets/{SkillManager-p1CI0tYa.js → SkillManager-CohW5iqS.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CUPvLoba.js → WorkspaceManager-DxaKsaJK.js} +1 -1
- package/dist/web/assets/index-B02wDWNC.css +1 -0
- package/dist/web/assets/index-EMrm1wk-.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/commands/toggle-proxy.js +1 -1
- package/src/server/api/claude-hooks.js +23 -536
- package/src/server/api/codex-proxy.js +1 -1
- package/src/server/api/opencode-proxy.js +92 -7
- package/src/server/index.js +2 -1
- package/src/server/services/codex-channels.js +1 -1
- package/src/server/services/gemini-channels.js +52 -31
- package/src/server/services/native-oauth-adapters.js +152 -5
- package/src/server/services/notification-hooks.js +579 -5
- package/src/server/services/opencode-channels.js +11 -2
- package/dist/web/assets/index-B4Wl3JfR.js +0 -2
- package/dist/web/assets/index-Bgt_oqoE.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>CC-TOOL - ClaudeCode增强工作助手</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-EMrm1wk-.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-3bf-fPGP.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/vendors-CKPV1OAU.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/naive-ui-Bdxp09n2.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/icons-B5Pl4lrD.js">
|
|
14
14
|
<link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B02wDWNC.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
package/package.json
CHANGED
|
@@ -125,7 +125,7 @@ function restoreSingleChannelMode(cliType) {
|
|
|
125
125
|
if (cliType === 'codex') {
|
|
126
126
|
const { getChannels, applyChannelToSettings } = require('../server/services/codex-channels');
|
|
127
127
|
const target = pickRestoredChannel(cliType, getChannels().channels || []);
|
|
128
|
-
return target ? applyChannelToSettings(target.id
|
|
128
|
+
return target ? applyChannelToSettings(target.id) : null;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
if (cliType === 'gemini') {
|
|
@@ -1,569 +1,56 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
-
const
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const http = require('http');
|
|
8
|
-
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
9
|
-
const { resolvePreferredHomeDir, normalizeWindowsHomePath } = require('../../utils/home-dir');
|
|
3
|
+
const notificationHooks = require('../services/notification-hooks');
|
|
10
4
|
const { createSameOriginGuard } = require('../services/network-access');
|
|
5
|
+
const { resolvePreferredHomeDir, normalizeWindowsHomePath } = require('../../utils/home-dir');
|
|
11
6
|
|
|
12
|
-
// 检测操作系统
|
|
13
|
-
const platform = os.platform(); // 'darwin' | 'win32' | 'linux'
|
|
14
7
|
router.use(createSameOriginGuard({
|
|
15
8
|
message: '禁止跨站访问 Claude Hooks 配置接口'
|
|
16
9
|
}));
|
|
17
10
|
|
|
18
|
-
const HOME_DIR = resolvePreferredHomeDir(platform, process.env, os.homedir());
|
|
19
|
-
|
|
20
|
-
// Claude settings.json 路径
|
|
21
|
-
const CLAUDE_SETTINGS_PATH = NATIVE_PATHS.claude.settings;
|
|
22
|
-
|
|
23
|
-
// UI 配置路径(记录用户是否主动关闭过、飞书配置等)
|
|
24
|
-
const UI_CONFIG_PATH = PATHS.uiConfig;
|
|
25
|
-
|
|
26
|
-
// 通知脚本路径(用于飞书通知)
|
|
27
|
-
const NOTIFY_SCRIPT_PATH = PATHS.notifyHook;
|
|
28
|
-
|
|
29
|
-
function buildWindowsPopupCommand() {
|
|
30
|
-
return `powershell -NoProfile -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40)"`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 读取 Claude settings.json
|
|
34
|
-
function readClaudeSettings() {
|
|
35
|
-
try {
|
|
36
|
-
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
37
|
-
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
|
|
38
|
-
return JSON.parse(content);
|
|
39
|
-
}
|
|
40
|
-
return {};
|
|
41
|
-
} catch (error) {
|
|
42
|
-
console.error('Failed to read Claude settings:', error);
|
|
43
|
-
return {};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// 写入 Claude settings.json
|
|
48
|
-
function writeClaudeSettings(settings) {
|
|
49
|
-
try {
|
|
50
|
-
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
51
|
-
if (!fs.existsSync(dir)) {
|
|
52
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
-
}
|
|
54
|
-
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8');
|
|
55
|
-
return true;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.error('Failed to write Claude settings:', error);
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 读取 UI 配置
|
|
63
|
-
function readUIConfig() {
|
|
64
|
-
try {
|
|
65
|
-
if (fs.existsSync(UI_CONFIG_PATH)) {
|
|
66
|
-
const content = fs.readFileSync(UI_CONFIG_PATH, 'utf8');
|
|
67
|
-
return JSON.parse(content);
|
|
68
|
-
}
|
|
69
|
-
return {};
|
|
70
|
-
} catch (error) {
|
|
71
|
-
return {};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// 写入 UI 配置
|
|
76
|
-
function writeUIConfig(config) {
|
|
77
|
-
try {
|
|
78
|
-
const dir = path.dirname(UI_CONFIG_PATH);
|
|
79
|
-
if (!fs.existsSync(dir)) {
|
|
80
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
fs.writeFileSync(UI_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
|
83
|
-
return true;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error('Failed to write UI config:', error);
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 生成系统通知命令(跨平台)
|
|
91
|
-
function generateSystemNotificationCommand(type, platformOverride = platform) {
|
|
92
|
-
if (platformOverride === 'darwin') {
|
|
93
|
-
// macOS
|
|
94
|
-
if (type === 'dialog') {
|
|
95
|
-
return `osascript -e 'display dialog "Claude Code 任务已完成 | 等待交互" with title "Coding Tool" buttons {"好的"} default button 1 with icon note'`;
|
|
96
|
-
} else {
|
|
97
|
-
// 优先使用 terminal-notifier(点击可打开终端),否则使用 osascript
|
|
98
|
-
// terminal-notifier 需要 brew install terminal-notifier
|
|
99
|
-
return `if command -v terminal-notifier &>/dev/null; then terminal-notifier -title "Coding Tool" -message "任务已完成 | 等待交互" -sound Glass -activate com.apple.Terminal; else osascript -e 'display notification "任务已完成 | 等待交互" with title "Coding Tool" sound name "Glass"'; fi`;
|
|
100
|
-
}
|
|
101
|
-
} else if (platformOverride === 'win32') {
|
|
102
|
-
// Windows
|
|
103
|
-
if (type === 'dialog') {
|
|
104
|
-
return `powershell -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('Claude Code 任务已完成 | 等待交互', 'Coding Tool', 'OK', 'Information')" || ${buildWindowsPopupCommand()}`;
|
|
105
|
-
} else {
|
|
106
|
-
return buildWindowsPopupCommand();
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
// Linux
|
|
110
|
-
if (type === 'dialog') {
|
|
111
|
-
return `zenity --info --title="Coding Tool" --text="Claude Code 任务已完成 | 等待交互" 2>/dev/null || notify-send "Coding Tool" "任务已完成 | 等待交互"`;
|
|
112
|
-
} else {
|
|
113
|
-
return `notify-send "Coding Tool" "任务已完成 | 等待交互"`;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 生成通知脚本内容(支持系统通知 + 飞书通知)
|
|
119
|
-
function generateNotifyScript(config) {
|
|
120
|
-
const { systemNotification, feishu } = config;
|
|
121
|
-
|
|
122
|
-
let script = `#!/usr/bin/env node
|
|
123
|
-
// CC-Tool 通知脚本 - 自动生成,请勿手动修改
|
|
124
|
-
const https = require('https');
|
|
125
|
-
const http = require('http');
|
|
126
|
-
const { execSync } = require('child_process');
|
|
127
|
-
const os = require('os');
|
|
128
|
-
|
|
129
|
-
const platform = os.platform();
|
|
130
|
-
const timestamp = new Date().toLocaleString('zh-CN');
|
|
131
|
-
|
|
132
|
-
`;
|
|
133
|
-
|
|
134
|
-
// 系统通知部分
|
|
135
|
-
if (systemNotification && systemNotification.enabled) {
|
|
136
|
-
const cmd = generateSystemNotificationCommand(systemNotification.type);
|
|
137
|
-
script += `// 系统通知
|
|
138
|
-
try {
|
|
139
|
-
execSync(${JSON.stringify(cmd)}, { stdio: 'ignore', windowsHide: true });
|
|
140
|
-
} catch (e) {
|
|
141
|
-
console.error('系统通知失败:', e.message);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 飞书通知部分
|
|
148
|
-
if (feishu && feishu.enabled && feishu.webhookUrl) {
|
|
149
|
-
script += `// 飞书通知
|
|
150
|
-
const feishuUrl = ${JSON.stringify(feishu.webhookUrl)};
|
|
151
|
-
const feishuData = JSON.stringify({
|
|
152
|
-
msg_type: 'interactive',
|
|
153
|
-
card: {
|
|
154
|
-
header: {
|
|
155
|
-
title: { tag: 'plain_text', content: '[DONE] Coding Tool - 任务完成' },
|
|
156
|
-
template: 'green'
|
|
157
|
-
},
|
|
158
|
-
elements: [
|
|
159
|
-
{
|
|
160
|
-
tag: 'div',
|
|
161
|
-
text: { tag: 'lark_md', content: '**状态**: Claude Code 任务已完成 | 等待交互' }
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
tag: 'div',
|
|
165
|
-
text: { tag: 'lark_md', content: '**时间**: ' + timestamp }
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
tag: 'div',
|
|
169
|
-
text: { tag: 'lark_md', content: '**设备**: ' + os.hostname() }
|
|
170
|
-
}
|
|
171
|
-
]
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
const urlObj = new URL(feishuUrl);
|
|
177
|
-
const options = {
|
|
178
|
-
hostname: urlObj.hostname,
|
|
179
|
-
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
180
|
-
path: urlObj.pathname + urlObj.search,
|
|
181
|
-
method: 'POST',
|
|
182
|
-
headers: {
|
|
183
|
-
'Content-Type': 'application/json',
|
|
184
|
-
'Content-Length': Buffer.byteLength(feishuData)
|
|
185
|
-
},
|
|
186
|
-
timeout: 10000
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const reqModule = urlObj.protocol === 'https:' ? https : http;
|
|
190
|
-
const req = reqModule.request(options, (res) => {
|
|
191
|
-
// 忽略响应
|
|
192
|
-
});
|
|
193
|
-
req.on('error', (e) => {
|
|
194
|
-
console.error('飞书通知失败:', e.message);
|
|
195
|
-
});
|
|
196
|
-
req.write(feishuData);
|
|
197
|
-
req.end();
|
|
198
|
-
} catch (e) {
|
|
199
|
-
console.error('飞书通知失败:', e.message);
|
|
200
|
-
}
|
|
201
|
-
`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return script;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function parseNotifyTypeMarker(command) {
|
|
208
|
-
const marker = command.match(/--cc-notify-type=(['"])?(dialog|notification)\1/i);
|
|
209
|
-
return marker ? marker[2].toLowerCase() : null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function getStopHookCommand(settings) {
|
|
213
|
-
const hooks = settings?.hooks?.Stop;
|
|
214
|
-
if (!Array.isArray(hooks) || hooks.length === 0) {
|
|
215
|
-
return '';
|
|
216
|
-
}
|
|
217
|
-
const firstHook = hooks[0]?.hooks;
|
|
218
|
-
if (!Array.isArray(firstHook) || firstHook.length === 0) {
|
|
219
|
-
return '';
|
|
220
|
-
}
|
|
221
|
-
return firstHook[0]?.command || '';
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function normalizePathForCompare(rawPath) {
|
|
225
|
-
return String(rawPath || '').replace(/\\/g, '/');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function shouldRepairStopHook(settings, expectedScriptPath = NOTIFY_SCRIPT_PATH, fileExists = fs.existsSync) {
|
|
229
|
-
const command = getStopHookCommand(settings);
|
|
230
|
-
if (!command || !command.includes('notify-hook.js')) {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const normalizedCommand = normalizePathForCompare(command);
|
|
235
|
-
const normalizedExpected = normalizePathForCompare(expectedScriptPath);
|
|
236
|
-
if (!normalizedCommand.includes(normalizedExpected)) {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const markerType = parseNotifyTypeMarker(command);
|
|
241
|
-
if (!markerType) {
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return !fileExists(expectedScriptPath);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function buildStopHookCommand(type) {
|
|
249
|
-
const notifyType = type === 'dialog' ? 'dialog' : 'notification';
|
|
250
|
-
return `node "${NOTIFY_SCRIPT_PATH}" --cc-notify-type=${notifyType}`;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 写入通知脚本
|
|
254
|
-
function writeNotifyScript(config) {
|
|
255
|
-
try {
|
|
256
|
-
const dir = path.dirname(NOTIFY_SCRIPT_PATH);
|
|
257
|
-
if (!fs.existsSync(dir)) {
|
|
258
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const script = generateNotifyScript(config);
|
|
262
|
-
fs.writeFileSync(NOTIFY_SCRIPT_PATH, script, { mode: 0o755 });
|
|
263
|
-
return true;
|
|
264
|
-
} catch (error) {
|
|
265
|
-
console.error('Failed to write notify script:', error);
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// 从现有 hooks 配置中解析 Stop hook 状态
|
|
271
|
-
function parseStopHookStatus(settings) {
|
|
272
|
-
const hooks = settings.hooks;
|
|
273
|
-
if (!hooks || !hooks.Stop || !Array.isArray(hooks.Stop) || hooks.Stop.length === 0) {
|
|
274
|
-
return { enabled: false, type: 'notification' };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const stopHook = hooks.Stop[0];
|
|
278
|
-
if (!stopHook.hooks || !Array.isArray(stopHook.hooks) || stopHook.hooks.length === 0) {
|
|
279
|
-
return { enabled: false, type: 'notification' };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const command = stopHook.hooks[0].command || '';
|
|
283
|
-
const markerType = parseNotifyTypeMarker(command);
|
|
284
|
-
|
|
285
|
-
if (markerType) {
|
|
286
|
-
return { enabled: true, type: markerType };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// 判断通知类型(跨平台检测)
|
|
290
|
-
const isDialog = command.includes('display dialog') ||
|
|
291
|
-
command.includes('MessageBox') ||
|
|
292
|
-
command.includes('zenity --info');
|
|
293
|
-
const isNotification = command.includes('display notification') ||
|
|
294
|
-
command.includes('Popup') ||
|
|
295
|
-
command.includes('notify-send') ||
|
|
296
|
-
command.includes('ToastNotificationManager') ||
|
|
297
|
-
command.includes('CreateToastNotifier');
|
|
298
|
-
|
|
299
|
-
// 检查是否是我们的通知脚本
|
|
300
|
-
const isOurScript = command.includes('notify-hook.js');
|
|
301
|
-
|
|
302
|
-
if (isDialog || isNotification || isOurScript) {
|
|
303
|
-
return {
|
|
304
|
-
enabled: true,
|
|
305
|
-
type: isDialog ? 'dialog' : 'notification'
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return { enabled: false, type: 'notification' };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// 获取飞书配置
|
|
313
|
-
function getFeishuConfig() {
|
|
314
|
-
const uiConfig = readUIConfig();
|
|
315
|
-
return {
|
|
316
|
-
enabled: uiConfig.feishuNotification?.enabled || false,
|
|
317
|
-
webhookUrl: uiConfig.feishuNotification?.webhookUrl || ''
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// 保存飞书配置
|
|
322
|
-
function saveFeishuConfig(feishu) {
|
|
323
|
-
const uiConfig = readUIConfig();
|
|
324
|
-
uiConfig.feishuNotification = {
|
|
325
|
-
enabled: feishu.enabled || false,
|
|
326
|
-
webhookUrl: feishu.webhookUrl || ''
|
|
327
|
-
};
|
|
328
|
-
return writeUIConfig(uiConfig);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// 更新 Stop hook 配置
|
|
332
|
-
function updateStopHook(systemNotification, feishu) {
|
|
333
|
-
const settings = readClaudeSettings();
|
|
334
|
-
|
|
335
|
-
// 检查是否有任何通知需要启用
|
|
336
|
-
const hasSystemNotification = systemNotification && systemNotification.enabled;
|
|
337
|
-
const hasFeishu = feishu && feishu.enabled && feishu.webhookUrl;
|
|
338
|
-
|
|
339
|
-
if (!hasSystemNotification && !hasFeishu) {
|
|
340
|
-
// 都关闭了,移除 Stop hook
|
|
341
|
-
if (settings.hooks && settings.hooks.Stop) {
|
|
342
|
-
delete settings.hooks.Stop;
|
|
343
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
344
|
-
delete settings.hooks;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
// 删除通知脚本
|
|
348
|
-
if (fs.existsSync(NOTIFY_SCRIPT_PATH)) {
|
|
349
|
-
fs.unlinkSync(NOTIFY_SCRIPT_PATH);
|
|
350
|
-
}
|
|
351
|
-
} else {
|
|
352
|
-
// 生成并写入通知脚本
|
|
353
|
-
if (!writeNotifyScript({ systemNotification, feishu })) {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// 更新 Stop hook 指向通知脚本
|
|
358
|
-
settings.hooks = settings.hooks || {};
|
|
359
|
-
settings.hooks.Stop = [
|
|
360
|
-
{
|
|
361
|
-
hooks: [
|
|
362
|
-
{
|
|
363
|
-
type: 'command',
|
|
364
|
-
command: buildStopHookCommand(systemNotification?.type)
|
|
365
|
-
}
|
|
366
|
-
]
|
|
367
|
-
}
|
|
368
|
-
];
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return writeClaudeSettings(settings);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// 初始化默认 hooks 配置(服务启动时调用)
|
|
375
|
-
function initDefaultHooks() {
|
|
376
|
-
try {
|
|
377
|
-
const uiConfig = readUIConfig();
|
|
378
|
-
|
|
379
|
-
// 如果用户主动关闭过通知,不自动开启
|
|
380
|
-
if (uiConfig.claudeNotificationDisabledByUser === true) {
|
|
381
|
-
console.log('[Claude Hooks] 用户已主动关闭通知,跳过自动初始化');
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const settings = readClaudeSettings();
|
|
386
|
-
const currentStatus = parseStopHookStatus(settings);
|
|
387
|
-
|
|
388
|
-
// 如果已经有 Stop hook 配置,优先尝试自愈旧路径,再决定是否跳过
|
|
389
|
-
if (currentStatus.enabled) {
|
|
390
|
-
if (shouldRepairStopHook(settings)) {
|
|
391
|
-
const systemNotification = {
|
|
392
|
-
enabled: true,
|
|
393
|
-
type: currentStatus.type || 'notification'
|
|
394
|
-
};
|
|
395
|
-
const feishu = getFeishuConfig();
|
|
396
|
-
if (updateStopHook(systemNotification, feishu)) {
|
|
397
|
-
console.log('[Claude Hooks] 检测到旧版 Stop hook 路径,已自动修复');
|
|
398
|
-
} else {
|
|
399
|
-
console.warn('[Claude Hooks] Stop hook 路径修复失败,保留原配置');
|
|
400
|
-
}
|
|
401
|
-
} else {
|
|
402
|
-
console.log('[Claude Hooks] 已存在 Stop hook 配置,跳过初始化');
|
|
403
|
-
}
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// 写入默认配置(右上角卡片通知)
|
|
408
|
-
const systemNotification = { enabled: true, type: 'notification' };
|
|
409
|
-
const feishu = getFeishuConfig();
|
|
410
|
-
|
|
411
|
-
if (updateStopHook(systemNotification, feishu)) {
|
|
412
|
-
console.log('[Claude Hooks] 已自动开启任务完成通知(右上角卡片)');
|
|
413
|
-
}
|
|
414
|
-
} catch (error) {
|
|
415
|
-
console.error('[Claude Hooks] 初始化默认配置失败:', error);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// GET /api/claude/hooks - 获取 hooks 配置状态
|
|
420
11
|
router.get('/', (req, res) => {
|
|
421
12
|
try {
|
|
422
|
-
|
|
423
|
-
const stopHook = parseStopHookStatus(settings);
|
|
424
|
-
const feishu = getFeishuConfig();
|
|
425
|
-
|
|
426
|
-
res.json({
|
|
427
|
-
success: true,
|
|
428
|
-
stopHook,
|
|
429
|
-
feishu,
|
|
430
|
-
platform
|
|
431
|
-
});
|
|
13
|
+
res.json(notificationHooks.getLegacyClaudeHookSettings());
|
|
432
14
|
} catch (error) {
|
|
433
15
|
console.error('Error getting Claude hooks:', error);
|
|
434
|
-
res.status(500).json({ error: error.message });
|
|
16
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
435
17
|
}
|
|
436
18
|
});
|
|
437
19
|
|
|
438
|
-
// POST /api/claude/hooks - 保存 hooks 配置
|
|
439
20
|
router.post('/', (req, res) => {
|
|
440
21
|
try {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// 更新 Stop hook
|
|
449
|
-
const systemNotification = stopHook ? {
|
|
450
|
-
enabled: stopHook.enabled,
|
|
451
|
-
type: stopHook.type || 'notification'
|
|
452
|
-
} : { enabled: false, type: 'notification' };
|
|
453
|
-
|
|
454
|
-
const feishuConfig = feishu || getFeishuConfig();
|
|
455
|
-
|
|
456
|
-
// 更新用户关闭标记
|
|
457
|
-
const uiConfig = readUIConfig();
|
|
458
|
-
if (systemNotification.enabled || feishuConfig.enabled) {
|
|
459
|
-
// 用户开启了通知,清除关闭标记
|
|
460
|
-
if (uiConfig.claudeNotificationDisabledByUser) {
|
|
461
|
-
delete uiConfig.claudeNotificationDisabledByUser;
|
|
462
|
-
writeUIConfig(uiConfig);
|
|
463
|
-
}
|
|
464
|
-
} else {
|
|
465
|
-
// 用户关闭了所有通知
|
|
466
|
-
uiConfig.claudeNotificationDisabledByUser = true;
|
|
467
|
-
writeUIConfig(uiConfig);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (updateStopHook(systemNotification, feishuConfig)) {
|
|
471
|
-
res.json({
|
|
472
|
-
success: true,
|
|
473
|
-
message: '配置已保存',
|
|
474
|
-
stopHook: systemNotification,
|
|
475
|
-
feishu: feishuConfig
|
|
476
|
-
});
|
|
477
|
-
} else {
|
|
478
|
-
res.status(500).json({ error: '保存配置失败' });
|
|
479
|
-
}
|
|
22
|
+
const result = notificationHooks.saveLegacyClaudeHookSettings(req.body || {});
|
|
23
|
+
res.json({
|
|
24
|
+
...result,
|
|
25
|
+
message: '配置已保存'
|
|
26
|
+
});
|
|
480
27
|
} catch (error) {
|
|
481
28
|
console.error('Error saving Claude hooks:', error);
|
|
482
|
-
res.status(500).json({ error: error.message });
|
|
29
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
483
30
|
}
|
|
484
31
|
});
|
|
485
32
|
|
|
486
|
-
|
|
487
|
-
router.post('/test', (req, res) => {
|
|
33
|
+
router.post('/test', async (req, res) => {
|
|
488
34
|
try {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const data = JSON.stringify({
|
|
495
|
-
msg_type: 'interactive',
|
|
496
|
-
card: {
|
|
497
|
-
header: {
|
|
498
|
-
title: { tag: 'plain_text', content: '[TEST] Coding Tool - 测试通知' },
|
|
499
|
-
template: 'blue'
|
|
500
|
-
},
|
|
501
|
-
elements: [
|
|
502
|
-
{
|
|
503
|
-
tag: 'div',
|
|
504
|
-
text: { tag: 'lark_md', content: '**状态**: 这是一条测试通知' }
|
|
505
|
-
},
|
|
506
|
-
{
|
|
507
|
-
tag: 'div',
|
|
508
|
-
text: { tag: 'lark_md', content: '**时间**: ' + new Date().toLocaleString('zh-CN') }
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
tag: 'div',
|
|
512
|
-
text: { tag: 'lark_md', content: '**设备**: ' + os.hostname() }
|
|
513
|
-
}
|
|
514
|
-
]
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
const options = {
|
|
519
|
-
hostname: urlObj.hostname,
|
|
520
|
-
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
521
|
-
path: urlObj.pathname + urlObj.search,
|
|
522
|
-
method: 'POST',
|
|
523
|
-
headers: {
|
|
524
|
-
'Content-Type': 'application/json',
|
|
525
|
-
'Content-Length': Buffer.byteLength(data)
|
|
526
|
-
},
|
|
527
|
-
timeout: 10000
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
const reqModule = urlObj.protocol === 'https:' ? https : http;
|
|
531
|
-
const request = reqModule.request(options, (response) => {
|
|
532
|
-
let body = '';
|
|
533
|
-
response.on('data', chunk => body += chunk);
|
|
534
|
-
response.on('end', () => {
|
|
535
|
-
res.json({ success: true, message: '飞书测试通知已发送' });
|
|
536
|
-
});
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
request.on('error', (e) => {
|
|
540
|
-
res.status(500).json({ error: '飞书通知发送失败: ' + e.message });
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
request.write(data);
|
|
544
|
-
request.end();
|
|
545
|
-
} else {
|
|
546
|
-
// 测试系统通知
|
|
547
|
-
const command = generateSystemNotificationCommand(type || 'notification');
|
|
548
|
-
const { execSync } = require('child_process');
|
|
549
|
-
execSync(command, { stdio: 'ignore', windowsHide: true });
|
|
550
|
-
res.json({ success: true, message: '系统测试通知已发送' });
|
|
551
|
-
}
|
|
35
|
+
await notificationHooks.testNotification(req.body || {});
|
|
36
|
+
res.json({
|
|
37
|
+
success: true,
|
|
38
|
+
message: req.body?.testFeishu ? '飞书测试通知已发送' : '系统测试通知已发送'
|
|
39
|
+
});
|
|
552
40
|
} catch (error) {
|
|
553
41
|
console.error('Error testing notification:', error);
|
|
554
|
-
res.status(500).json({ error: error.message });
|
|
42
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
555
43
|
}
|
|
556
44
|
});
|
|
557
45
|
|
|
558
|
-
// 导出初始化函数供服务启动时调用
|
|
559
46
|
module.exports = router;
|
|
560
|
-
module.exports.initDefaultHooks = initDefaultHooks;
|
|
47
|
+
module.exports.initDefaultHooks = notificationHooks.initDefaultHooks;
|
|
561
48
|
module.exports._test = {
|
|
562
|
-
generateSystemNotificationCommand,
|
|
563
|
-
parseStopHookStatus,
|
|
564
|
-
parseNotifyTypeMarker,
|
|
565
|
-
buildStopHookCommand,
|
|
49
|
+
generateSystemNotificationCommand: notificationHooks._test.generateSystemNotificationCommand,
|
|
50
|
+
parseStopHookStatus: notificationHooks._test.parseStopHookStatus,
|
|
51
|
+
parseNotifyTypeMarker: notificationHooks._test.parseNotifyTypeMarker,
|
|
52
|
+
buildStopHookCommand: notificationHooks._test.buildStopHookCommand,
|
|
566
53
|
normalizeWindowsHomePath,
|
|
567
54
|
resolvePreferredHomeDir,
|
|
568
|
-
shouldRepairStopHook
|
|
55
|
+
shouldRepairStopHook: notificationHooks._test.shouldRepairStopHook
|
|
569
56
|
};
|
|
@@ -223,7 +223,7 @@ router.post('/stop', async (req, res) => {
|
|
|
223
223
|
// 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
|
|
224
224
|
if (activeChannel) {
|
|
225
225
|
const { applyChannelToSettings } = require('../services/codex-channels');
|
|
226
|
-
applyChannelToSettings(activeChannel.id
|
|
226
|
+
applyChannelToSettings(activeChannel.id);
|
|
227
227
|
console.log(`[Codex Proxy] Single-channel mode restored: ${activeChannel.name}`);
|
|
228
228
|
}
|
|
229
229
|
|