coding-tool-x 3.4.6 → 3.4.8
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/package.json +1 -1
- package/src/reset-config.js +9 -1
- package/src/server/api/claude-hooks.js +9 -5
- package/src/server/api/proxy.js +21 -23
- package/src/server/services/channels.js +53 -1
- package/src/server/services/notification-hooks.js +20 -7
- package/src/server/services/opencode-channels.js +60 -5
- package/src/server/services/settings-manager.js +9 -1
package/package.json
CHANGED
package/src/reset-config.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const { PATHS, NATIVE_PATHS, ensureStorageDirMigrated } = require('./config/paths');
|
|
3
|
+
const { isWindowsLikePlatform } = require('./utils/home-dir');
|
|
4
|
+
|
|
5
|
+
function buildEchoCommand(value) {
|
|
6
|
+
if (isWindowsLikePlatform(process.platform, process.env)) {
|
|
7
|
+
return `cmd /c echo ${value}`;
|
|
8
|
+
}
|
|
9
|
+
return `echo '${value}'`;
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
// 恢复配置到默认状态
|
|
5
13
|
async function resetConfig() {
|
|
@@ -56,7 +64,7 @@ async function resetConfig() {
|
|
|
56
64
|
if (!settings.env) settings.env = {};
|
|
57
65
|
settings.env.ANTHROPIC_BASE_URL = activeChannel.baseUrl;
|
|
58
66
|
settings.env.ANTHROPIC_API_KEY = activeChannel.apiKey;
|
|
59
|
-
settings.apiKeyHelper =
|
|
67
|
+
settings.apiKeyHelper = buildEchoCommand(activeChannel.apiKey);
|
|
60
68
|
|
|
61
69
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
62
70
|
console.log(`[OK] 已恢复到渠道: ${activeChannel.name}`);
|
|
@@ -26,6 +26,10 @@ const UI_CONFIG_PATH = PATHS.uiConfig;
|
|
|
26
26
|
// 通知脚本路径(用于飞书通知)
|
|
27
27
|
const NOTIFY_SCRIPT_PATH = PATHS.notifyHook;
|
|
28
28
|
|
|
29
|
+
function buildWindowsPopupCommand() {
|
|
30
|
+
return `powershell -NoProfile -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40)"`;
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
// 读取 Claude settings.json
|
|
30
34
|
function readClaudeSettings() {
|
|
31
35
|
try {
|
|
@@ -84,8 +88,8 @@ function writeUIConfig(config) {
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
// 生成系统通知命令(跨平台)
|
|
87
|
-
function generateSystemNotificationCommand(type) {
|
|
88
|
-
if (
|
|
91
|
+
function generateSystemNotificationCommand(type, platformOverride = platform) {
|
|
92
|
+
if (platformOverride === 'darwin') {
|
|
89
93
|
// macOS
|
|
90
94
|
if (type === 'dialog') {
|
|
91
95
|
return `osascript -e 'display dialog "Claude Code 任务已完成 | 等待交互" with title "Coding Tool" buttons {"好的"} default button 1 with icon note'`;
|
|
@@ -94,12 +98,12 @@ function generateSystemNotificationCommand(type) {
|
|
|
94
98
|
// terminal-notifier 需要 brew install terminal-notifier
|
|
95
99
|
return `if command -v terminal-notifier &>/dev/null; then terminal-notifier -title "Coding Tool" -message "任务已完成 | 等待交互" -sound Glass -activate com.apple.Terminal; else osascript -e 'display notification "任务已完成 | 等待交互" with title "Coding Tool" sound name "Glass"'; fi`;
|
|
96
100
|
}
|
|
97
|
-
} else if (
|
|
101
|
+
} else if (platformOverride === 'win32') {
|
|
98
102
|
// Windows
|
|
99
103
|
if (type === 'dialog') {
|
|
100
|
-
return `powershell -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('Claude Code 任务已完成 | 等待交互', 'Coding Tool', 'OK', 'Information')"`;
|
|
104
|
+
return `powershell -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('Claude Code 任务已完成 | 等待交互', 'Coding Tool', 'OK', 'Information')" || ${buildWindowsPopupCommand()}`;
|
|
101
105
|
} else {
|
|
102
|
-
return `powershell -NoProfile -Command "try { [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null; $xml = New-Object Windows.Data.Xml.Dom.XmlDocument; $xml.LoadXml('<toast><visual><binding template=\\"ToastGeneric\\"><text>Coding Tool</text><text>任务已完成 | 等待交互</text></binding></visual><audio src=\\"ms-winsoundevent:Notification.Default\\"/></toast>'); $toast = [Windows.UI.Notifications.ToastNotification]::new($xml); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Coding Tool').Show($toast) } catch { $wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40) }"`;
|
|
106
|
+
return `powershell -NoProfile -Command "try { [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null; $xml = New-Object Windows.Data.Xml.Dom.XmlDocument; $xml.LoadXml('<toast><visual><binding template=\\"ToastGeneric\\"><text>Coding Tool</text><text>任务已完成 | 等待交互</text></binding></visual><audio src=\\"ms-winsoundevent:Notification.Default\\"/></toast>'); $toast = [Windows.UI.Notifications.ToastNotification]::new($xml); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Coding Tool').Show($toast) } catch { $wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('任务已完成 | 等待交互', 5, 'Coding Tool', 0x40) }" || ${buildWindowsPopupCommand()}`;
|
|
103
107
|
}
|
|
104
108
|
} else {
|
|
105
109
|
// Linux
|
package/src/server/api/proxy.js
CHANGED
|
@@ -107,34 +107,32 @@ function findActiveChannelFromSettings() {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
if (!baseUrl || !apiKey || baseUrl.includes('127.0.0.1')) {
|
|
111
|
-
console.log('[Proxy] Invalid settings: empty baseUrl/apiKey or localhost detected');
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
110
|
const channels = getAllChannels();
|
|
116
111
|
|
|
117
112
|
// Level 1: Exact match (baseUrl + apiKey)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
if (baseUrl && apiKey && !baseUrl.includes('127.0.0.1')) {
|
|
114
|
+
let matchingChannel = channels.find(ch =>
|
|
115
|
+
ch.baseUrl === baseUrl && ch.apiKey === apiKey
|
|
116
|
+
);
|
|
117
|
+
if (matchingChannel) {
|
|
118
|
+
console.log(`[Proxy] Level 1 - Exact match: ${matchingChannel.name}`);
|
|
119
|
+
return matchingChannel;
|
|
120
|
+
}
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
// Level 2: Match by baseUrl only (when apiKey differs)
|
|
123
|
+
matchingChannel = channels.find(ch => ch.baseUrl === baseUrl);
|
|
124
|
+
if (matchingChannel) {
|
|
125
|
+
console.log(`[Proxy] Level 2 - Matched by baseUrl only: ${matchingChannel.name}`);
|
|
126
|
+
return matchingChannel;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
console.log('[Proxy] settings.json has no valid baseUrl/apiKey, falling back to channel list');
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
// Level 3: Use active-channel.json for last known active channel
|
|
135
133
|
const activeChannelId = loadActiveChannelId();
|
|
136
134
|
if (activeChannelId) {
|
|
137
|
-
matchingChannel = channels.find(ch => ch.id === activeChannelId);
|
|
135
|
+
const matchingChannel = channels.find(ch => ch.id === activeChannelId);
|
|
138
136
|
if (matchingChannel) {
|
|
139
137
|
console.log(`[Proxy] Level 3 - Using last active channel: ${matchingChannel.name}`);
|
|
140
138
|
return matchingChannel;
|
|
@@ -142,10 +140,10 @@ function findActiveChannelFromSettings() {
|
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
// Level 4: Return first enabled channel as last resort
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
console.log(`[Proxy] Level 4 - Using first enabled channel: ${
|
|
148
|
-
return
|
|
143
|
+
const fallbackChannel = channels.find(ch => ch.enabled !== false);
|
|
144
|
+
if (fallbackChannel) {
|
|
145
|
+
console.log(`[Proxy] Level 4 - Using first enabled channel: ${fallbackChannel.name}`);
|
|
146
|
+
return fallbackChannel;
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
console.log('[Proxy] No matching channel found after all fallback levels');
|
|
@@ -4,6 +4,7 @@ const BaseChannelService = require('./base/base-channel-service');
|
|
|
4
4
|
const { isProxyConfig } = require('./settings-manager');
|
|
5
5
|
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
6
6
|
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
7
|
+
const { isWindowsLikePlatform } = require('../../utils/home-dir');
|
|
7
8
|
|
|
8
9
|
// ── Claude 特有工具函数 ──
|
|
9
10
|
|
|
@@ -51,7 +52,10 @@ function extractApiKeyFromHelper(apiKeyHelper) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
function buildApiKeyHelperCommand() {
|
|
54
|
-
|
|
55
|
+
if (isWindowsLikePlatform(process.platform, process.env)) {
|
|
56
|
+
return 'cmd /c echo ctx-managed';
|
|
57
|
+
}
|
|
58
|
+
return "echo 'ctx-managed'";
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
// ── Claude 原生设置写入 ──
|
|
@@ -59,6 +63,7 @@ function buildApiKeyHelperCommand() {
|
|
|
59
63
|
function updateClaudeSettingsWithModelConfig(channel) {
|
|
60
64
|
clearNativeOAuth('claude');
|
|
61
65
|
const settingsPath = getClaudeSettingsPath();
|
|
66
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
62
67
|
|
|
63
68
|
let settings = {};
|
|
64
69
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -105,12 +110,17 @@ function updateClaudeSettingsWithModelConfig(channel) {
|
|
|
105
110
|
delete settings.env.NO_PROXY;
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
if (settings.env && Object.keys(settings.env).length === 0) {
|
|
114
|
+
delete settings.env;
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
settings.apiKeyHelper = buildApiKeyHelperCommand();
|
|
109
118
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
function updateClaudeSettings(baseUrl, apiKey) {
|
|
113
122
|
const settingsPath = getClaudeSettingsPath();
|
|
123
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
114
124
|
|
|
115
125
|
let settings = {};
|
|
116
126
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -133,10 +143,19 @@ function updateClaudeSettings(baseUrl, apiKey) {
|
|
|
133
143
|
settings.env.ANTHROPIC_API_KEY = apiKey;
|
|
134
144
|
}
|
|
135
145
|
|
|
146
|
+
if (settings.env && Object.keys(settings.env).length === 0) {
|
|
147
|
+
delete settings.env;
|
|
148
|
+
}
|
|
149
|
+
|
|
136
150
|
settings.apiKeyHelper = buildApiKeyHelperCommand();
|
|
137
151
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
138
152
|
}
|
|
139
153
|
|
|
154
|
+
function resolveCurrentManagedChannel(channels = []) {
|
|
155
|
+
const allChannels = Array.isArray(channels) ? channels : [];
|
|
156
|
+
return allChannels.find(ch => ch.enabled !== false) || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
140
159
|
// ── ClaudeChannelService ──
|
|
141
160
|
|
|
142
161
|
class ClaudeChannelService extends BaseChannelService {
|
|
@@ -185,6 +204,39 @@ class ClaudeChannelService extends BaseChannelService {
|
|
|
185
204
|
this._cacheInitialized = true;
|
|
186
205
|
}
|
|
187
206
|
|
|
207
|
+
_onAfterCreate(channel, _allChannels) {
|
|
208
|
+
if (!isProxyConfig() && channel.enabled !== false) {
|
|
209
|
+
this._applyToNativeSettings(channel);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_onAfterUpdate(oldChannel, nextChannel, allChannels) {
|
|
214
|
+
if (isProxyConfig()) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (oldChannel.enabled === false && nextChannel.enabled !== false) {
|
|
219
|
+
this._applyToNativeSettings(nextChannel);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const activeChannel = resolveCurrentManagedChannel(allChannels);
|
|
224
|
+
if (nextChannel.enabled !== false && activeChannel?.id === nextChannel.id) {
|
|
225
|
+
this._applyToNativeSettings(nextChannel);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_onAfterDelete(_channel, allChannels) {
|
|
230
|
+
if (isProxyConfig()) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const activeChannel = resolveCurrentManagedChannel(allChannels);
|
|
235
|
+
if (activeChannel) {
|
|
236
|
+
this._applyToNativeSettings(activeChannel);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
188
240
|
_applyToNativeSettings(channel) {
|
|
189
241
|
updateClaudeSettingsWithModelConfig(channel);
|
|
190
242
|
}
|
|
@@ -237,6 +237,10 @@ function escapeForXml(value) {
|
|
|
237
237
|
.replace(/'/g, ''');
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
function buildWindowsPopupCommand(title, message) {
|
|
241
|
+
return `powershell -NoProfile -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('${escapeForPowerShellSingleQuote(message)}', 5, '${escapeForPowerShellSingleQuote(title)}', 0x40)"`;
|
|
242
|
+
}
|
|
243
|
+
|
|
240
244
|
function generateNotifyScript(feishu = {}) {
|
|
241
245
|
const feishuEnabled = feishu.enabled === true && !!feishu.webhookUrl;
|
|
242
246
|
|
|
@@ -336,11 +340,13 @@ function notify(mode, message) {
|
|
|
336
340
|
}
|
|
337
341
|
|
|
338
342
|
if (platform === 'win32') {
|
|
343
|
+
const popupCommand = buildWindowsPopupCommand(title, message)
|
|
339
344
|
if (mode === 'dialog') {
|
|
340
345
|
const ps = "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('" +
|
|
341
346
|
escapeForPowerShellSingleQuote(message) + "', '" +
|
|
342
347
|
escapeForPowerShellSingleQuote(title) + "', 'OK', 'Information')"
|
|
343
|
-
|
|
348
|
+
const command = 'powershell -NoProfile -Command ' + JSON.stringify(ps) + ' || ' + popupCommand
|
|
349
|
+
execSync(command, { stdio: 'ignore', windowsHide: true })
|
|
344
350
|
} else {
|
|
345
351
|
const toastXml = '<toast><visual><binding template="ToastGeneric"><text>' +
|
|
346
352
|
escapeForXml(title) + '</text><text>' + escapeForXml(message) +
|
|
@@ -354,7 +360,8 @@ function notify(mode, message) {
|
|
|
354
360
|
"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Coding Tool').Show($toast) " +
|
|
355
361
|
"} catch { $wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('" +
|
|
356
362
|
escapeForPowerShellSingleQuote(message) + "', 5, '" + escapeForPowerShellSingleQuote(title) + "', 0x40) }"
|
|
357
|
-
|
|
363
|
+
const command = 'powershell -NoProfile -Command ' + JSON.stringify(ps) + ' || ' + popupCommand
|
|
364
|
+
execSync(command, { stdio: 'ignore', windowsHide: true })
|
|
358
365
|
}
|
|
359
366
|
return
|
|
360
367
|
}
|
|
@@ -468,6 +475,10 @@ function escapeForXml(value) {
|
|
|
468
475
|
.replace(/"/g, '"')
|
|
469
476
|
.replace(/'/g, ''')
|
|
470
477
|
}
|
|
478
|
+
|
|
479
|
+
function buildWindowsPopupCommand(title, message) {
|
|
480
|
+
return \`powershell -NoProfile -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('\${escapeForPowerShellSingleQuote(message)}', 5, '\${escapeForPowerShellSingleQuote(title)}', 0x40)"\`
|
|
481
|
+
}
|
|
471
482
|
`;
|
|
472
483
|
}
|
|
473
484
|
|
|
@@ -884,10 +895,10 @@ function sendFeishuTest(webhookUrl) {
|
|
|
884
895
|
});
|
|
885
896
|
}
|
|
886
897
|
|
|
887
|
-
function generateSystemNotificationCommand(type, message) {
|
|
898
|
+
function generateSystemNotificationCommand(type, message, platformOverride = os.platform()) {
|
|
888
899
|
const normalizedType = normalizeType(type);
|
|
889
900
|
const title = 'Coding Tool';
|
|
890
|
-
const platform =
|
|
901
|
+
const platform = platformOverride;
|
|
891
902
|
|
|
892
903
|
if (platform === 'darwin') {
|
|
893
904
|
if (normalizedType === 'dialog') {
|
|
@@ -897,12 +908,13 @@ function generateSystemNotificationCommand(type, message) {
|
|
|
897
908
|
}
|
|
898
909
|
|
|
899
910
|
if (platform === 'win32') {
|
|
911
|
+
const popupCommand = buildWindowsPopupCommand(title, message);
|
|
900
912
|
if (normalizedType === 'dialog') {
|
|
901
|
-
return `powershell -NoProfile -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('${escapeForPowerShellSingleQuote(message)}', '${escapeForPowerShellSingleQuote(title)}', 'OK', 'Information')"`;
|
|
913
|
+
return `powershell -NoProfile -Command "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('${escapeForPowerShellSingleQuote(message)}', '${escapeForPowerShellSingleQuote(title)}', 'OK', 'Information')" || ${popupCommand}`;
|
|
902
914
|
}
|
|
903
915
|
|
|
904
916
|
const toastXml = `<toast><visual><binding template="ToastGeneric"><text>${escapeForXml(title)}</text><text>${escapeForXml(message)}</text></binding></visual><audio src="ms-winsoundevent:Notification.Default"/></toast>`;
|
|
905
|
-
return `powershell -NoProfile -Command "try { [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null; $xml = New-Object Windows.Data.Xml.Dom.XmlDocument; $xml.LoadXml('${toastXml.replace(/'/g, "''")}'); $toast = [Windows.UI.Notifications.ToastNotification]::new($xml); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Coding Tool').Show($toast) } catch { $wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('${escapeForPowerShellSingleQuote(message)}', 5, '${escapeForPowerShellSingleQuote(title)}', 0x40) }"`;
|
|
917
|
+
return `powershell -NoProfile -Command "try { [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null; $xml = New-Object Windows.Data.Xml.Dom.XmlDocument; $xml.LoadXml('${toastXml.replace(/'/g, "''")}'); $toast = [Windows.UI.Notifications.ToastNotification]::new($xml); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Coding Tool').Show($toast) } catch { $wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('${escapeForPowerShellSingleQuote(message)}', 5, '${escapeForPowerShellSingleQuote(title)}', 0x40) }" || ${popupCommand}`;
|
|
906
918
|
}
|
|
907
919
|
|
|
908
920
|
if (normalizedType === 'dialog') {
|
|
@@ -947,6 +959,7 @@ module.exports = {
|
|
|
947
959
|
buildClaudeCommand,
|
|
948
960
|
buildOpenCodePluginContent,
|
|
949
961
|
getOpenCodeManagedPluginPath,
|
|
950
|
-
generateNotifyScript
|
|
962
|
+
generateNotifyScript,
|
|
963
|
+
generateSystemNotificationCommand
|
|
951
964
|
}
|
|
952
965
|
};
|
|
@@ -2,7 +2,10 @@ 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 {
|
|
5
|
+
const {
|
|
6
|
+
setChannelConfig,
|
|
7
|
+
clearManagedChannelConfig
|
|
8
|
+
} = require('./opencode-settings-manager');
|
|
6
9
|
const { normalizeGatewaySourceType } = require('./base/proxy-utils');
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -98,6 +101,30 @@ function deriveProviderKey(channel) {
|
|
|
98
101
|
return `opencode_${base}`;
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
function getOpenCodeProxyRunning() {
|
|
105
|
+
const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
|
|
106
|
+
return Boolean(getOpenCodeProxyStatus()?.running);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveCurrentManagedChannel(channels = []) {
|
|
110
|
+
const allChannels = Array.isArray(channels) ? channels : [];
|
|
111
|
+
return allChannels.find(channel => channel.enabled !== false) || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function syncManagedChannelConfig(channels = [], preferredChannel = null) {
|
|
115
|
+
const targetChannel = preferredChannel && preferredChannel.enabled !== false
|
|
116
|
+
? preferredChannel
|
|
117
|
+
: resolveCurrentManagedChannel(channels);
|
|
118
|
+
|
|
119
|
+
if (targetChannel) {
|
|
120
|
+
setChannelConfig(targetChannel);
|
|
121
|
+
return targetChannel;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
clearManagedChannelConfig();
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
// 保存渠道数据
|
|
102
129
|
function saveChannels(data) {
|
|
103
130
|
const filePath = getChannelsFilePath();
|
|
@@ -115,6 +142,7 @@ function getChannels() {
|
|
|
115
142
|
// 添加渠道
|
|
116
143
|
function createChannel(name, baseUrl, apiKey, extraConfig = {}) {
|
|
117
144
|
const data = loadChannels();
|
|
145
|
+
const isProxyRunning = getOpenCodeProxyRunning();
|
|
118
146
|
|
|
119
147
|
const newChannel = {
|
|
120
148
|
id: crypto.randomUUID(),
|
|
@@ -139,8 +167,22 @@ function createChannel(name, baseUrl, apiKey, extraConfig = {}) {
|
|
|
139
167
|
newChannel.providerKey = extraConfig.providerKey || deriveProviderKey(newChannel);
|
|
140
168
|
|
|
141
169
|
data.channels.push(newChannel);
|
|
170
|
+
|
|
171
|
+
if (!isProxyRunning && newChannel.enabled !== false) {
|
|
172
|
+
data.channels.forEach((channel, index) => {
|
|
173
|
+
if (index !== data.channels.length - 1 && channel.enabled) {
|
|
174
|
+
channel.enabled = false;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
console.log(`[OpenCode Single-channel mode] Enabled "${newChannel.name}", disabled all others`);
|
|
178
|
+
}
|
|
179
|
+
|
|
142
180
|
saveChannels(data);
|
|
143
181
|
|
|
182
|
+
if (!isProxyRunning && newChannel.enabled !== false) {
|
|
183
|
+
syncManagedChannelConfig(data.channels, newChannel);
|
|
184
|
+
}
|
|
185
|
+
|
|
144
186
|
return newChannel;
|
|
145
187
|
}
|
|
146
188
|
|
|
@@ -173,10 +215,7 @@ function updateChannel(channelId, updates) {
|
|
|
173
215
|
merged.providerKey = updates.providerKey || deriveProviderKey(merged);
|
|
174
216
|
data.channels[index] = merged;
|
|
175
217
|
|
|
176
|
-
|
|
177
|
-
const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
|
|
178
|
-
const proxyStatus = getOpenCodeProxyStatus();
|
|
179
|
-
const isProxyRunning = proxyStatus.running;
|
|
218
|
+
const isProxyRunning = getOpenCodeProxyRunning();
|
|
180
219
|
|
|
181
220
|
// Single-channel enforcement when proxy is OFF: enabling a channel disables all others
|
|
182
221
|
if (!isProxyRunning && merged.enabled && !oldChannel.enabled) {
|
|
@@ -189,6 +228,18 @@ function updateChannel(channelId, updates) {
|
|
|
189
228
|
}
|
|
190
229
|
|
|
191
230
|
saveChannels(data);
|
|
231
|
+
|
|
232
|
+
if (!isProxyRunning) {
|
|
233
|
+
if (oldChannel.enabled === false && merged.enabled !== false) {
|
|
234
|
+
syncManagedChannelConfig(data.channels, merged);
|
|
235
|
+
} else {
|
|
236
|
+
const activeChannel = resolveCurrentManagedChannel(data.channels);
|
|
237
|
+
if (merged.enabled !== false && activeChannel?.id === merged.id) {
|
|
238
|
+
syncManagedChannelConfig(data.channels, merged);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
192
243
|
return data.channels[index];
|
|
193
244
|
}
|
|
194
245
|
|
|
@@ -204,6 +255,10 @@ async function deleteChannel(channelId) {
|
|
|
204
255
|
data.channels.splice(index, 1);
|
|
205
256
|
saveChannels(data);
|
|
206
257
|
|
|
258
|
+
if (!getOpenCodeProxyRunning()) {
|
|
259
|
+
syncManagedChannelConfig(data.channels);
|
|
260
|
+
}
|
|
261
|
+
|
|
207
262
|
return { success: true };
|
|
208
263
|
}
|
|
209
264
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
3
|
+
const { isWindowsLikePlatform } = require('../../utils/home-dir');
|
|
4
|
+
|
|
5
|
+
function buildEchoCommand(value) {
|
|
6
|
+
if (isWindowsLikePlatform(process.platform, process.env)) {
|
|
7
|
+
return `cmd /c echo ${value}`;
|
|
8
|
+
}
|
|
9
|
+
return `echo '${value}'`;
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
// Claude Code 配置文件路径
|
|
5
13
|
function getSettingsPath() {
|
|
@@ -113,7 +121,7 @@ function setProxyConfig(proxyPort) {
|
|
|
113
121
|
// 修改为代理配置(使用 Claude Code 的标准格式)
|
|
114
122
|
settings.env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${proxyPort}`;
|
|
115
123
|
settings.env.ANTHROPIC_API_KEY = 'PROXY_KEY';
|
|
116
|
-
settings.apiKeyHelper =
|
|
124
|
+
settings.apiKeyHelper = buildEchoCommand('PROXY_KEY');
|
|
117
125
|
|
|
118
126
|
// 写入
|
|
119
127
|
writeSettings(settings);
|