coding-tool-x 3.5.4 → 3.5.6
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/CHANGELOG.md +7 -0
- package/README.md +8 -4
- package/dist/web/assets/{Analytics-CmN09J9U.js → Analytics-CRNCHeui.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CeTAPmep.js → ConfigTemplates-C0erJdo2.js} +1 -1
- package/dist/web/assets/{Home-BYtCM3rK.js → Home-CL5z6Q4d.js} +1 -1
- package/dist/web/assets/{PluginManager-OAH1eMO0.js → PluginManager-hDx0XMO_.js} +1 -1
- package/dist/web/assets/{ProjectList-B0pIy1cv.js → ProjectList-BNsz96av.js} +1 -1
- package/dist/web/assets/{SessionList-DbB6ASiA.js → SessionList-CG1UhFo3.js} +1 -1
- package/dist/web/assets/{SkillManager-wp1dhL1z.js → SkillManager-D6Vwpajh.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Ce6wQoKb.js → WorkspaceManager-C3TjeOPy.js} +1 -1
- package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
- package/dist/web/assets/index-GuER-BmS.js +2 -0
- package/dist/web/assets/{index-B02wDWNC.css → index-VGAxnLqi.css} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +41 -4
- package/src/index.js +1 -0
- package/src/server/api/codex-sessions.js +6 -3
- package/src/server/api/dashboard.js +25 -1
- package/src/server/api/gemini-sessions.js +6 -3
- package/src/server/api/hooks.js +17 -1
- package/src/server/api/opencode-sessions.js +6 -3
- package/src/server/api/plugins.js +24 -33
- package/src/server/api/sessions.js +6 -3
- package/src/server/codex-proxy-server.js +24 -59
- package/src/server/gemini-proxy-server.js +25 -66
- package/src/server/index.js +6 -4
- package/src/server/opencode-proxy-server.js +24 -59
- package/src/server/proxy-server.js +18 -30
- package/src/server/services/base/response-usage-parser.js +187 -0
- package/src/server/services/codex-sessions.js +107 -9
- package/src/server/services/network-access.js +14 -0
- package/src/server/services/notification-hooks.js +175 -16
- package/src/server/services/plugins-service.js +502 -44
- package/src/server/services/proxy-log-helper.js +21 -3
- package/src/server/services/session-launch-command.js +81 -0
- package/src/server/services/sessions.js +103 -33
- package/src/server/services/statistics-service.js +7 -0
- package/src/server/websocket-server.js +25 -1
- package/dist/web/assets/index-CHwVofQH.js +0 -2
|
@@ -10,6 +10,7 @@ const ALL_SESSIONS_CACHE_TTL_MS = 20 * 1000;
|
|
|
10
10
|
const PROJECTS_CACHE_TTL_MS = 300 * 1000;
|
|
11
11
|
const PROJECT_SESSIONS_CACHE_TTL_MS = 120 * 1000;
|
|
12
12
|
const FAST_META_READ_BYTES = 64 * 1024;
|
|
13
|
+
const MAX_SESSION_META_SUMMARY_CACHE_ENTRIES = 5000;
|
|
13
14
|
const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
|
|
14
15
|
|
|
15
16
|
let countsCache = {
|
|
@@ -32,6 +33,8 @@ let allSessionsCache = {
|
|
|
32
33
|
value: []
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
let sessionMetaSummaryCache = new Map();
|
|
37
|
+
|
|
35
38
|
const CODEX_PROJECTS_CACHE_KEY = `${CacheKeys.PROJECTS}codex`;
|
|
36
39
|
const codexSessionCacheKeys = new Set();
|
|
37
40
|
|
|
@@ -39,6 +42,47 @@ function getCodexSessionsCacheKey(projectName) {
|
|
|
39
42
|
return `${CacheKeys.SESSIONS}codex:${projectName}`;
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
function getSessionMetaSummaryCacheKey(filePath, fileMeta = {}) {
|
|
46
|
+
return `${filePath}:${fileMeta.mtimeMs || 0}:${fileMeta.size || 0}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getCachedSessionMetaSummary(filePath, fileMeta) {
|
|
50
|
+
if (!fileMeta) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return sessionMetaSummaryCache.get(getSessionMetaSummaryCacheKey(filePath, fileMeta)) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setCachedSessionMetaSummary(filePath, fileMeta, summary) {
|
|
57
|
+
if (!fileMeta || !summary?.payload) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cacheKey = getSessionMetaSummaryCacheKey(filePath, fileMeta);
|
|
62
|
+
sessionMetaSummaryCache.delete(cacheKey);
|
|
63
|
+
sessionMetaSummaryCache.set(cacheKey, summary);
|
|
64
|
+
|
|
65
|
+
while (sessionMetaSummaryCache.size > MAX_SESSION_META_SUMMARY_CACHE_ENTRIES) {
|
|
66
|
+
const oldestKey = sessionMetaSummaryCache.keys().next().value;
|
|
67
|
+
if (!oldestKey) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
sessionMetaSummaryCache.delete(oldestKey);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pruneSessionMetaSummaryCache(files = []) {
|
|
75
|
+
const activeKeys = new Set(
|
|
76
|
+
files.map(file => getSessionMetaSummaryCacheKey(file.filePath, file))
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
for (const cacheKey of sessionMetaSummaryCache.keys()) {
|
|
80
|
+
if (!activeKeys.has(cacheKey)) {
|
|
81
|
+
sessionMetaSummaryCache.delete(cacheKey);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
42
86
|
/**
|
|
43
87
|
* 获取会话目录
|
|
44
88
|
*/
|
|
@@ -128,6 +172,7 @@ function scanSessionFiles() {
|
|
|
128
172
|
expiresAt,
|
|
129
173
|
value: new Map(parsed.map(file => [file.sessionId, file]))
|
|
130
174
|
};
|
|
175
|
+
pruneSessionMetaSummaryCache(parsed);
|
|
131
176
|
|
|
132
177
|
return parsed;
|
|
133
178
|
}
|
|
@@ -145,7 +190,7 @@ function getAllSessions() {
|
|
|
145
190
|
const files = scanSessionFiles();
|
|
146
191
|
|
|
147
192
|
const parsed = files.map(file => {
|
|
148
|
-
const fastSummary = readSessionMetaSummaryFast(file.filePath);
|
|
193
|
+
const fastSummary = readSessionMetaSummaryFast(file.filePath, file);
|
|
149
194
|
let session = null;
|
|
150
195
|
|
|
151
196
|
if (fastSummary && fastSummary.payload) {
|
|
@@ -216,7 +261,7 @@ function normalizeSession(codexSession) {
|
|
|
216
261
|
filePath: filePath || '',
|
|
217
262
|
gitBranch: meta.git?.branch || null,
|
|
218
263
|
firstMessage: preview || null,
|
|
219
|
-
forkedFrom: null,
|
|
264
|
+
forkedFrom: null,
|
|
220
265
|
|
|
221
266
|
// 额外的 Codex 特有字段(前端可能需要)
|
|
222
267
|
source: 'codex'
|
|
@@ -655,9 +700,10 @@ function forkSession(sessionId) {
|
|
|
655
700
|
|
|
656
701
|
const newFileName = `rollout-${timestamp}-${newSessionId}.jsonl`;
|
|
657
702
|
const newFilePath = path.join(targetDir, newFileName);
|
|
703
|
+
const rewrittenContent = rewriteForkedCodexSessionContent(content, newSessionId, now.toISOString());
|
|
658
704
|
|
|
659
705
|
// 写入新文件
|
|
660
|
-
fs.writeFileSync(newFilePath,
|
|
706
|
+
fs.writeFileSync(newFilePath, rewrittenContent, 'utf8');
|
|
661
707
|
|
|
662
708
|
// 保存 fork 关系(复用 Claude Code 的 fork 关系存储)
|
|
663
709
|
const { getForkRelations, saveForkRelations } = require('./sessions');
|
|
@@ -674,6 +720,50 @@ function forkSession(sessionId) {
|
|
|
674
720
|
};
|
|
675
721
|
}
|
|
676
722
|
|
|
723
|
+
function rewriteForkedCodexSessionContent(content, newSessionId, nowIsoTimestamp) {
|
|
724
|
+
const lines = String(content || '').split('\n');
|
|
725
|
+
|
|
726
|
+
return lines.map((line) => {
|
|
727
|
+
if (!line.trim()) {
|
|
728
|
+
return line;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let parsed;
|
|
732
|
+
try {
|
|
733
|
+
parsed = JSON.parse(line);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
return line;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (parsed.type === 'session_meta' && parsed.payload && typeof parsed.payload === 'object') {
|
|
739
|
+
return JSON.stringify({
|
|
740
|
+
...parsed,
|
|
741
|
+
timestamp: nowIsoTimestamp,
|
|
742
|
+
payload: {
|
|
743
|
+
...parsed.payload,
|
|
744
|
+
id: newSessionId,
|
|
745
|
+
timestamp: nowIsoTimestamp
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (parsed.type === 'event' && parsed.event && typeof parsed.event === 'object' && parsed.event.type === 'session_start') {
|
|
751
|
+
const nextEvent = { ...parsed.event };
|
|
752
|
+
if (typeof nextEvent.session_id === 'string' && nextEvent.session_id.trim()) {
|
|
753
|
+
nextEvent.session_id = newSessionId;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return JSON.stringify({
|
|
757
|
+
...parsed,
|
|
758
|
+
timestamp: nowIsoTimestamp,
|
|
759
|
+
event: nextEvent
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return line;
|
|
764
|
+
}).join('\n');
|
|
765
|
+
}
|
|
766
|
+
|
|
677
767
|
/**
|
|
678
768
|
* 获取会话排序(按项目)
|
|
679
769
|
* @param {string} projectName - 项目名称
|
|
@@ -803,7 +893,12 @@ function extractCodexPreviewFromResponseItem(payload = {}) {
|
|
|
803
893
|
return text.substring(0, 100);
|
|
804
894
|
}
|
|
805
895
|
|
|
806
|
-
function readSessionMetaSummaryFast(filePath) {
|
|
896
|
+
function readSessionMetaSummaryFast(filePath, fileMeta = null) {
|
|
897
|
+
const cached = getCachedSessionMetaSummary(filePath, fileMeta);
|
|
898
|
+
if (cached) {
|
|
899
|
+
return cached;
|
|
900
|
+
}
|
|
901
|
+
|
|
807
902
|
let fd;
|
|
808
903
|
try {
|
|
809
904
|
fd = fs.openSync(filePath, 'r');
|
|
@@ -841,7 +936,9 @@ function readSessionMetaSummaryFast(filePath) {
|
|
|
841
936
|
}
|
|
842
937
|
|
|
843
938
|
if (!payload) return null;
|
|
844
|
-
|
|
939
|
+
const summary = { payload, preview };
|
|
940
|
+
setCachedSessionMetaSummary(filePath, fileMeta, summary);
|
|
941
|
+
return summary;
|
|
845
942
|
} catch (err) {
|
|
846
943
|
return null;
|
|
847
944
|
} finally {
|
|
@@ -855,8 +952,8 @@ function readSessionMetaSummaryFast(filePath) {
|
|
|
855
952
|
}
|
|
856
953
|
}
|
|
857
954
|
|
|
858
|
-
function readSessionMetaPayloadFast(filePath) {
|
|
859
|
-
const summary = readSessionMetaSummaryFast(filePath);
|
|
955
|
+
function readSessionMetaPayloadFast(filePath, fileMeta = null) {
|
|
956
|
+
const summary = readSessionMetaSummaryFast(filePath, fileMeta);
|
|
860
957
|
return summary?.payload || null;
|
|
861
958
|
}
|
|
862
959
|
|
|
@@ -881,7 +978,7 @@ function calculateProjectAndSessionCounts() {
|
|
|
881
978
|
|
|
882
979
|
const projectNames = new Set();
|
|
883
980
|
sessions.forEach((session) => {
|
|
884
|
-
const payload = readSessionMetaPayloadFast(session.filePath);
|
|
981
|
+
const payload = readSessionMetaPayloadFast(session.filePath, session);
|
|
885
982
|
const projectName = extractCodexProjectNameFromMeta(payload || {});
|
|
886
983
|
if (projectName) {
|
|
887
984
|
projectNames.add(projectName);
|
|
@@ -932,5 +1029,6 @@ module.exports = {
|
|
|
932
1029
|
saveSessionOrder,
|
|
933
1030
|
getProjectOrder,
|
|
934
1031
|
saveProjectOrder,
|
|
935
|
-
getProjectAndSessionCounts
|
|
1032
|
+
getProjectAndSessionCounts,
|
|
1033
|
+
rewriteForkedCodexSessionContent
|
|
936
1034
|
};
|
|
@@ -50,6 +50,19 @@ function isSameOriginRequest(req) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function isRemoteMutationAllowed(envValue) {
|
|
54
|
+
if (envValue === undefined || envValue === null) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalized = String(envValue).trim().toLowerCase();
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return !['0', 'false', 'no', 'off'].includes(normalized);
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
function createRemoteMutationGuard(options = {}) {
|
|
54
67
|
const enabled = options.enabled === true;
|
|
55
68
|
const allowRemoteMutation = options.allowRemoteMutation === true;
|
|
@@ -112,6 +125,7 @@ module.exports = {
|
|
|
112
125
|
isLoopbackAddress,
|
|
113
126
|
isLoopbackRequest,
|
|
114
127
|
isSameOriginRequest,
|
|
128
|
+
isRemoteMutationAllowed,
|
|
115
129
|
createRemoteMutationGuard,
|
|
116
130
|
createRemoteRouteGuard,
|
|
117
131
|
createSameOriginGuard
|
|
@@ -5,6 +5,7 @@ const http = require('http');
|
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const { execSync, execFileSync } = require('child_process');
|
|
7
7
|
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
8
|
+
const { loadConfig } = require('../../config/loader');
|
|
8
9
|
const { loadUIConfig, saveUIConfig } = require('./ui-config');
|
|
9
10
|
const codexSettingsManager = require('./codex-settings-manager');
|
|
10
11
|
const geminiSettingsManager = require('./gemini-settings-manager');
|
|
@@ -57,6 +58,13 @@ function getFeishuConfig() {
|
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
function getBrowserNotificationConfig() {
|
|
62
|
+
const uiConfig = loadUIConfig();
|
|
63
|
+
return {
|
|
64
|
+
enabled: uiConfig.browserNotification?.enabled === true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
|
|
61
69
|
const nextConfig = (uiConfig && typeof uiConfig === 'object') ? { ...uiConfig } : {};
|
|
62
70
|
if (claudeEnabled) {
|
|
@@ -67,7 +75,7 @@ function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
|
|
|
67
75
|
return nextConfig;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
function saveNotificationUiConfig(feishu = {}, claudeEnabled) {
|
|
78
|
+
function saveNotificationUiConfig(feishu = {}, browser = {}, claudeEnabled) {
|
|
71
79
|
let uiConfig = loadUIConfig();
|
|
72
80
|
if (typeof claudeEnabled === 'boolean') {
|
|
73
81
|
uiConfig = applyClaudeDisablePreference(uiConfig, claudeEnabled);
|
|
@@ -76,6 +84,9 @@ function saveNotificationUiConfig(feishu = {}, claudeEnabled) {
|
|
|
76
84
|
enabled: feishu.enabled === true,
|
|
77
85
|
webhookUrl: feishu.webhookUrl || ''
|
|
78
86
|
};
|
|
87
|
+
uiConfig.browserNotification = {
|
|
88
|
+
enabled: browser.enabled === true
|
|
89
|
+
};
|
|
79
90
|
saveUIConfig(uiConfig);
|
|
80
91
|
}
|
|
81
92
|
|
|
@@ -115,6 +126,84 @@ function removeNotifyScript() {
|
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
128
|
|
|
129
|
+
function getNotificationSourceLabel(source = 'claude') {
|
|
130
|
+
switch (String(source || '').trim().toLowerCase()) {
|
|
131
|
+
case 'codex':
|
|
132
|
+
return 'Codex CLI';
|
|
133
|
+
case 'gemini':
|
|
134
|
+
return 'Gemini CLI';
|
|
135
|
+
case 'opencode':
|
|
136
|
+
return 'OpenCode';
|
|
137
|
+
default:
|
|
138
|
+
return 'Claude Code';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveFallbackNotificationMessage(source = 'claude', eventType = '') {
|
|
143
|
+
const normalizedSource = String(source || '').trim().toLowerCase();
|
|
144
|
+
const normalizedEventType = String(eventType || '').trim();
|
|
145
|
+
|
|
146
|
+
if (normalizedSource === 'codex') {
|
|
147
|
+
if (normalizedEventType === 'agent-turn-complete') {
|
|
148
|
+
return 'Codex CLI 回合已完成 | 等待交互';
|
|
149
|
+
}
|
|
150
|
+
return 'Codex CLI 已返回结果 | 等待交互';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (normalizedSource === 'gemini') {
|
|
154
|
+
return 'Gemini CLI 回合已完成 | 等待交互';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (normalizedSource === 'opencode') {
|
|
158
|
+
if (normalizedEventType === 'session.error') {
|
|
159
|
+
return 'OpenCode 会话异常,请检查日志';
|
|
160
|
+
}
|
|
161
|
+
return 'OpenCode 响应已完成 | 等待交互';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return 'Claude Code 任务已完成 | 等待交互';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildBrowserNotificationEndpoint() {
|
|
168
|
+
try {
|
|
169
|
+
const config = loadConfig();
|
|
170
|
+
const port = Number(config?.ports?.webUI);
|
|
171
|
+
const resolvedPort = Number.isFinite(port) && port > 0 ? port : 19999;
|
|
172
|
+
return `http://127.0.0.1:${resolvedPort}/api/hooks/browser-event`;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return 'http://127.0.0.1:19999/api/hooks/browser-event';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildBrowserNotificationPayload(input = {}) {
|
|
179
|
+
const rawSource = String(input?.source || '').trim().toLowerCase();
|
|
180
|
+
const source = ['claude', 'codex', 'gemini', 'opencode'].includes(rawSource) ? rawSource : 'claude';
|
|
181
|
+
const eventType = String(input?.eventType || input?.event_type || '').trim();
|
|
182
|
+
const rawMessage = String(input?.message || '').trim();
|
|
183
|
+
const timestamp = Number(input?.timestamp);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
title: String(input?.title || '').trim() || 'Coding Tool',
|
|
187
|
+
source,
|
|
188
|
+
sourceLabel: getNotificationSourceLabel(source),
|
|
189
|
+
mode: normalizeType(input?.mode),
|
|
190
|
+
eventType,
|
|
191
|
+
message: rawMessage || resolveFallbackNotificationMessage(source, eventType),
|
|
192
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function emitBrowserNotificationEvent(input = {}) {
|
|
197
|
+
const browser = getBrowserNotificationConfig();
|
|
198
|
+
if (!browser.enabled) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const { broadcastBrowserNotification } = require('../websocket-server');
|
|
203
|
+
broadcastBrowserNotification(buildBrowserNotificationPayload(input));
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
118
207
|
function parseManagedType(input) {
|
|
119
208
|
const value = String(input || '');
|
|
120
209
|
const matches = [
|
|
@@ -433,8 +522,10 @@ function runWindowsPowerShellCommand(command) {
|
|
|
433
522
|
});
|
|
434
523
|
}
|
|
435
524
|
|
|
436
|
-
function generateNotifyScript(feishu = {}) {
|
|
525
|
+
function generateNotifyScript(feishu = {}, browser = {}) {
|
|
437
526
|
const feishuEnabled = feishu.enabled === true && !!feishu.webhookUrl;
|
|
527
|
+
const browserEnabled = browser.enabled === true;
|
|
528
|
+
const browserEndpoint = browserEnabled ? buildBrowserNotificationEndpoint() : '';
|
|
438
529
|
|
|
439
530
|
return `#!/usr/bin/env node
|
|
440
531
|
// Coding Tool 通知脚本 - 自动生成,请勿手动修改
|
|
@@ -446,6 +537,8 @@ const { execSync, execFileSync } = require('child_process')
|
|
|
446
537
|
|
|
447
538
|
const FEISHU_ENABLED = ${feishuEnabled ? 'true' : 'false'}
|
|
448
539
|
const FEISHU_WEBHOOK_URL = ${JSON.stringify(feishuEnabled ? feishu.webhookUrl : '')}
|
|
540
|
+
const BROWSER_ENABLED = ${browserEnabled ? 'true' : 'false'}
|
|
541
|
+
const BROWSER_ENDPOINT = ${JSON.stringify(browserEndpoint)}
|
|
449
542
|
|
|
450
543
|
function readArg(name) {
|
|
451
544
|
const prefix = \`\${name}=\`
|
|
@@ -483,21 +576,25 @@ function readOptionalPayload() {
|
|
|
483
576
|
return null
|
|
484
577
|
}
|
|
485
578
|
|
|
486
|
-
function
|
|
487
|
-
|
|
579
|
+
function resolveEventType(eventType, payload) {
|
|
580
|
+
return eventType || payload?.type || payload?.hook_event?.event_type || ''
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function resolveMessage(source, effectiveEventType) {
|
|
584
|
+
const normalizedSource = String(source || '').trim().toLowerCase()
|
|
488
585
|
|
|
489
|
-
if (
|
|
586
|
+
if (normalizedSource === 'codex') {
|
|
490
587
|
if (effectiveEventType === 'agent-turn-complete') {
|
|
491
588
|
return 'Codex CLI 回合已完成 | 等待交互'
|
|
492
589
|
}
|
|
493
590
|
return 'Codex CLI 已返回结果 | 等待交互'
|
|
494
591
|
}
|
|
495
592
|
|
|
496
|
-
if (
|
|
593
|
+
if (normalizedSource === 'gemini') {
|
|
497
594
|
return 'Gemini CLI 回合已完成 | 等待交互'
|
|
498
595
|
}
|
|
499
596
|
|
|
500
|
-
if (
|
|
597
|
+
if (normalizedSource === 'opencode') {
|
|
501
598
|
if (effectiveEventType === 'session.error') {
|
|
502
599
|
return 'OpenCode 会话异常,请检查日志'
|
|
503
600
|
}
|
|
@@ -628,15 +725,63 @@ function sendFeishu(message, source) {
|
|
|
628
725
|
})
|
|
629
726
|
}
|
|
630
727
|
|
|
728
|
+
function sendBrowserNotification(payload) {
|
|
729
|
+
if (!BROWSER_ENABLED || !BROWSER_ENDPOINT) {
|
|
730
|
+
return Promise.resolve()
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return new Promise((resolve) => {
|
|
734
|
+
try {
|
|
735
|
+
const urlObj = new URL(BROWSER_ENDPOINT)
|
|
736
|
+
const data = JSON.stringify(payload)
|
|
737
|
+
const options = {
|
|
738
|
+
hostname: urlObj.hostname,
|
|
739
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
740
|
+
path: urlObj.pathname + urlObj.search,
|
|
741
|
+
method: 'POST',
|
|
742
|
+
headers: {
|
|
743
|
+
'Content-Type': 'application/json',
|
|
744
|
+
'Content-Length': Buffer.byteLength(data)
|
|
745
|
+
},
|
|
746
|
+
timeout: 5000
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const requestModule = urlObj.protocol === 'https:' ? https : http
|
|
750
|
+
const request = requestModule.request(options, () => resolve())
|
|
751
|
+
request.on('error', () => resolve())
|
|
752
|
+
request.on('timeout', () => {
|
|
753
|
+
request.destroy()
|
|
754
|
+
resolve()
|
|
755
|
+
})
|
|
756
|
+
request.write(data)
|
|
757
|
+
request.end()
|
|
758
|
+
} catch (error) {
|
|
759
|
+
resolve()
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
|
|
631
764
|
(async () => {
|
|
632
765
|
const source = readArg('--source') || 'claude'
|
|
633
766
|
const mode = readArg('--mode') || readArg('--cc-notify-type') || 'notification'
|
|
634
767
|
const eventType = readArg('--event-type') || ''
|
|
635
768
|
const payload = readOptionalPayload()
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
769
|
+
const effectiveEventType = resolveEventType(eventType, payload)
|
|
770
|
+
const normalizedMode = mode === 'dialog' ? 'dialog' : 'notification'
|
|
771
|
+
const message = resolveMessage(source, effectiveEventType)
|
|
772
|
+
const timestamp = Date.now()
|
|
773
|
+
|
|
774
|
+
notify(normalizedMode, message)
|
|
775
|
+
await Promise.all([
|
|
776
|
+
sendFeishu(message, source),
|
|
777
|
+
sendBrowserNotification({
|
|
778
|
+
source,
|
|
779
|
+
mode: normalizedMode,
|
|
780
|
+
eventType: effectiveEventType,
|
|
781
|
+
message,
|
|
782
|
+
timestamp
|
|
783
|
+
})
|
|
784
|
+
])
|
|
640
785
|
})().catch(() => {
|
|
641
786
|
process.exit(0)
|
|
642
787
|
})
|
|
@@ -856,9 +1001,9 @@ function runWindowsPowerShellCommand(command) {
|
|
|
856
1001
|
`;
|
|
857
1002
|
}
|
|
858
1003
|
|
|
859
|
-
function writeNotifyScript(feishu = {}) {
|
|
1004
|
+
function writeNotifyScript(feishu = {}, browser = getBrowserNotificationConfig()) {
|
|
860
1005
|
ensureParentDir(PATHS.notifyHook);
|
|
861
|
-
fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu), { mode: 0o755 });
|
|
1006
|
+
fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu, browser), { mode: 0o755 });
|
|
862
1007
|
}
|
|
863
1008
|
|
|
864
1009
|
function getClaudeHookStatus() {
|
|
@@ -1167,6 +1312,7 @@ function getNotificationSettings() {
|
|
|
1167
1312
|
success: true,
|
|
1168
1313
|
platform: os.platform(),
|
|
1169
1314
|
feishu: getFeishuConfig(),
|
|
1315
|
+
browser: getBrowserNotificationConfig(),
|
|
1170
1316
|
platforms: {
|
|
1171
1317
|
claude: getClaudeHookStatus(),
|
|
1172
1318
|
codex: getCodexHookStatus(),
|
|
@@ -1185,6 +1331,7 @@ function normalizePlatformInput(platform = {}) {
|
|
|
1185
1331
|
|
|
1186
1332
|
function saveNotificationSettings(input = {}) {
|
|
1187
1333
|
const existingFeishu = getFeishuConfig();
|
|
1334
|
+
const existingBrowser = getBrowserNotificationConfig();
|
|
1188
1335
|
const requestedWebhookUrl = String(input?.feishu?.webhookUrl || '').trim();
|
|
1189
1336
|
const feishu = {
|
|
1190
1337
|
enabled: input?.feishu?.enabled === true,
|
|
@@ -1193,6 +1340,11 @@ function saveNotificationSettings(input = {}) {
|
|
|
1193
1340
|
if (feishu.enabled && feishu.webhookUrl) {
|
|
1194
1341
|
validateFeishuWebhookUrl(feishu.webhookUrl);
|
|
1195
1342
|
}
|
|
1343
|
+
const browser = {
|
|
1344
|
+
enabled: input?.browser !== undefined
|
|
1345
|
+
? input?.browser?.enabled === true
|
|
1346
|
+
: existingBrowser.enabled === true
|
|
1347
|
+
};
|
|
1196
1348
|
const platforms = {
|
|
1197
1349
|
claude: normalizePlatformInput(input?.platforms?.claude),
|
|
1198
1350
|
codex: normalizePlatformInput(input?.platforms?.codex),
|
|
@@ -1200,11 +1352,11 @@ function saveNotificationSettings(input = {}) {
|
|
|
1200
1352
|
opencode: normalizePlatformInput(input?.platforms?.opencode)
|
|
1201
1353
|
};
|
|
1202
1354
|
|
|
1203
|
-
saveNotificationUiConfig(feishu, platforms.claude.enabled);
|
|
1355
|
+
saveNotificationUiConfig(feishu, browser, platforms.claude.enabled);
|
|
1204
1356
|
|
|
1205
1357
|
const hasManagedPlatform = Object.values(platforms).some((platform) => platform.enabled);
|
|
1206
1358
|
if (hasManagedPlatform) {
|
|
1207
|
-
writeNotifyScript(feishu);
|
|
1359
|
+
writeNotifyScript(feishu, browser);
|
|
1208
1360
|
}
|
|
1209
1361
|
|
|
1210
1362
|
saveClaudeHook(platforms.claude.enabled, platforms.claude.type);
|
|
@@ -1347,6 +1499,9 @@ function buildLegacyClaudeSaveInput(input = {}, currentSettings = getNotificatio
|
|
|
1347
1499
|
: {
|
|
1348
1500
|
enabled: currentSettings?.feishu?.enabled === true,
|
|
1349
1501
|
webhookUrl: currentSettings?.feishu?.webhookUrl || ''
|
|
1502
|
+
},
|
|
1503
|
+
browser: {
|
|
1504
|
+
enabled: currentSettings?.browser?.enabled === true
|
|
1350
1505
|
}
|
|
1351
1506
|
};
|
|
1352
1507
|
}
|
|
@@ -1356,6 +1511,7 @@ function getLegacyClaudeHookSettings() {
|
|
|
1356
1511
|
success: true,
|
|
1357
1512
|
stopHook: parseStopHookStatus(readClaudeSettings()),
|
|
1358
1513
|
feishu: getFeishuConfig(),
|
|
1514
|
+
browser: getBrowserNotificationConfig(),
|
|
1359
1515
|
platform: os.platform()
|
|
1360
1516
|
};
|
|
1361
1517
|
}
|
|
@@ -1553,7 +1709,7 @@ function syncManagedNotificationAssets() {
|
|
|
1553
1709
|
const hasManagedPlatform = Object.values(settings?.platforms || {}).some((platform) => platform?.enabled === true);
|
|
1554
1710
|
|
|
1555
1711
|
if (hasManagedPlatform) {
|
|
1556
|
-
writeNotifyScript(settings.feishu || {});
|
|
1712
|
+
writeNotifyScript(settings.feishu || {}, settings.browser || {});
|
|
1557
1713
|
} else {
|
|
1558
1714
|
removeNotifyScript();
|
|
1559
1715
|
}
|
|
@@ -1575,6 +1731,7 @@ module.exports = {
|
|
|
1575
1731
|
getLegacyClaudeHookSettings,
|
|
1576
1732
|
saveNotificationSettings,
|
|
1577
1733
|
saveLegacyClaudeHookSettings,
|
|
1734
|
+
emitBrowserNotificationEvent,
|
|
1578
1735
|
testNotification,
|
|
1579
1736
|
initDefaultHooks,
|
|
1580
1737
|
syncManagedNotificationAssets,
|
|
@@ -1600,6 +1757,8 @@ module.exports = {
|
|
|
1600
1757
|
buildGeminiCommand,
|
|
1601
1758
|
buildStopHookCommand,
|
|
1602
1759
|
buildClaudeCommand,
|
|
1760
|
+
buildBrowserNotificationEndpoint,
|
|
1761
|
+
buildBrowserNotificationPayload,
|
|
1603
1762
|
buildOpenCodePluginContent,
|
|
1604
1763
|
getOpenCodeManagedPluginPath,
|
|
1605
1764
|
generateNotifyScript,
|