coding-tool-x 3.3.6 → 3.3.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/CHANGELOG.md +14 -0
- package/dist/web/assets/{Analytics-TtaduRqL.js → Analytics-DLpoDZ2M.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BP2lLBMN.js → ConfigTemplates-D_hRb55W.js} +1 -1
- package/dist/web/assets/Home-BMoFdAwy.css +1 -0
- package/dist/web/assets/Home-DNwp-0J-.js +1 -0
- package/dist/web/assets/{PluginManager-HmISlyMK.js → PluginManager-JXsyym1s.js} +1 -1
- package/dist/web/assets/{ProjectList-DoN8Hjbu.js → ProjectList-DZWSeb-q.js} +1 -1
- package/dist/web/assets/{SessionList-Da8BYzNi.js → SessionList-Cs624DR3.js} +1 -1
- package/dist/web/assets/{SkillManager-DqLAXh9o.js → SkillManager-bEliz7qz.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-B_TxOgPW.js → WorkspaceManager-J3RecFGn.js} +1 -1
- package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
- package/dist/web/assets/index-BXeSvAwU.js +2 -0
- package/dist/web/assets/index-DWAC3Tdv.css +1 -0
- package/dist/web/index.html +3 -3
- package/package.json +3 -2
- package/src/commands/daemon.js +44 -6
- package/src/commands/toggle-proxy.js +100 -5
- package/src/config/default.js +1 -1
- package/src/config/model-metadata.js +2 -2
- package/src/config/model-metadata.json +7 -2
- package/src/config/paths.js +102 -19
- package/src/server/api/channels.js +9 -0
- package/src/server/api/codex-channels.js +9 -0
- package/src/server/api/codex-proxy.js +22 -11
- package/src/server/api/gemini-proxy.js +22 -11
- package/src/server/api/mcp.js +26 -4
- package/src/server/api/oauth-credentials.js +163 -0
- package/src/server/api/opencode-proxy.js +22 -10
- package/src/server/api/plugins.js +3 -1
- package/src/server/api/proxy.js +39 -44
- package/src/server/api/skills.js +91 -13
- package/src/server/codex-proxy-server.js +1 -11
- package/src/server/index.js +26 -2
- package/src/server/services/channels.js +18 -22
- package/src/server/services/codex-channels.js +124 -175
- package/src/server/services/codex-config.js +2 -5
- package/src/server/services/codex-settings-manager.js +12 -348
- package/src/server/services/config-export-service.js +572 -117
- package/src/server/services/gemini-channels.js +11 -9
- package/src/server/services/mcp-client.js +70 -13
- package/src/server/services/mcp-service.js +74 -29
- package/src/server/services/model-detector.js +1 -0
- package/src/server/services/native-keychain.js +243 -0
- package/src/server/services/native-oauth-adapters.js +890 -0
- package/src/server/services/oauth-credentials-service.js +786 -0
- package/src/server/services/oauth-utils.js +49 -0
- package/src/server/services/opencode-channels.js +13 -9
- package/src/server/services/opencode-settings-manager.js +169 -16
- package/src/server/services/plugins-service.js +22 -1
- package/src/server/services/settings-manager.js +13 -0
- package/src/server/services/skill-service.js +712 -332
- package/src/utils/port-helper.js +87 -2
- package/dist/web/assets/Home-BsSioaaB.css +0 -1
- package/dist/web/assets/Home-CbbyopS-.js +0 -1
- package/dist/web/assets/index-By3mDEvx.js +0 -2
- package/dist/web/assets/index-CsWInMQV.css +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"lastUpdated": "2026-
|
|
2
|
+
"lastUpdated": "2026-03-06",
|
|
3
3
|
"defaultModels": {
|
|
4
4
|
"claude": [
|
|
5
5
|
"claude-opus-4-6",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"claude-haiku-4-5-20251001"
|
|
10
10
|
],
|
|
11
11
|
"codex": [
|
|
12
|
+
"gpt-5.4",
|
|
12
13
|
"gpt-5.3-codex",
|
|
13
14
|
"gpt-5.2-codex",
|
|
14
15
|
"gpt-5.1-codex-max",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
},
|
|
31
32
|
"defaultSpeedTestModels": {
|
|
32
33
|
"claude": "claude-haiku-4-5",
|
|
33
|
-
"codex": "gpt-5.
|
|
34
|
+
"codex": "gpt-5.4",
|
|
34
35
|
"gemini": "gemini-2.5-pro"
|
|
35
36
|
},
|
|
36
37
|
"aliases": {
|
|
@@ -73,6 +74,10 @@
|
|
|
73
74
|
"limit": { "context": 1000000, "output": 32768 },
|
|
74
75
|
"pricing": { "input": 2, "output": 8 }
|
|
75
76
|
},
|
|
77
|
+
"gpt-5.4": {
|
|
78
|
+
"limit": { "context": 1000000, "output": 32768 },
|
|
79
|
+
"pricing": { "input": 2, "output": 8 }
|
|
80
|
+
},
|
|
76
81
|
"gpt-5-codex": {
|
|
77
82
|
"limit": { "context": 1000000, "output": 32768 },
|
|
78
83
|
"pricing": { "input": 2, "output": 8 }
|
package/src/config/paths.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
-
const { resolvePreferredHomeDir } = require('../utils/home-dir');
|
|
6
|
+
const { resolvePreferredHomeDir, isWindowsLikePlatform } = require('../utils/home-dir');
|
|
7
7
|
|
|
8
8
|
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
9
9
|
|
|
@@ -63,6 +63,71 @@ function ensureStorageDirMigrated() {
|
|
|
63
63
|
return CC_TOOL_BASE_DIR;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function resolveExistingEnvPath(envValue) {
|
|
67
|
+
if (typeof envValue !== 'string') {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
const trimmed = envValue.trim();
|
|
71
|
+
return trimmed || '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pickExistingDir(candidates, fallback) {
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
if (candidate && fs.existsSync(candidate)) {
|
|
77
|
+
return candidate;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getClaudeConfigDir() {
|
|
84
|
+
return resolveExistingEnvPath(process.env.CLAUDE_CONFIG_DIR) || path.join(HOME_DIR, '.claude');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getCodexDir() {
|
|
88
|
+
return resolveExistingEnvPath(process.env.CODEX_HOME) || path.join(HOME_DIR, '.codex');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getGeminiDir() {
|
|
92
|
+
return path.join(HOME_DIR, '.gemini');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getOpenCodeDataDir() {
|
|
96
|
+
if (isWindowsLikePlatform(process.platform, process.env)) {
|
|
97
|
+
const localAppData = resolveExistingEnvPath(process.env.LOCALAPPDATA);
|
|
98
|
+
return path.join(localAppData || path.join(HOME_DIR, 'AppData', 'Local'), 'opencode');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const xdgDataHome = resolveExistingEnvPath(process.env.XDG_DATA_HOME);
|
|
102
|
+
const preferredDir = path.join(xdgDataHome || path.join(HOME_DIR, '.local', 'share'), 'opencode');
|
|
103
|
+
|
|
104
|
+
if (process.platform === 'darwin') {
|
|
105
|
+
const legacyDarwinDir = path.join(HOME_DIR, 'Library', 'Application Support', 'opencode');
|
|
106
|
+
return pickExistingDir([preferredDir, legacyDarwinDir], preferredDir);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return preferredDir;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getOpenCodeConfigDir() {
|
|
113
|
+
if (isWindowsLikePlatform(process.platform, process.env)) {
|
|
114
|
+
const appData = resolveExistingEnvPath(process.env.APPDATA);
|
|
115
|
+
return path.join(appData || path.join(HOME_DIR, 'AppData', 'Roaming'), 'opencode');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const xdgConfigHome = resolveExistingEnvPath(process.env.XDG_CONFIG_HOME);
|
|
119
|
+
const preferredDir = path.join(xdgConfigHome || path.join(HOME_DIR, '.config'), 'opencode');
|
|
120
|
+
|
|
121
|
+
if (process.platform === 'darwin') {
|
|
122
|
+
const xdgDataHome = resolveExistingEnvPath(process.env.XDG_DATA_HOME);
|
|
123
|
+
const legacyDataDir = path.join(xdgDataHome || path.join(HOME_DIR, '.local', 'share'), 'opencode');
|
|
124
|
+
const legacyDarwinDir = path.join(HOME_DIR, 'Library', 'Application Support', 'opencode');
|
|
125
|
+
return pickExistingDir([preferredDir, legacyDarwinDir, legacyDataDir], preferredDir);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return preferredDir;
|
|
129
|
+
}
|
|
130
|
+
|
|
66
131
|
// 路径配置
|
|
67
132
|
const PATHS = {
|
|
68
133
|
// 基础目录
|
|
@@ -126,6 +191,9 @@ const PATHS = {
|
|
|
126
191
|
// UI 配置
|
|
127
192
|
uiConfig: path.join(CC_TOOL_BASE_DIR, 'ui-config.json'),
|
|
128
193
|
|
|
194
|
+
// OAuth 凭证注册表
|
|
195
|
+
oauthCredentials: path.join(CC_TOOL_BASE_DIR, 'oauth-credentials.json'),
|
|
196
|
+
|
|
129
197
|
// 飞书通知脚本
|
|
130
198
|
notifyHook: path.join(CC_TOOL_BASE_DIR, 'notify-hook.js'),
|
|
131
199
|
|
|
@@ -151,35 +219,45 @@ const PATHS = {
|
|
|
151
219
|
const NATIVE_PATHS = {
|
|
152
220
|
// Claude Code 原生配置
|
|
153
221
|
claude: {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
222
|
+
dir: getClaudeConfigDir(),
|
|
223
|
+
settings: path.join(getClaudeConfigDir(), 'settings.json'),
|
|
224
|
+
settingsBackup: path.join(getClaudeConfigDir(), 'settings.json.cc-tool-backup'),
|
|
225
|
+
projects: path.join(getClaudeConfigDir(), 'projects'),
|
|
226
|
+
credentials: path.join(getClaudeConfigDir(), '.credentials.json')
|
|
157
227
|
},
|
|
158
228
|
|
|
159
229
|
// Codex 原生配置
|
|
160
230
|
codex: {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
231
|
+
dir: getCodexDir(),
|
|
232
|
+
config: path.join(getCodexDir(), 'config.toml'),
|
|
233
|
+
configBackup: path.join(getCodexDir(), 'config.toml.cc-tool-backup'),
|
|
234
|
+
auth: path.join(getCodexDir(), 'auth.json'),
|
|
235
|
+
authBackup: path.join(getCodexDir(), 'auth.json.cc-tool-backup'),
|
|
236
|
+
sessions: path.join(getCodexDir(), 'sessions')
|
|
166
237
|
},
|
|
167
238
|
|
|
168
239
|
// Gemini 原生配置
|
|
169
240
|
gemini: {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
241
|
+
dir: getGeminiDir(),
|
|
242
|
+
env: path.join(getGeminiDir(), '.env'),
|
|
243
|
+
envBackup: path.join(getGeminiDir(), '.env.cc-tool-backup'),
|
|
244
|
+
tmp: path.join(getGeminiDir(), 'tmp'),
|
|
245
|
+
settings: path.join(getGeminiDir(), 'settings.json'),
|
|
246
|
+
settingsBackup: path.join(getGeminiDir(), 'settings.json.cc-tool-backup'),
|
|
247
|
+
googleAccounts: path.join(getGeminiDir(), 'google_accounts.json'),
|
|
248
|
+
oauthCredentialsLegacy: path.join(getGeminiDir(), 'oauth_creds.json'),
|
|
249
|
+
oauthCredentialsEncrypted: path.join(getGeminiDir(), 'mcp-oauth-tokens-v2.json')
|
|
173
250
|
},
|
|
174
251
|
|
|
175
252
|
// OpenCode 原生配置
|
|
176
253
|
opencode: {
|
|
177
|
-
data:
|
|
178
|
-
config:
|
|
179
|
-
sessions: path.join(
|
|
180
|
-
projects: path.join(
|
|
181
|
-
messages: path.join(
|
|
182
|
-
log: path.join(
|
|
254
|
+
data: getOpenCodeDataDir(),
|
|
255
|
+
config: getOpenCodeConfigDir(),
|
|
256
|
+
sessions: path.join(getOpenCodeDataDir(), 'storage', 'session'),
|
|
257
|
+
projects: path.join(getOpenCodeDataDir(), 'storage', 'project'),
|
|
258
|
+
messages: path.join(getOpenCodeDataDir(), 'storage', 'message'),
|
|
259
|
+
log: path.join(getOpenCodeDataDir(), 'log'),
|
|
260
|
+
auth: path.join(getOpenCodeDataDir(), 'auth.json')
|
|
183
261
|
}
|
|
184
262
|
};
|
|
185
263
|
|
|
@@ -190,5 +268,10 @@ module.exports = {
|
|
|
190
268
|
CTX_BASE_DIR,
|
|
191
269
|
CC_TOOL_BASE_DIR,
|
|
192
270
|
LEGACY_BASE_DIRS,
|
|
193
|
-
ensureStorageDirMigrated
|
|
271
|
+
ensureStorageDirMigrated,
|
|
272
|
+
getClaudeConfigDir,
|
|
273
|
+
getCodexDir,
|
|
274
|
+
getGeminiDir,
|
|
275
|
+
getOpenCodeDataDir,
|
|
276
|
+
getOpenCodeConfigDir
|
|
194
277
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const {
|
|
4
5
|
getAllChannels,
|
|
5
6
|
applyChannelToSettings,
|
|
@@ -18,6 +19,8 @@ const {
|
|
|
18
19
|
sanitizeBatchConcurrency,
|
|
19
20
|
runWithConcurrencyLimit
|
|
20
21
|
} = require('../services/speed-test');
|
|
22
|
+
const { PATHS } = require('../../config/paths');
|
|
23
|
+
const { deleteBackup } = require('../services/settings-manager');
|
|
21
24
|
const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
|
|
22
25
|
const { broadcastLog, broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
|
|
23
26
|
const { clearRedirectCache } = require('../proxy-server');
|
|
@@ -185,6 +188,12 @@ router.post('/:id/apply-to-settings', async (req, res) => {
|
|
|
185
188
|
// Stop proxy and restore backup
|
|
186
189
|
const { stopProxyServer } = require('../proxy-server');
|
|
187
190
|
await stopProxyServer({ clearStartTime: false });
|
|
191
|
+
deleteBackup();
|
|
192
|
+
try {
|
|
193
|
+
fs.unlinkSync(PATHS.activeChannel.claude);
|
|
194
|
+
} catch {
|
|
195
|
+
// ignore missing active channel marker
|
|
196
|
+
}
|
|
188
197
|
|
|
189
198
|
// Re-apply channel settings after proxy stop to prevent race condition
|
|
190
199
|
// (stopProxyServer restores backup, then we overwrite it with current channel)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const router = express.Router();
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const {
|
|
4
5
|
getChannels,
|
|
5
6
|
createChannel,
|
|
@@ -20,6 +21,8 @@ const {
|
|
|
20
21
|
runWithConcurrencyLimit
|
|
21
22
|
} = require('../services/speed-test');
|
|
22
23
|
const { clearCodexRedirectCache } = require('../codex-proxy-server');
|
|
24
|
+
const { deleteBackup } = require('../services/codex-settings-manager');
|
|
25
|
+
const { PATHS } = require('../../config/paths');
|
|
23
26
|
const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
|
|
24
27
|
const CODEX_GATEWAY_SOURCE_TYPE = 'codex';
|
|
25
28
|
|
|
@@ -349,6 +352,12 @@ module.exports = (config) => {
|
|
|
349
352
|
if (proxyStatus && proxyStatus.running) {
|
|
350
353
|
console.log(`Codex proxy is running, stopping to apply channel settings: ${channel.name}`);
|
|
351
354
|
await stopCodexProxyServer({ clearStartTime: false });
|
|
355
|
+
deleteBackup();
|
|
356
|
+
try {
|
|
357
|
+
fs.unlinkSync(PATHS.activeChannel.codex);
|
|
358
|
+
} catch {
|
|
359
|
+
// ignore missing active channel marker
|
|
360
|
+
}
|
|
352
361
|
|
|
353
362
|
broadcastLog({
|
|
354
363
|
type: 'action',
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
readConfig
|
|
16
16
|
} = require('../services/codex-settings-manager');
|
|
17
17
|
const { getChannels, getEnabledChannels } = require('../services/codex-channels');
|
|
18
|
+
const { clearNativeOAuth } = require('../services/native-oauth-adapters');
|
|
18
19
|
const { clearAllLogs } = require('../websocket-server');
|
|
19
20
|
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
20
21
|
const fs = require('fs');
|
|
@@ -42,6 +43,24 @@ function selectLatestEnabledChannel(channels) {
|
|
|
42
43
|
}, enabledChannels[0]);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function resolveActiveChannel(channels, activeChannelId = null) {
|
|
47
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (activeChannelId) {
|
|
52
|
+
const matched = channels.find(channel => channel.id === activeChannelId);
|
|
53
|
+
if (matched) {
|
|
54
|
+
return matched;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return selectLatestEnabledChannel(channels)
|
|
59
|
+
|| channels.find(channel => channel.enabled !== false)
|
|
60
|
+
|| channels[0]
|
|
61
|
+
|| null;
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
// 保存激活渠道ID
|
|
46
65
|
function saveActiveChannelId(channelId) {
|
|
47
66
|
ensureStorageDirMigrated();
|
|
@@ -82,8 +101,7 @@ router.get('/status', (req, res) => {
|
|
|
82
101
|
try {
|
|
83
102
|
const proxyStatus = getCodexProxyStatus();
|
|
84
103
|
const { channels } = getChannels();
|
|
85
|
-
const
|
|
86
|
-
const activeChannel = enabledChannels[0]; // 多渠道模式:第一个启用的渠道
|
|
104
|
+
const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
|
|
87
105
|
const configStatus = {
|
|
88
106
|
isProxyConfig: isProxyConfig(),
|
|
89
107
|
configExists: configExists(),
|
|
@@ -143,6 +161,7 @@ router.post('/start', async (req, res) => {
|
|
|
143
161
|
}
|
|
144
162
|
|
|
145
163
|
// 5. 设置代理配置(备份并修改 config.toml 和 auth.json)
|
|
164
|
+
clearNativeOAuth('codex');
|
|
146
165
|
const configResult = setProxyConfig(proxyResult.port);
|
|
147
166
|
|
|
148
167
|
const updatedStatus = getCodexProxyStatus();
|
|
@@ -179,17 +198,9 @@ router.post('/start', async (req, res) => {
|
|
|
179
198
|
// 停止代理
|
|
180
199
|
router.post('/stop', async (req, res) => {
|
|
181
200
|
try {
|
|
182
|
-
// 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
|
|
183
201
|
const { channels } = getChannels();
|
|
184
202
|
const activeChannelId = loadActiveChannelId();
|
|
185
|
-
|
|
186
|
-
if (!activeChannel && activeChannelId) {
|
|
187
|
-
activeChannel = channels.find(ch => ch.id === activeChannelId);
|
|
188
|
-
}
|
|
189
|
-
if (!activeChannel) {
|
|
190
|
-
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
191
|
-
activeChannel = enabledChannels[0] || channels[0] || null;
|
|
192
|
-
}
|
|
203
|
+
const activeChannel = resolveActiveChannel(channels, activeChannelId);
|
|
193
204
|
|
|
194
205
|
// 2. 停止代理服务器
|
|
195
206
|
const proxyResult = await stopCodexProxyServer();
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
readEnv
|
|
17
17
|
} = require('../services/gemini-settings-manager');
|
|
18
18
|
const { getChannels, getEnabledChannels } = require('../services/gemini-channels');
|
|
19
|
+
const { clearNativeOAuth } = require('../services/native-oauth-adapters');
|
|
19
20
|
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
20
21
|
const fs = require('fs');
|
|
21
22
|
const path = require('path');
|
|
@@ -37,6 +38,24 @@ function selectLatestEnabledChannel(channels) {
|
|
|
37
38
|
}, enabledChannels[0]);
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function resolveActiveChannel(channels, activeChannelId = null) {
|
|
42
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (activeChannelId) {
|
|
47
|
+
const matched = channels.find(channel => channel.id === activeChannelId);
|
|
48
|
+
if (matched) {
|
|
49
|
+
return matched;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return selectLatestEnabledChannel(channels)
|
|
54
|
+
|| channels.find(channel => channel.enabled !== false)
|
|
55
|
+
|| channels[0]
|
|
56
|
+
|| null;
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
// 保存激活渠道ID
|
|
41
60
|
function saveActiveChannelId(channelId) {
|
|
42
61
|
ensureStorageDirMigrated();
|
|
@@ -83,8 +102,7 @@ router.get('/status', (req, res) => {
|
|
|
83
102
|
currentProxyPort: getCurrentProxyPort()
|
|
84
103
|
};
|
|
85
104
|
const { channels } = getChannels();
|
|
86
|
-
const
|
|
87
|
-
const activeChannel = enabledChannels[0]; // 多渠道模式:第一个启用的渠道
|
|
105
|
+
const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
|
|
88
106
|
|
|
89
107
|
res.json({
|
|
90
108
|
proxy: proxyStatus,
|
|
@@ -141,6 +159,7 @@ router.post('/start', async (req, res) => {
|
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
// 5. 设置代理配置(备份并修改 .env 和 settings.json)
|
|
162
|
+
clearNativeOAuth('gemini');
|
|
144
163
|
setProxyConfig(proxyResult.port);
|
|
145
164
|
|
|
146
165
|
const { broadcastProxyState } = require('../websocket-server');
|
|
@@ -164,17 +183,9 @@ router.post('/start', async (req, res) => {
|
|
|
164
183
|
// 停止代理
|
|
165
184
|
router.post('/stop', async (req, res) => {
|
|
166
185
|
try {
|
|
167
|
-
// 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
|
|
168
186
|
const { channels } = getChannels();
|
|
169
187
|
const activeChannelId = loadActiveChannelId();
|
|
170
|
-
|
|
171
|
-
if (!activeChannel && activeChannelId) {
|
|
172
|
-
activeChannel = channels.find(ch => ch.id === activeChannelId);
|
|
173
|
-
}
|
|
174
|
-
if (!activeChannel) {
|
|
175
|
-
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
176
|
-
activeChannel = enabledChannels[0] || channels[0] || null;
|
|
177
|
-
}
|
|
188
|
+
const activeChannel = resolveActiveChannel(channels, activeChannelId);
|
|
178
189
|
|
|
179
190
|
// 2. 停止代理服务器
|
|
180
191
|
const proxyResult = await stopGeminiProxyServer();
|
package/src/server/api/mcp.js
CHANGED
|
@@ -241,7 +241,9 @@ router.post('/servers/:id/test', async (req, res) => {
|
|
|
241
241
|
console.error('[MCP API] Test server failed:', error);
|
|
242
242
|
res.status(500).json({
|
|
243
243
|
success: false,
|
|
244
|
-
error: error.message
|
|
244
|
+
error: error.message,
|
|
245
|
+
message: error.message,
|
|
246
|
+
hint: error?.data?.hint || null
|
|
245
247
|
});
|
|
246
248
|
}
|
|
247
249
|
});
|
|
@@ -341,9 +343,24 @@ router.get('/servers/:id/tools', async (req, res) => {
|
|
|
341
343
|
try {
|
|
342
344
|
const { id } = req.params;
|
|
343
345
|
const result = await mcpService.getServerTools(id);
|
|
346
|
+
if (result.status === 'error') {
|
|
347
|
+
return res.status(502).json({
|
|
348
|
+
success: false,
|
|
349
|
+
error: result.error || '获取工具列表失败',
|
|
350
|
+
message: result.error || '获取工具列表失败',
|
|
351
|
+
hint: result.hint || null,
|
|
352
|
+
duration: result.duration,
|
|
353
|
+
tools: []
|
|
354
|
+
});
|
|
355
|
+
}
|
|
344
356
|
res.json({ success: true, ...result });
|
|
345
357
|
} catch (err) {
|
|
346
|
-
res.status(404).json({
|
|
358
|
+
res.status(404).json({
|
|
359
|
+
success: false,
|
|
360
|
+
error: err.message,
|
|
361
|
+
message: err.message,
|
|
362
|
+
hint: err?.data?.hint || null
|
|
363
|
+
});
|
|
347
364
|
}
|
|
348
365
|
});
|
|
349
366
|
|
|
@@ -357,13 +374,18 @@ router.post('/servers/:id/tools/test', async (req, res) => {
|
|
|
357
374
|
const { toolName, arguments: args } = req.body;
|
|
358
375
|
|
|
359
376
|
if (!toolName) {
|
|
360
|
-
return res.status(400).json({ success: false, error: '缺少 toolName 参数' });
|
|
377
|
+
return res.status(400).json({ success: false, error: '缺少 toolName 参数', message: '缺少 toolName 参数' });
|
|
361
378
|
}
|
|
362
379
|
|
|
363
380
|
const result = await mcpService.callServerTool(id, toolName, args || {});
|
|
364
381
|
res.json({ success: true, ...result });
|
|
365
382
|
} catch (err) {
|
|
366
|
-
res.status(500).json({
|
|
383
|
+
res.status(500).json({
|
|
384
|
+
success: false,
|
|
385
|
+
error: err.message,
|
|
386
|
+
message: err.message,
|
|
387
|
+
hint: err?.data?.hint || null
|
|
388
|
+
});
|
|
367
389
|
}
|
|
368
390
|
});
|
|
369
391
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
SUPPORTED_TOOLS,
|
|
5
|
+
getAllToolSummaries,
|
|
6
|
+
getToolSummary,
|
|
7
|
+
importCredential,
|
|
8
|
+
syncLocalCredential,
|
|
9
|
+
setDefaultCredential,
|
|
10
|
+
deleteCredential,
|
|
11
|
+
applyStoredCredential,
|
|
12
|
+
clearNativeOAuthState,
|
|
13
|
+
fetchCredentialUsage
|
|
14
|
+
} = require('../services/oauth-credentials-service');
|
|
15
|
+
|
|
16
|
+
function assertTool(tool) {
|
|
17
|
+
if (!SUPPORTED_TOOLS.includes(tool)) {
|
|
18
|
+
const error = new Error(`Unsupported OAuth tool: ${tool}`);
|
|
19
|
+
error.statusCode = 404;
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function broadcastToolProxyState(tool) {
|
|
25
|
+
const { broadcastProxyState } = require('../websocket-server');
|
|
26
|
+
|
|
27
|
+
if (tool === 'claude') {
|
|
28
|
+
const { getProxyStatus } = require('../proxy-server');
|
|
29
|
+
const { getAllChannels } = require('../services/channels');
|
|
30
|
+
const channels = getAllChannels();
|
|
31
|
+
const activeChannel = channels.find(ch => ch.enabled !== false) || null;
|
|
32
|
+
broadcastProxyState('claude', getProxyStatus(), activeChannel, channels);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (tool === 'codex') {
|
|
37
|
+
const { getCodexProxyStatus } = require('../codex-proxy-server');
|
|
38
|
+
const { getChannels } = require('../services/codex-channels');
|
|
39
|
+
const channels = getChannels().channels || [];
|
|
40
|
+
const activeChannel = channels.find(ch => ch.enabled !== false) || null;
|
|
41
|
+
broadcastProxyState('codex', getCodexProxyStatus(), activeChannel, channels);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (tool === 'gemini') {
|
|
46
|
+
const { getGeminiProxyStatus } = require('../gemini-proxy-server');
|
|
47
|
+
const { getChannels } = require('../services/gemini-channels');
|
|
48
|
+
const channels = getChannels().channels || [];
|
|
49
|
+
const activeChannel = channels.find(ch => ch.enabled !== false) || null;
|
|
50
|
+
broadcastProxyState('gemini', getGeminiProxyStatus(), activeChannel, channels);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (tool === 'opencode') {
|
|
55
|
+
const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
|
|
56
|
+
const { getChannels } = require('../services/opencode-channels');
|
|
57
|
+
const channels = getChannels().channels || [];
|
|
58
|
+
const activeChannel = channels.find(ch => ch.enabled !== false) || null;
|
|
59
|
+
broadcastProxyState('opencode', getOpenCodeProxyStatus(), activeChannel, channels);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
router.get('/', (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
res.json({ tools: getAllToolSummaries() });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
res.status(500).json({ error: error.message });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
router.get('/:tool', (req, res) => {
|
|
72
|
+
try {
|
|
73
|
+
const { tool } = req.params;
|
|
74
|
+
assertTool(tool);
|
|
75
|
+
res.json({ tool, summary: getToolSummary(tool) });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
router.post('/:tool/import', (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const { tool } = req.params;
|
|
84
|
+
assertTool(tool);
|
|
85
|
+
const credential = importCredential(tool, req.body || {});
|
|
86
|
+
res.json({ tool, credential, summary: getToolSummary(tool) });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
router.post('/:tool/sync-local', (req, res) => {
|
|
93
|
+
try {
|
|
94
|
+
const { tool } = req.params;
|
|
95
|
+
assertTool(tool);
|
|
96
|
+
const result = syncLocalCredential(tool);
|
|
97
|
+
res.json({ tool, ...result });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
router.post('/:tool/:credentialId/default', (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
const { tool, credentialId } = req.params;
|
|
106
|
+
assertTool(tool);
|
|
107
|
+
const summary = setDefaultCredential(tool, credentialId);
|
|
108
|
+
res.json({ tool, summary });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
router.post('/:tool/:credentialId/apply', async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
const { tool, credentialId } = req.params;
|
|
117
|
+
assertTool(tool);
|
|
118
|
+
const result = await applyStoredCredential(tool, credentialId);
|
|
119
|
+
broadcastToolProxyState(tool);
|
|
120
|
+
res.json({
|
|
121
|
+
tool,
|
|
122
|
+
...result,
|
|
123
|
+
message: `${tool} 已切换到 OAuth 凭证控制`
|
|
124
|
+
});
|
|
125
|
+
} catch (error) {
|
|
126
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
router.post('/:tool/clear-native', (req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
const { tool } = req.params;
|
|
133
|
+
assertTool(tool);
|
|
134
|
+
const nativeState = clearNativeOAuthState(tool);
|
|
135
|
+
res.json({ tool, nativeState });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
router.get('/:tool/:credentialId/usage', async (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const { tool, credentialId } = req.params;
|
|
144
|
+
assertTool(tool);
|
|
145
|
+
const result = await fetchCredentialUsage(tool, credentialId);
|
|
146
|
+
res.json({ tool, credentialId, usage: result });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
router.delete('/:tool/:credentialId', (req, res) => {
|
|
153
|
+
try {
|
|
154
|
+
const { tool, credentialId } = req.params;
|
|
155
|
+
assertTool(tool);
|
|
156
|
+
const summary = deleteCredential(tool, credentialId);
|
|
157
|
+
res.json({ tool, summary });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
res.status(error.statusCode || 500).json({ error: error.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
module.exports = router;
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
getCurrentProxyPort
|
|
17
17
|
} = require('../services/opencode-settings-manager');
|
|
18
18
|
const { getChannels, getEnabledChannels, applyChannelToSettings } = require('../services/opencode-channels');
|
|
19
|
+
const { clearNativeOAuth } = require('../services/native-oauth-adapters');
|
|
19
20
|
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
20
21
|
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
21
22
|
const fs = require('fs');
|
|
@@ -42,6 +43,24 @@ function selectLatestEnabledChannel(channels) {
|
|
|
42
43
|
}, enabledChannels[0]);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function resolveActiveChannel(channels, activeChannelId = null) {
|
|
47
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (activeChannelId) {
|
|
52
|
+
const matched = channels.find(channel => channel.id === activeChannelId);
|
|
53
|
+
if (matched) {
|
|
54
|
+
return matched;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return selectLatestEnabledChannel(channels)
|
|
59
|
+
|| channels.find(channel => channel.enabled !== false)
|
|
60
|
+
|| channels[0]
|
|
61
|
+
|| null;
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
// 保存激活渠道ID
|
|
46
65
|
function saveActiveChannelId(channelId) {
|
|
47
66
|
ensureStorageDirMigrated();
|
|
@@ -84,7 +103,7 @@ router.get('/status', (req, res) => {
|
|
|
84
103
|
const proxyStatus = getOpenCodeProxyStatus();
|
|
85
104
|
const { channels } = getChannels();
|
|
86
105
|
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
87
|
-
const activeChannel =
|
|
106
|
+
const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
|
|
88
107
|
const configStatus = {
|
|
89
108
|
isProxyConfig: isProxyConfig(),
|
|
90
109
|
configExists: configExists(),
|
|
@@ -176,6 +195,7 @@ router.post('/start', async (req, res) => {
|
|
|
176
195
|
});
|
|
177
196
|
|
|
178
197
|
const activeModel = currentChannel.model || currentChannel.speedTestModel || null;
|
|
198
|
+
clearNativeOAuth('opencode');
|
|
179
199
|
setProxyConfig(proxyResult.port, { channels: channelPayloads, model: activeModel });
|
|
180
200
|
|
|
181
201
|
// 5. 广播状态更新
|
|
@@ -199,17 +219,9 @@ router.post('/start', async (req, res) => {
|
|
|
199
219
|
// 停止代理
|
|
200
220
|
router.post('/stop', async (req, res) => {
|
|
201
221
|
try {
|
|
202
|
-
// 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
|
|
203
222
|
const { channels } = getChannels();
|
|
204
223
|
const activeChannelId = loadActiveChannelId();
|
|
205
|
-
|
|
206
|
-
if (!activeChannel && activeChannelId) {
|
|
207
|
-
activeChannel = channels.find(ch => ch.id === activeChannelId);
|
|
208
|
-
}
|
|
209
|
-
if (!activeChannel) {
|
|
210
|
-
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
211
|
-
activeChannel = enabledChannels[0] || channels[0] || null;
|
|
212
|
-
}
|
|
224
|
+
const activeChannel = resolveActiveChannel(channels, activeChannelId);
|
|
213
225
|
|
|
214
226
|
// 2. 停止代理服务器
|
|
215
227
|
const proxyResult = await stopOpenCodeProxyServer();
|