coding-tool-x 3.5.9 → 3.5.11
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 +11 -0
- package/README.md +4 -25
- package/bin/ctx.js +1 -6
- package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-Bw-0r4Ys.js} +2 -2
- package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
- package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-C6QRIeDj.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/{Home-C9TQNB6f.js → Home-B_Z9ythw.js} +1 -1
- package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-Ce-mws6Q.js} +1 -1
- package/dist/web/assets/ProjectList-9Z7FFCIT.js +1 -0
- package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
- package/dist/web/assets/SessionList-CyETQcs8.js +1 -0
- package/dist/web/assets/SessionList-D12muXvx.css +1 -0
- package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-DSUcP1se.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-Cvbc11bJ.js} +1 -1
- package/dist/web/assets/icons-lKCTySl7.js +1 -0
- package/dist/web/assets/index-BS7aplYp.css +1 -0
- package/dist/web/assets/index-CTeg1xG-.js +2 -0
- package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
- package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
- package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
- package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
- package/dist/web/index.html +8 -8
- package/package.json +1 -2
- package/src/commands/daemon.js +5 -33
- package/src/commands/export-config.js +6 -6
- package/src/commands/stats.js +4 -41
- package/src/commands/ui.js +3 -12
- package/src/config/channel-preset-websites.js +114 -0
- package/src/config/default.js +9 -5
- package/src/config/loader.js +2 -2
- package/src/config/model-metadata.json +16 -20
- package/src/config/paths.js +33 -166
- package/src/index.js +34 -125
- package/src/server/api/agents.js +2 -52
- package/src/server/api/codex-channels.js +4 -0
- package/src/server/api/codex-proxy.js +45 -2
- package/src/server/api/codex-sessions.js +89 -12
- package/src/server/api/commands.js +2 -38
- package/src/server/api/dashboard.js +1 -25
- package/src/server/api/gemini-proxy.js +4 -2
- package/src/server/api/gemini-sessions.js +86 -9
- package/src/server/api/hooks.js +1 -17
- package/src/server/api/opencode-proxy.js +7 -2
- package/src/server/api/opencode-sessions.js +83 -10
- package/src/server/api/plugins.js +34 -128
- package/src/server/api/proxy.js +5 -3
- package/src/server/api/sessions.js +181 -27
- package/src/server/api/workspaces.js +0 -14
- package/src/server/index.js +9 -31
- package/src/server/services/agents-service.js +62 -269
- package/src/server/services/base/base-channel-service.js +3 -1
- package/src/server/services/channels.js +5 -0
- package/src/server/services/codex-channels.js +2 -0
- package/src/server/services/codex-sessions.js +88 -108
- package/src/server/services/commands-service.js +81 -281
- package/src/server/services/config-export-service.js +7 -7
- package/src/server/services/config-registry-service.js +9 -8
- package/src/server/services/config-sync-manager.js +41 -61
- package/src/server/services/config-sync-service.js +3 -3
- package/src/server/services/gemini-channels.js +17 -6
- package/src/server/services/gemini-config.js +4 -3
- package/src/server/services/gemini-sessions.js +59 -26
- package/src/server/services/gemini-settings-manager.js +3 -2
- package/src/server/services/mcp-service.js +14 -9
- package/src/server/services/native-oauth-adapters.js +3 -3
- package/src/server/services/network-access.js +2 -61
- package/src/server/services/notification-hooks.js +19 -184
- package/src/server/services/opencode-channels.js +9 -0
- package/src/server/services/opencode-sessions.js +6 -16
- package/src/server/services/opencode-settings-manager.js +3 -3
- package/src/server/services/plugins-service.js +62 -996
- package/src/server/services/prompts-service.js +9 -5
- package/src/server/services/sessions.js +171 -183
- package/src/server/services/skill-service.js +18 -155
- package/src/server/websocket-server.js +4 -35
- package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
- package/dist/web/assets/Home-qzk118Of.css +0 -1
- package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
- package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
- package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
- package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
- package/dist/web/assets/icons-CEq2hYB-.js +0 -1
- package/dist/web/assets/index-Dih_bOsv.css +0 -1
- package/dist/web/assets/index-Duc7QP4e.js +0 -2
- package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
- package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
- package/docs/Caddyfile.example +0 -19
- package/docs/reverse-proxy-https.md +0 -57
- package/src/server/services/https-cert.js +0 -171
- package/src/server/services/session-launch-command.js +0 -58
- package/src/server/services/web-ui-runtime.js +0 -54
|
@@ -6,24 +6,29 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
9
10
|
const toml = require('@iarna/toml');
|
|
10
11
|
const { spawn } = require('child_process');
|
|
11
12
|
const http = require('http');
|
|
12
13
|
const https = require('https');
|
|
13
14
|
const { McpClient, buildMissingCommandMessage, createMissingCommandHint } = require('./mcp-client');
|
|
14
|
-
const { NATIVE_PATHS, PATHS
|
|
15
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
16
|
+
const { resolvePreferredHomeDir } = require('../../utils/home-dir');
|
|
17
|
+
|
|
18
|
+
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
15
19
|
|
|
16
20
|
// MCP 配置文件路径
|
|
17
21
|
const MCP_SERVERS_FILE = PATHS.mcpServers;
|
|
18
22
|
|
|
19
23
|
// 各平台配置文件路径
|
|
20
|
-
const CLAUDE_CONFIG_PATH =
|
|
24
|
+
const CLAUDE_CONFIG_PATH = path.join(HOME_DIR, '.claude.json');
|
|
21
25
|
const CODEX_CONFIG_PATH = NATIVE_PATHS.codex.config;
|
|
22
|
-
const GEMINI_CONFIG_PATH = NATIVE_PATHS.gemini.settings;
|
|
26
|
+
const GEMINI_CONFIG_PATH = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'settings.json');
|
|
27
|
+
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
23
28
|
const OPENCODE_CONFIG_PATHS = {
|
|
24
|
-
jsonc:
|
|
25
|
-
json:
|
|
26
|
-
legacy:
|
|
29
|
+
jsonc: path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'),
|
|
30
|
+
json: path.join(OPENCODE_CONFIG_DIR, 'opencode.json'),
|
|
31
|
+
legacy: path.join(OPENCODE_CONFIG_DIR, 'config.json')
|
|
27
32
|
};
|
|
28
33
|
|
|
29
34
|
// MCP 客户端连接池
|
|
@@ -274,7 +279,7 @@ function readJsonFile(filePath, defaultValue = {}) {
|
|
|
274
279
|
* 安全写入 JSON 文件(原子写入)
|
|
275
280
|
*/
|
|
276
281
|
function writeJsonFile(filePath, data) {
|
|
277
|
-
ensureDir(
|
|
282
|
+
ensureDir(path.dirname(filePath));
|
|
278
283
|
const tempPath = filePath + '.tmp';
|
|
279
284
|
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
280
285
|
fs.renameSync(tempPath, filePath);
|
|
@@ -300,7 +305,7 @@ function readTomlFile(filePath, defaultValue = {}) {
|
|
|
300
305
|
* 安全写入 TOML 文件(原子写入)
|
|
301
306
|
*/
|
|
302
307
|
function writeTomlFile(filePath, data) {
|
|
303
|
-
ensureDir(
|
|
308
|
+
ensureDir(path.dirname(filePath));
|
|
304
309
|
const tempPath = filePath + '.tmp';
|
|
305
310
|
fs.writeFileSync(tempPath, toml.stringify(data), 'utf-8');
|
|
306
311
|
fs.renameSync(tempPath, filePath);
|
|
@@ -407,7 +412,7 @@ function readOpenCodeConfig() {
|
|
|
407
412
|
* 写入 OpenCode 配置(保持 JSON 格式)
|
|
408
413
|
*/
|
|
409
414
|
function writeOpenCodeConfig(filePath, data) {
|
|
410
|
-
ensureDir(
|
|
415
|
+
ensureDir(path.dirname(filePath));
|
|
411
416
|
const tempPath = filePath + '.tmp';
|
|
412
417
|
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
413
418
|
fs.renameSync(tempPath, filePath);
|
|
@@ -4,7 +4,7 @@ const os = require('os');
|
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const toml = require('toml');
|
|
6
6
|
const tomlStringify = require('@iarna/toml').stringify;
|
|
7
|
-
const { NATIVE_PATHS, PATHS
|
|
7
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
8
8
|
const claudeSettingsManager = require('./settings-manager');
|
|
9
9
|
const codexSettingsManager = require('./codex-settings-manager');
|
|
10
10
|
const geminiSettingsManager = require('./gemini-settings-manager');
|
|
@@ -35,7 +35,7 @@ function ensureFileMode(filePath, mode = 0o600) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function writeJsonFile(filePath, value) {
|
|
38
|
-
ensureDir(
|
|
38
|
+
ensureDir(path.dirname(filePath));
|
|
39
39
|
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
|
|
40
40
|
ensureFileMode(filePath);
|
|
41
41
|
}
|
|
@@ -562,7 +562,7 @@ function applyGeminiOAuth(credential) {
|
|
|
562
562
|
}
|
|
563
563
|
};
|
|
564
564
|
|
|
565
|
-
ensureDir(
|
|
565
|
+
ensureDir(path.dirname(NATIVE_PATHS.gemini.oauthCredentialsEncrypted));
|
|
566
566
|
fs.writeFileSync(
|
|
567
567
|
NATIVE_PATHS.gemini.oauthCredentialsEncrypted,
|
|
568
568
|
encryptGeminiPayload(JSON.stringify(payload, null, 2)),
|
|
@@ -16,21 +16,6 @@ function isLoopbackAddress(address) {
|
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function hasTrustedProxySocket(req) {
|
|
20
|
-
if (!req) return false;
|
|
21
|
-
return isLoopbackAddress(req.socket && req.socket.remoteAddress);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function getForwardedHeaderValue(req, headerName) {
|
|
25
|
-
if (!hasTrustedProxySocket(req) || !req.headers) {
|
|
26
|
-
return '';
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const rawValue = req.headers[headerName];
|
|
30
|
-
const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
31
|
-
return String(value || '').split(',')[0].trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
19
|
function isLoopbackRequest(req) {
|
|
35
20
|
if (!req) return false;
|
|
36
21
|
const socketAddress = req.socket && req.socket.remoteAddress;
|
|
@@ -45,34 +30,6 @@ function isLoopbackRequest(req) {
|
|
|
45
30
|
return true;
|
|
46
31
|
}
|
|
47
32
|
|
|
48
|
-
function getRequestProtocol(req) {
|
|
49
|
-
const forwardedProto = getForwardedHeaderValue(req, 'x-forwarded-proto').toLowerCase();
|
|
50
|
-
if (forwardedProto === 'https' || forwardedProto === 'https:') {
|
|
51
|
-
return 'https:';
|
|
52
|
-
}
|
|
53
|
-
if (forwardedProto === 'http' || forwardedProto === 'http:') {
|
|
54
|
-
return 'http:';
|
|
55
|
-
}
|
|
56
|
-
return req && req.socket && req.socket.encrypted ? 'https:' : 'http:';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getRequestHost(req) {
|
|
60
|
-
const forwardedHost = getForwardedHeaderValue(req, 'x-forwarded-host');
|
|
61
|
-
if (forwardedHost) {
|
|
62
|
-
return forwardedHost;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!req || !req.headers) {
|
|
66
|
-
return '';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const host = req.headers.host;
|
|
70
|
-
if (Array.isArray(host)) {
|
|
71
|
-
return String(host[0] || '').trim();
|
|
72
|
-
}
|
|
73
|
-
return String(host || '').trim();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
33
|
function isSameOriginRequest(req) {
|
|
77
34
|
if (!req) return false;
|
|
78
35
|
const origin = req.headers && req.headers.origin;
|
|
@@ -80,32 +37,19 @@ function isSameOriginRequest(req) {
|
|
|
80
37
|
return true;
|
|
81
38
|
}
|
|
82
39
|
|
|
83
|
-
const host =
|
|
40
|
+
const host = req.headers && req.headers.host;
|
|
84
41
|
if (!host) {
|
|
85
42
|
return false;
|
|
86
43
|
}
|
|
87
44
|
|
|
88
45
|
try {
|
|
89
46
|
const originUrl = new URL(origin);
|
|
90
|
-
return originUrl.host
|
|
47
|
+
return originUrl.host === host;
|
|
91
48
|
} catch (error) {
|
|
92
49
|
return false;
|
|
93
50
|
}
|
|
94
51
|
}
|
|
95
52
|
|
|
96
|
-
function isRemoteMutationAllowed(envValue) {
|
|
97
|
-
if (envValue === undefined || envValue === null) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const normalized = String(envValue).trim().toLowerCase();
|
|
102
|
-
if (!normalized) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return !['0', 'false', 'no', 'off'].includes(normalized);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
53
|
function createRemoteMutationGuard(options = {}) {
|
|
110
54
|
const enabled = options.enabled === true;
|
|
111
55
|
const allowRemoteMutation = options.allowRemoteMutation === true;
|
|
@@ -167,10 +111,7 @@ module.exports = {
|
|
|
167
111
|
normalizeAddress,
|
|
168
112
|
isLoopbackAddress,
|
|
169
113
|
isLoopbackRequest,
|
|
170
|
-
getRequestProtocol,
|
|
171
|
-
getRequestHost,
|
|
172
114
|
isSameOriginRequest,
|
|
173
|
-
isRemoteMutationAllowed,
|
|
174
115
|
createRemoteMutationGuard,
|
|
175
116
|
createRemoteRouteGuard,
|
|
176
117
|
createSameOriginGuard
|
|
@@ -4,12 +4,10 @@ const os = require('os');
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const { execSync, execFileSync } = require('child_process');
|
|
7
|
-
const { PATHS, NATIVE_PATHS
|
|
8
|
-
const { loadConfig } = require('../../config/loader');
|
|
7
|
+
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
9
8
|
const { loadUIConfig, saveUIConfig } = require('./ui-config');
|
|
10
9
|
const codexSettingsManager = require('./codex-settings-manager');
|
|
11
10
|
const geminiSettingsManager = require('./gemini-settings-manager');
|
|
12
|
-
const { getWebUiProtocol } = require('./web-ui-runtime');
|
|
13
11
|
|
|
14
12
|
const MANAGED_HOOK_NAME = 'coding-tool-notify';
|
|
15
13
|
const MANAGED_OPENCODE_PLUGIN_FILE = 'coding-tool-notify.js';
|
|
@@ -19,7 +17,7 @@ function normalizeType(type) {
|
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
function ensureParentDir(filePath) {
|
|
22
|
-
const dir =
|
|
20
|
+
const dir = path.dirname(filePath);
|
|
23
21
|
if (!fs.existsSync(dir)) {
|
|
24
22
|
fs.mkdirSync(dir, { recursive: true });
|
|
25
23
|
}
|
|
@@ -59,13 +57,6 @@ function getFeishuConfig() {
|
|
|
59
57
|
};
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
function getBrowserNotificationConfig() {
|
|
63
|
-
const uiConfig = loadUIConfig();
|
|
64
|
-
return {
|
|
65
|
-
enabled: uiConfig.browserNotification?.enabled === true
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
60
|
function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
|
|
70
61
|
const nextConfig = (uiConfig && typeof uiConfig === 'object') ? { ...uiConfig } : {};
|
|
71
62
|
if (claudeEnabled) {
|
|
@@ -76,7 +67,7 @@ function applyClaudeDisablePreference(uiConfig = {}, claudeEnabled) {
|
|
|
76
67
|
return nextConfig;
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
function saveNotificationUiConfig(feishu = {},
|
|
70
|
+
function saveNotificationUiConfig(feishu = {}, claudeEnabled) {
|
|
80
71
|
let uiConfig = loadUIConfig();
|
|
81
72
|
if (typeof claudeEnabled === 'boolean') {
|
|
82
73
|
uiConfig = applyClaudeDisablePreference(uiConfig, claudeEnabled);
|
|
@@ -85,9 +76,6 @@ function saveNotificationUiConfig(feishu = {}, browser = {}, claudeEnabled) {
|
|
|
85
76
|
enabled: feishu.enabled === true,
|
|
86
77
|
webhookUrl: feishu.webhookUrl || ''
|
|
87
78
|
};
|
|
88
|
-
uiConfig.browserNotification = {
|
|
89
|
-
enabled: browser.enabled === true
|
|
90
|
-
};
|
|
91
79
|
saveUIConfig(uiConfig);
|
|
92
80
|
}
|
|
93
81
|
|
|
@@ -127,85 +115,6 @@ function removeNotifyScript() {
|
|
|
127
115
|
}
|
|
128
116
|
}
|
|
129
117
|
|
|
130
|
-
function getNotificationSourceLabel(source = 'claude') {
|
|
131
|
-
switch (String(source || '').trim().toLowerCase()) {
|
|
132
|
-
case 'codex':
|
|
133
|
-
return 'Codex CLI';
|
|
134
|
-
case 'gemini':
|
|
135
|
-
return 'Gemini CLI';
|
|
136
|
-
case 'opencode':
|
|
137
|
-
return 'OpenCode';
|
|
138
|
-
default:
|
|
139
|
-
return 'Claude Code';
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function resolveFallbackNotificationMessage(source = 'claude', eventType = '') {
|
|
144
|
-
const normalizedSource = String(source || '').trim().toLowerCase();
|
|
145
|
-
const normalizedEventType = String(eventType || '').trim();
|
|
146
|
-
|
|
147
|
-
if (normalizedSource === 'codex') {
|
|
148
|
-
if (normalizedEventType === 'agent-turn-complete') {
|
|
149
|
-
return 'Codex CLI 回合已完成 | 等待交互';
|
|
150
|
-
}
|
|
151
|
-
return 'Codex CLI 已返回结果 | 等待交互';
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (normalizedSource === 'gemini') {
|
|
155
|
-
return 'Gemini CLI 回合已完成 | 等待交互';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (normalizedSource === 'opencode') {
|
|
159
|
-
if (normalizedEventType === 'session.error') {
|
|
160
|
-
return 'OpenCode 会话异常,请检查日志';
|
|
161
|
-
}
|
|
162
|
-
return 'OpenCode 响应已完成 | 等待交互';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return 'Claude Code 任务已完成 | 等待交互';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function buildBrowserNotificationEndpoint() {
|
|
169
|
-
try {
|
|
170
|
-
const config = loadConfig();
|
|
171
|
-
const port = Number(config?.ports?.webUI);
|
|
172
|
-
const resolvedPort = Number.isFinite(port) && port > 0 ? port : 19999;
|
|
173
|
-
const protocol = getWebUiProtocol();
|
|
174
|
-
return `${protocol}://127.0.0.1:${resolvedPort}/api/hooks/browser-event`;
|
|
175
|
-
} catch (error) {
|
|
176
|
-
return `${getWebUiProtocol()}://127.0.0.1:19999/api/hooks/browser-event`;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function buildBrowserNotificationPayload(input = {}) {
|
|
181
|
-
const rawSource = String(input?.source || '').trim().toLowerCase();
|
|
182
|
-
const source = ['claude', 'codex', 'gemini', 'opencode'].includes(rawSource) ? rawSource : 'claude';
|
|
183
|
-
const eventType = String(input?.eventType || input?.event_type || '').trim();
|
|
184
|
-
const rawMessage = String(input?.message || '').trim();
|
|
185
|
-
const timestamp = Number(input?.timestamp);
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
title: String(input?.title || '').trim() || 'Coding Tool',
|
|
189
|
-
source,
|
|
190
|
-
sourceLabel: getNotificationSourceLabel(source),
|
|
191
|
-
mode: normalizeType(input?.mode),
|
|
192
|
-
eventType,
|
|
193
|
-
message: rawMessage || resolveFallbackNotificationMessage(source, eventType),
|
|
194
|
-
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now()
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function emitBrowserNotificationEvent(input = {}) {
|
|
199
|
-
const browser = getBrowserNotificationConfig();
|
|
200
|
-
if (!browser.enabled) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const { broadcastBrowserNotification } = require('../websocket-server');
|
|
205
|
-
broadcastBrowserNotification(buildBrowserNotificationPayload(input));
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
118
|
function parseManagedType(input) {
|
|
210
119
|
const value = String(input || '');
|
|
211
120
|
const matches = [
|
|
@@ -269,7 +178,7 @@ function buildGeminiCommand(type) {
|
|
|
269
178
|
}
|
|
270
179
|
|
|
271
180
|
function getOpenCodeManagedPluginPath() {
|
|
272
|
-
return
|
|
181
|
+
return path.join(NATIVE_PATHS.opencode.config, 'plugins', MANAGED_OPENCODE_PLUGIN_FILE);
|
|
273
182
|
}
|
|
274
183
|
|
|
275
184
|
function buildOpenCodePluginContent(type) {
|
|
@@ -524,10 +433,8 @@ function runWindowsPowerShellCommand(command) {
|
|
|
524
433
|
});
|
|
525
434
|
}
|
|
526
435
|
|
|
527
|
-
function generateNotifyScript(feishu = {}
|
|
436
|
+
function generateNotifyScript(feishu = {}) {
|
|
528
437
|
const feishuEnabled = feishu.enabled === true && !!feishu.webhookUrl;
|
|
529
|
-
const browserEnabled = browser.enabled === true;
|
|
530
|
-
const browserEndpoint = browserEnabled ? buildBrowserNotificationEndpoint() : '';
|
|
531
438
|
|
|
532
439
|
return `#!/usr/bin/env node
|
|
533
440
|
// Coding Tool 通知脚本 - 自动生成,请勿手动修改
|
|
@@ -539,8 +446,6 @@ const { execSync, execFileSync } = require('child_process')
|
|
|
539
446
|
|
|
540
447
|
const FEISHU_ENABLED = ${feishuEnabled ? 'true' : 'false'}
|
|
541
448
|
const FEISHU_WEBHOOK_URL = ${JSON.stringify(feishuEnabled ? feishu.webhookUrl : '')}
|
|
542
|
-
const BROWSER_ENABLED = ${browserEnabled ? 'true' : 'false'}
|
|
543
|
-
const BROWSER_ENDPOINT = ${JSON.stringify(browserEndpoint)}
|
|
544
449
|
|
|
545
450
|
function readArg(name) {
|
|
546
451
|
const prefix = \`\${name}=\`
|
|
@@ -578,25 +483,21 @@ function readOptionalPayload() {
|
|
|
578
483
|
return null
|
|
579
484
|
}
|
|
580
485
|
|
|
581
|
-
function
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function resolveMessage(source, effectiveEventType) {
|
|
586
|
-
const normalizedSource = String(source || '').trim().toLowerCase()
|
|
486
|
+
function resolveMessage(source, eventType, payload) {
|
|
487
|
+
const effectiveEventType = eventType || payload?.type || payload?.hook_event?.event_type || ''
|
|
587
488
|
|
|
588
|
-
if (
|
|
489
|
+
if (source === 'codex') {
|
|
589
490
|
if (effectiveEventType === 'agent-turn-complete') {
|
|
590
491
|
return 'Codex CLI 回合已完成 | 等待交互'
|
|
591
492
|
}
|
|
592
493
|
return 'Codex CLI 已返回结果 | 等待交互'
|
|
593
494
|
}
|
|
594
495
|
|
|
595
|
-
if (
|
|
496
|
+
if (source === 'gemini') {
|
|
596
497
|
return 'Gemini CLI 回合已完成 | 等待交互'
|
|
597
498
|
}
|
|
598
499
|
|
|
599
|
-
if (
|
|
500
|
+
if (source === 'opencode') {
|
|
600
501
|
if (effectiveEventType === 'session.error') {
|
|
601
502
|
return 'OpenCode 会话异常,请检查日志'
|
|
602
503
|
}
|
|
@@ -727,67 +628,15 @@ function sendFeishu(message, source) {
|
|
|
727
628
|
})
|
|
728
629
|
}
|
|
729
630
|
|
|
730
|
-
function sendBrowserNotification(payload) {
|
|
731
|
-
if (!BROWSER_ENABLED || !BROWSER_ENDPOINT) {
|
|
732
|
-
return Promise.resolve()
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return new Promise((resolve) => {
|
|
736
|
-
try {
|
|
737
|
-
const urlObj = new URL(BROWSER_ENDPOINT)
|
|
738
|
-
const data = JSON.stringify(payload)
|
|
739
|
-
const options = {
|
|
740
|
-
hostname: urlObj.hostname,
|
|
741
|
-
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
742
|
-
path: urlObj.pathname + urlObj.search,
|
|
743
|
-
method: 'POST',
|
|
744
|
-
headers: {
|
|
745
|
-
'Content-Type': 'application/json',
|
|
746
|
-
'Content-Length': Buffer.byteLength(data)
|
|
747
|
-
},
|
|
748
|
-
timeout: 5000
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (urlObj.protocol === 'https:' && ['127.0.0.1', 'localhost', '::1'].includes(urlObj.hostname)) {
|
|
752
|
-
options.rejectUnauthorized = false
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const requestModule = urlObj.protocol === 'https:' ? https : http
|
|
756
|
-
const request = requestModule.request(options, () => resolve())
|
|
757
|
-
request.on('error', () => resolve())
|
|
758
|
-
request.on('timeout', () => {
|
|
759
|
-
request.destroy()
|
|
760
|
-
resolve()
|
|
761
|
-
})
|
|
762
|
-
request.write(data)
|
|
763
|
-
request.end()
|
|
764
|
-
} catch (error) {
|
|
765
|
-
resolve()
|
|
766
|
-
}
|
|
767
|
-
})
|
|
768
|
-
}
|
|
769
|
-
|
|
770
631
|
(async () => {
|
|
771
632
|
const source = readArg('--source') || 'claude'
|
|
772
633
|
const mode = readArg('--mode') || readArg('--cc-notify-type') || 'notification'
|
|
773
634
|
const eventType = readArg('--event-type') || ''
|
|
774
635
|
const payload = readOptionalPayload()
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
notify(normalizedMode, message)
|
|
781
|
-
await Promise.all([
|
|
782
|
-
sendFeishu(message, source),
|
|
783
|
-
sendBrowserNotification({
|
|
784
|
-
source,
|
|
785
|
-
mode: normalizedMode,
|
|
786
|
-
eventType: effectiveEventType,
|
|
787
|
-
message,
|
|
788
|
-
timestamp
|
|
789
|
-
})
|
|
790
|
-
])
|
|
636
|
+
const message = resolveMessage(source, eventType, payload)
|
|
637
|
+
|
|
638
|
+
notify(mode === 'dialog' ? 'dialog' : 'notification', message)
|
|
639
|
+
await sendFeishu(message, source)
|
|
791
640
|
})().catch(() => {
|
|
792
641
|
process.exit(0)
|
|
793
642
|
})
|
|
@@ -1007,9 +856,9 @@ function runWindowsPowerShellCommand(command) {
|
|
|
1007
856
|
`;
|
|
1008
857
|
}
|
|
1009
858
|
|
|
1010
|
-
function writeNotifyScript(feishu = {}
|
|
859
|
+
function writeNotifyScript(feishu = {}) {
|
|
1011
860
|
ensureParentDir(PATHS.notifyHook);
|
|
1012
|
-
fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu
|
|
861
|
+
fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu), { mode: 0o755 });
|
|
1013
862
|
}
|
|
1014
863
|
|
|
1015
864
|
function getClaudeHookStatus() {
|
|
@@ -1318,7 +1167,6 @@ function getNotificationSettings() {
|
|
|
1318
1167
|
success: true,
|
|
1319
1168
|
platform: os.platform(),
|
|
1320
1169
|
feishu: getFeishuConfig(),
|
|
1321
|
-
browser: getBrowserNotificationConfig(),
|
|
1322
1170
|
platforms: {
|
|
1323
1171
|
claude: getClaudeHookStatus(),
|
|
1324
1172
|
codex: getCodexHookStatus(),
|
|
@@ -1337,7 +1185,6 @@ function normalizePlatformInput(platform = {}) {
|
|
|
1337
1185
|
|
|
1338
1186
|
function saveNotificationSettings(input = {}) {
|
|
1339
1187
|
const existingFeishu = getFeishuConfig();
|
|
1340
|
-
const existingBrowser = getBrowserNotificationConfig();
|
|
1341
1188
|
const requestedWebhookUrl = String(input?.feishu?.webhookUrl || '').trim();
|
|
1342
1189
|
const feishu = {
|
|
1343
1190
|
enabled: input?.feishu?.enabled === true,
|
|
@@ -1346,11 +1193,6 @@ function saveNotificationSettings(input = {}) {
|
|
|
1346
1193
|
if (feishu.enabled && feishu.webhookUrl) {
|
|
1347
1194
|
validateFeishuWebhookUrl(feishu.webhookUrl);
|
|
1348
1195
|
}
|
|
1349
|
-
const browser = {
|
|
1350
|
-
enabled: input?.browser !== undefined
|
|
1351
|
-
? input?.browser?.enabled === true
|
|
1352
|
-
: existingBrowser.enabled === true
|
|
1353
|
-
};
|
|
1354
1196
|
const platforms = {
|
|
1355
1197
|
claude: normalizePlatformInput(input?.platforms?.claude),
|
|
1356
1198
|
codex: normalizePlatformInput(input?.platforms?.codex),
|
|
@@ -1358,11 +1200,11 @@ function saveNotificationSettings(input = {}) {
|
|
|
1358
1200
|
opencode: normalizePlatformInput(input?.platforms?.opencode)
|
|
1359
1201
|
};
|
|
1360
1202
|
|
|
1361
|
-
saveNotificationUiConfig(feishu,
|
|
1203
|
+
saveNotificationUiConfig(feishu, platforms.claude.enabled);
|
|
1362
1204
|
|
|
1363
1205
|
const hasManagedPlatform = Object.values(platforms).some((platform) => platform.enabled);
|
|
1364
1206
|
if (hasManagedPlatform) {
|
|
1365
|
-
writeNotifyScript(feishu
|
|
1207
|
+
writeNotifyScript(feishu);
|
|
1366
1208
|
}
|
|
1367
1209
|
|
|
1368
1210
|
saveClaudeHook(platforms.claude.enabled, platforms.claude.type);
|
|
@@ -1505,9 +1347,6 @@ function buildLegacyClaudeSaveInput(input = {}, currentSettings = getNotificatio
|
|
|
1505
1347
|
: {
|
|
1506
1348
|
enabled: currentSettings?.feishu?.enabled === true,
|
|
1507
1349
|
webhookUrl: currentSettings?.feishu?.webhookUrl || ''
|
|
1508
|
-
},
|
|
1509
|
-
browser: {
|
|
1510
|
-
enabled: currentSettings?.browser?.enabled === true
|
|
1511
1350
|
}
|
|
1512
1351
|
};
|
|
1513
1352
|
}
|
|
@@ -1517,7 +1356,6 @@ function getLegacyClaudeHookSettings() {
|
|
|
1517
1356
|
success: true,
|
|
1518
1357
|
stopHook: parseStopHookStatus(readClaudeSettings()),
|
|
1519
1358
|
feishu: getFeishuConfig(),
|
|
1520
|
-
browser: getBrowserNotificationConfig(),
|
|
1521
1359
|
platform: os.platform()
|
|
1522
1360
|
};
|
|
1523
1361
|
}
|
|
@@ -1715,7 +1553,7 @@ function syncManagedNotificationAssets() {
|
|
|
1715
1553
|
const hasManagedPlatform = Object.values(settings?.platforms || {}).some((platform) => platform?.enabled === true);
|
|
1716
1554
|
|
|
1717
1555
|
if (hasManagedPlatform) {
|
|
1718
|
-
writeNotifyScript(settings.feishu || {}
|
|
1556
|
+
writeNotifyScript(settings.feishu || {});
|
|
1719
1557
|
} else {
|
|
1720
1558
|
removeNotifyScript();
|
|
1721
1559
|
}
|
|
@@ -1737,7 +1575,6 @@ module.exports = {
|
|
|
1737
1575
|
getLegacyClaudeHookSettings,
|
|
1738
1576
|
saveNotificationSettings,
|
|
1739
1577
|
saveLegacyClaudeHookSettings,
|
|
1740
|
-
emitBrowserNotificationEvent,
|
|
1741
1578
|
testNotification,
|
|
1742
1579
|
initDefaultHooks,
|
|
1743
1580
|
syncManagedNotificationAssets,
|
|
@@ -1763,8 +1600,6 @@ module.exports = {
|
|
|
1763
1600
|
buildGeminiCommand,
|
|
1764
1601
|
buildStopHookCommand,
|
|
1765
1602
|
buildClaudeCommand,
|
|
1766
|
-
buildBrowserNotificationEndpoint,
|
|
1767
|
-
buildBrowserNotificationPayload,
|
|
1768
1603
|
buildOpenCodePluginContent,
|
|
1769
1604
|
getOpenCodeManagedPluginPath,
|
|
1770
1605
|
generateNotifyScript,
|
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { PATHS } = require('../../config/paths');
|
|
5
|
+
const { resolveChannelWebsiteUrl } = require('../../config/channel-preset-websites');
|
|
5
6
|
const {
|
|
6
7
|
setChannelConfig,
|
|
7
8
|
clearManagedChannelConfig
|
|
@@ -83,6 +84,7 @@ function loadChannels() {
|
|
|
83
84
|
allowedModels: ch.allowedModels || []
|
|
84
85
|
};
|
|
85
86
|
normalized.providerKey = deriveProviderKey(normalized);
|
|
87
|
+
normalized.websiteUrl = resolveChannelWebsiteUrl('opencode', normalized);
|
|
86
88
|
return normalized;
|
|
87
89
|
});
|
|
88
90
|
}
|
|
@@ -165,6 +167,7 @@ function createChannel(name, baseUrl, apiKey, extraConfig = {}) {
|
|
|
165
167
|
updatedAt: Date.now()
|
|
166
168
|
};
|
|
167
169
|
newChannel.providerKey = extraConfig.providerKey || deriveProviderKey(newChannel);
|
|
170
|
+
newChannel.websiteUrl = resolveChannelWebsiteUrl('opencode', newChannel);
|
|
168
171
|
|
|
169
172
|
data.channels.push(newChannel);
|
|
170
173
|
|
|
@@ -213,6 +216,7 @@ function updateChannel(channelId, updates) {
|
|
|
213
216
|
updatedAt: Date.now()
|
|
214
217
|
};
|
|
215
218
|
merged.providerKey = updates.providerKey || deriveProviderKey(merged);
|
|
219
|
+
merged.websiteUrl = resolveChannelWebsiteUrl('opencode', merged);
|
|
216
220
|
data.channels[index] = merged;
|
|
217
221
|
|
|
218
222
|
const isProxyRunning = getOpenCodeProxyRunning();
|
|
@@ -268,6 +272,10 @@ function getEnabledChannels() {
|
|
|
268
272
|
return data.channels.filter(c => c.enabled !== false);
|
|
269
273
|
}
|
|
270
274
|
|
|
275
|
+
function markChannelAsRecentlyUsed(channelId) {
|
|
276
|
+
return updateChannel(channelId, {});
|
|
277
|
+
}
|
|
278
|
+
|
|
271
279
|
// 保存渠道顺序
|
|
272
280
|
function saveChannelOrder(order) {
|
|
273
281
|
const data = loadChannels();
|
|
@@ -438,6 +446,7 @@ module.exports = {
|
|
|
438
446
|
getChannels,
|
|
439
447
|
createChannel,
|
|
440
448
|
updateChannel,
|
|
449
|
+
markChannelAsRecentlyUsed,
|
|
441
450
|
deleteChannel,
|
|
442
451
|
getEnabledChannels,
|
|
443
452
|
saveChannelOrder,
|
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { execFileSync } = require('child_process');
|
|
5
|
-
const { NATIVE_PATHS, PATHS
|
|
5
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* OpenCode 会话服务
|
|
@@ -11,7 +11,7 @@ const { NATIVE_PATHS, PATHS, joinNativeBasePath } = require('../../config/paths'
|
|
|
11
11
|
|
|
12
12
|
const PROJECT_ORDER_FILE = PATHS.opencodeProjectOrder;
|
|
13
13
|
const SESSION_ORDER_FILE = PATHS.opencodeSessionOrder;
|
|
14
|
-
const OPENCODE_DB_PATH =
|
|
14
|
+
const OPENCODE_DB_PATH = path.join(NATIVE_PATHS.opencode.data, 'opencode.db');
|
|
15
15
|
const COUNTS_CACHE_TTL_MS = 30 * 1000;
|
|
16
16
|
const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
|
|
17
17
|
|
|
@@ -234,12 +234,12 @@ function getOpenCodeDataDir() {
|
|
|
234
234
|
|
|
235
235
|
// 兼容导出:保留旧路径函数
|
|
236
236
|
function getSessionsDir() {
|
|
237
|
-
return
|
|
237
|
+
return path.join(getOpenCodeDataDir(), 'storage', 'session');
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
// 兼容导出:保留旧路径函数
|
|
241
241
|
function getProjectsDir() {
|
|
242
|
-
return
|
|
242
|
+
return path.join(getOpenCodeDataDir(), 'storage', 'project');
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
function getProjectOrder() {
|
|
@@ -513,13 +513,7 @@ function getProjects() {
|
|
|
513
513
|
|
|
514
514
|
// 根据项目ID获取会话列表
|
|
515
515
|
function getSessionsByProjectId(projectId) {
|
|
516
|
-
const
|
|
517
|
-
const forkRelations = getForkRelations();
|
|
518
|
-
const sessions = getSessionRowsByProjectId(projectId).map((session) => {
|
|
519
|
-
const normalized = normalizeSession(session, projectId);
|
|
520
|
-
normalized.forkedFrom = forkRelations[normalized.sessionId] || null;
|
|
521
|
-
return normalized;
|
|
522
|
-
});
|
|
516
|
+
const sessions = getSessionRowsByProjectId(projectId).map(session => normalizeSession(session, projectId));
|
|
523
517
|
const order = getSessionOrder(projectId);
|
|
524
518
|
|
|
525
519
|
const fallbackSorted = sessions.sort(
|
|
@@ -564,11 +558,7 @@ function getSessionById(sessionId) {
|
|
|
564
558
|
return null;
|
|
565
559
|
}
|
|
566
560
|
|
|
567
|
-
|
|
568
|
-
const forkRelations = getForkRelations();
|
|
569
|
-
const normalized = normalizeSession(location.sessionData, location.projectId);
|
|
570
|
-
normalized.forkedFrom = forkRelations[normalized.sessionId] || null;
|
|
571
|
-
return normalized;
|
|
561
|
+
return normalizeSession(location.sessionData, location.projectId);
|
|
572
562
|
}
|
|
573
563
|
|
|
574
564
|
function buildSessionMessages(sessionId) {
|
|
@@ -23,9 +23,9 @@ function resolveModelCost(modelId) {
|
|
|
23
23
|
|
|
24
24
|
const CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
25
25
|
const CONFIG_PATHS = {
|
|
26
|
-
config:
|
|
27
|
-
opencode:
|
|
28
|
-
opencodec:
|
|
26
|
+
config: path.join(CONFIG_DIR, 'config.json'),
|
|
27
|
+
opencode: path.join(CONFIG_DIR, 'opencode.json'),
|
|
28
|
+
opencodec: path.join(CONFIG_DIR, 'opencode.jsonc')
|
|
29
29
|
};
|
|
30
30
|
const BACKUP_SUFFIX = '.cc-tool-backup';
|
|
31
31
|
const EMPTY_SENTINEL = '__CC_TOOL_NO_FILE__';
|