coding-tool-x 3.5.9 → 3.5.10

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 (91) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -25
  3. package/bin/ctx.js +1 -6
  4. package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-C9TQNB6f.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-jgeQOE24.js} +1 -1
  10. package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
  13. package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
  14. package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-D3B0Y5XE.js} +1 -1
  16. package/dist/web/assets/icons-lKCTySl7.js +1 -0
  17. package/dist/web/assets/index-BS7aplYp.css +1 -0
  18. package/dist/web/assets/index-BnyNm74G.js +2 -0
  19. package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
  20. package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
  21. package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
  22. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
  23. package/dist/web/index.html +8 -8
  24. package/package.json +1 -2
  25. package/src/commands/daemon.js +5 -33
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/stats.js +4 -41
  28. package/src/commands/ui.js +3 -12
  29. package/src/config/channel-preset-websites.js +114 -0
  30. package/src/config/default.js +9 -5
  31. package/src/config/loader.js +2 -2
  32. package/src/config/model-metadata.json +16 -20
  33. package/src/config/paths.js +33 -166
  34. package/src/index.js +34 -125
  35. package/src/server/api/agents.js +2 -52
  36. package/src/server/api/codex-proxy.js +4 -2
  37. package/src/server/api/codex-sessions.js +89 -12
  38. package/src/server/api/commands.js +2 -38
  39. package/src/server/api/dashboard.js +1 -25
  40. package/src/server/api/gemini-proxy.js +4 -2
  41. package/src/server/api/gemini-sessions.js +86 -9
  42. package/src/server/api/hooks.js +1 -17
  43. package/src/server/api/opencode-proxy.js +7 -2
  44. package/src/server/api/opencode-sessions.js +83 -10
  45. package/src/server/api/plugins.js +34 -128
  46. package/src/server/api/proxy.js +5 -3
  47. package/src/server/api/sessions.js +181 -27
  48. package/src/server/api/workspaces.js +0 -14
  49. package/src/server/index.js +9 -31
  50. package/src/server/services/agents-service.js +62 -269
  51. package/src/server/services/base/base-channel-service.js +3 -1
  52. package/src/server/services/channels.js +5 -0
  53. package/src/server/services/codex-channels.js +2 -0
  54. package/src/server/services/codex-sessions.js +78 -108
  55. package/src/server/services/commands-service.js +81 -281
  56. package/src/server/services/config-export-service.js +7 -7
  57. package/src/server/services/config-registry-service.js +9 -8
  58. package/src/server/services/config-sync-manager.js +41 -61
  59. package/src/server/services/config-sync-service.js +3 -3
  60. package/src/server/services/gemini-channels.js +17 -6
  61. package/src/server/services/gemini-config.js +4 -3
  62. package/src/server/services/gemini-sessions.js +51 -26
  63. package/src/server/services/gemini-settings-manager.js +3 -2
  64. package/src/server/services/mcp-service.js +14 -9
  65. package/src/server/services/native-oauth-adapters.js +3 -3
  66. package/src/server/services/network-access.js +2 -61
  67. package/src/server/services/notification-hooks.js +19 -184
  68. package/src/server/services/opencode-channels.js +9 -0
  69. package/src/server/services/opencode-sessions.js +6 -16
  70. package/src/server/services/opencode-settings-manager.js +3 -3
  71. package/src/server/services/plugins-service.js +62 -996
  72. package/src/server/services/prompts-service.js +9 -5
  73. package/src/server/services/sessions.js +161 -183
  74. package/src/server/services/skill-service.js +18 -155
  75. package/src/server/websocket-server.js +4 -35
  76. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  77. package/dist/web/assets/Home-qzk118Of.css +0 -1
  78. package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  81. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  84. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  85. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  86. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  87. package/docs/Caddyfile.example +0 -19
  88. package/docs/reverse-proxy-https.md +0 -57
  89. package/src/server/services/https-cert.js +0 -171
  90. package/src/server/services/session-launch-command.js +0 -58
  91. 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, HOME_DIR, getNativePathDir, joinNativeBasePath } = require('../../config/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 = joinNativeBasePath(HOME_DIR, '.claude.json');
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: NATIVE_PATHS.opencode.configJsonc,
25
- json: NATIVE_PATHS.opencode.configJson,
26
- legacy: NATIVE_PATHS.opencode.configLegacy
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(getNativePathDir(filePath));
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(getNativePathDir(filePath));
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(getNativePathDir(filePath));
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, getNativePathDir } = require('../../config/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(getNativePathDir(filePath));
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(getNativePathDir(NATIVE_PATHS.gemini.oauthCredentialsEncrypted));
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 = getRequestHost(req);
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.toLowerCase() === host.toLowerCase();
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, getNativePathDir, joinNativeBasePath } = require('../../config/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 = getNativePathDir(filePath);
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 = {}, browser = {}, claudeEnabled) {
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 joinNativeBasePath(NATIVE_PATHS.opencode.plugins, MANAGED_OPENCODE_PLUGIN_FILE);
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 = {}, browser = {}) {
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 resolveEventType(eventType, payload) {
582
- return eventType || payload?.type || payload?.hook_event?.event_type || ''
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 (normalizedSource === 'codex') {
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 (normalizedSource === 'gemini') {
496
+ if (source === 'gemini') {
596
497
  return 'Gemini CLI 回合已完成 | 等待交互'
597
498
  }
598
499
 
599
- if (normalizedSource === 'opencode') {
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 effectiveEventType = resolveEventType(eventType, payload)
776
- const normalizedMode = mode === 'dialog' ? 'dialog' : 'notification'
777
- const message = resolveMessage(source, effectiveEventType)
778
- const timestamp = Date.now()
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 = {}, browser = getBrowserNotificationConfig()) {
859
+ function writeNotifyScript(feishu = {}) {
1011
860
  ensureParentDir(PATHS.notifyHook);
1012
- fs.writeFileSync(PATHS.notifyHook, generateNotifyScript(feishu, browser), { mode: 0o755 });
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, browser, platforms.claude.enabled);
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, browser);
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 || {}, settings.browser || {});
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, joinNativeBasePath } = require('../../config/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 = joinNativeBasePath(NATIVE_PATHS.opencode.data, 'opencode.db');
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 NATIVE_PATHS?.opencode?.sessions || joinNativeBasePath(getOpenCodeDataDir(), 'storage', 'session');
237
+ return path.join(getOpenCodeDataDir(), 'storage', 'session');
238
238
  }
239
239
 
240
240
  // 兼容导出:保留旧路径函数
241
241
  function getProjectsDir() {
242
- return NATIVE_PATHS?.opencode?.projects || joinNativeBasePath(getOpenCodeDataDir(), 'storage', 'project');
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 { getForkRelations } = require('./sessions');
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
- const { getForkRelations } = require('./sessions');
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: NATIVE_PATHS.opencode.configLegacy,
27
- opencode: NATIVE_PATHS.opencode.configJson,
28
- opencodec: NATIVE_PATHS.opencode.configJsonc
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__';