@adversity/coding-tool-x 3.0.6 → 3.1.0
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 +23 -0
- package/dist/web/assets/{icons-BxudHPiX.js → icons-CO_2OFES.js} +1 -1
- package/dist/web/assets/{index-D2VfwJBa.js → index-DI8QOi-E.js} +2 -2
- package/dist/web/assets/{index-oXBzu0bd.css → index-uLHGdeZh.css} +1 -1
- package/dist/web/assets/{naive-ui-DT-Uur8K.js → naive-ui-B1re3c-e.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +1 -1
- package/src/commands/daemon.js +11 -1
- package/src/commands/ui.js +8 -1
- package/src/index.js +3 -1
- package/src/server/api/oauth.js +294 -0
- package/src/server/codex-proxy-server.js +3 -2
- package/src/server/config/oauth-providers.js +68 -0
- package/src/server/gemini-proxy-server.js +3 -1
- package/src/server/index.js +12 -3
- package/src/server/proxy-server.js +4 -2
- package/src/server/services/channels.js +33 -2
- package/src/server/services/codex-channels.js +27 -2
- package/src/server/services/gemini-channels.js +34 -1
- package/src/server/services/oauth-callback-server.js +284 -0
- package/src/server/services/oauth-service.js +378 -0
- package/src/server/services/oauth-token-storage.js +135 -0
package/dist/web/index.html
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
<link rel="icon" href="/favicon.ico">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>CC-TOOL - ClaudeCode增强工作助手</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DI8QOi-E.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vue-vendor-6JaYHOiI.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendors-D2HHw_aW.js">
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/naive-ui-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-CO_2OFES.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/naive-ui-B1re3c-e.js">
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-uLHGdeZh.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/commands/daemon.js
CHANGED
|
@@ -72,11 +72,18 @@ async function handleStart() {
|
|
|
72
72
|
const config = loadConfig();
|
|
73
73
|
const port = config.ports?.webUI || 10099;
|
|
74
74
|
|
|
75
|
+
// 检查是否启用 LAN 访问 (--host 标志)
|
|
76
|
+
const enableHost = process.argv.includes('--host');
|
|
77
|
+
const pmArgs = ['ui', '--daemon'];
|
|
78
|
+
if (enableHost) {
|
|
79
|
+
pmArgs.push('--host');
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
// 启动 PM2 进程
|
|
76
83
|
pm2.start({
|
|
77
84
|
name: PM2_APP_NAME,
|
|
78
85
|
script: path.join(__dirname, '../index.js'),
|
|
79
|
-
args:
|
|
86
|
+
args: pmArgs,
|
|
80
87
|
interpreter: 'node',
|
|
81
88
|
autorestart: true,
|
|
82
89
|
max_memory_restart: '500M',
|
|
@@ -97,6 +104,9 @@ async function handleStart() {
|
|
|
97
104
|
|
|
98
105
|
console.log(chalk.green('\n✅ Coding-Tool 服务已启动(后台运行)\n'));
|
|
99
106
|
console.log(chalk.gray(`Web UI: http://localhost:${port}`));
|
|
107
|
+
if (enableHost) {
|
|
108
|
+
console.log(chalk.yellow(`⚠️ LAN 访问已启用 (http://<your-ip>:${port})`));
|
|
109
|
+
}
|
|
100
110
|
console.log(chalk.gray('\n可以安全关闭此终端窗口'));
|
|
101
111
|
console.log(chalk.gray('\n常用命令:'));
|
|
102
112
|
console.log(chalk.gray(' ') + chalk.cyan('ctx status') + chalk.gray(' - 查看服务状态'));
|
package/src/commands/ui.js
CHANGED
|
@@ -8,9 +8,16 @@ async function handleUI() {
|
|
|
8
8
|
// 检查是否为 daemon 模式(PM2 启动)
|
|
9
9
|
const isDaemon = process.argv.includes('--daemon');
|
|
10
10
|
|
|
11
|
+
// 检查是否启用 LAN 访问 (--host 标志)
|
|
12
|
+
const enableHost = process.argv.includes('--host');
|
|
13
|
+
const host = enableHost ? '0.0.0.0' : '127.0.0.1';
|
|
14
|
+
|
|
11
15
|
if (!isDaemon) {
|
|
12
16
|
console.clear();
|
|
13
17
|
console.log(chalk.cyan.bold('\n🌐 启动 Coding-Tool Web UI...\n'));
|
|
18
|
+
if (enableHost) {
|
|
19
|
+
console.log(chalk.yellow('⚠️ LAN 访问已启用 (--host)\n'));
|
|
20
|
+
}
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
// 从配置加载端口
|
|
@@ -19,7 +26,7 @@ async function handleUI() {
|
|
|
19
26
|
const url = `http://localhost:${port}`;
|
|
20
27
|
|
|
21
28
|
try {
|
|
22
|
-
await startServer(port);
|
|
29
|
+
await startServer(port, host);
|
|
23
30
|
|
|
24
31
|
// 自动打开浏览器(仅非 daemon 模式)
|
|
25
32
|
if (!isDaemon) {
|
package/src/index.js
CHANGED
|
@@ -48,8 +48,10 @@ function showHelp() {
|
|
|
48
48
|
console.log(' ctx status 查看服务状态\n');
|
|
49
49
|
|
|
50
50
|
console.log(chalk.yellow('📱 UI 管理:'));
|
|
51
|
-
console.log(' ctx ui 前台启动 Web UI
|
|
51
|
+
console.log(' ctx ui 前台启动 Web UI(仅本地访问)');
|
|
52
|
+
console.log(' ctx ui --host 前台启动 Web UI(允许 LAN 访问)');
|
|
52
53
|
console.log(' ctx ui start 后台启动 Web UI');
|
|
54
|
+
console.log(' ctx ui start --host 后台启动 Web UI(允许 LAN 访问)');
|
|
53
55
|
console.log(' ctx ui stop 停止 Web UI');
|
|
54
56
|
console.log(' ctx ui restart 重启 Web UI\n');
|
|
55
57
|
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const { OAUTH_PROVIDERS, getProviderConfig } = require('../config/oauth-providers');
|
|
6
|
+
const {
|
|
7
|
+
startFlow,
|
|
8
|
+
getPendingFlow,
|
|
9
|
+
completeFlow,
|
|
10
|
+
failFlow,
|
|
11
|
+
cancelFlow,
|
|
12
|
+
exchangeCodeForToken,
|
|
13
|
+
refreshToken
|
|
14
|
+
} = require('../services/oauth-service');
|
|
15
|
+
const {
|
|
16
|
+
startCallbackServer,
|
|
17
|
+
stopCallbackServer,
|
|
18
|
+
isServerRunning
|
|
19
|
+
} = require('../services/oauth-callback-server');
|
|
20
|
+
const {
|
|
21
|
+
getAllTokens,
|
|
22
|
+
getToken,
|
|
23
|
+
saveToken,
|
|
24
|
+
updateToken,
|
|
25
|
+
deleteToken
|
|
26
|
+
} = require('../services/oauth-token-storage');
|
|
27
|
+
|
|
28
|
+
// Track callback servers by state
|
|
29
|
+
const stateToPort = new Map();
|
|
30
|
+
|
|
31
|
+
// GET /api/oauth/providers - List available OAuth providers
|
|
32
|
+
router.get('/providers', (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const providers = Object.entries(OAUTH_PROVIDERS).map(([id, config]) => ({
|
|
35
|
+
id,
|
|
36
|
+
name: config.name,
|
|
37
|
+
scopes: config.scopes
|
|
38
|
+
}));
|
|
39
|
+
res.json({ providers });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Error fetching OAuth providers:', error);
|
|
42
|
+
res.status(500).json({ error: error.message });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// POST /api/oauth/start - Start OAuth flow
|
|
47
|
+
router.post('/start', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { provider, channelId, mode = 'browser' } = req.body;
|
|
50
|
+
|
|
51
|
+
if (!provider) {
|
|
52
|
+
return res.status(400).json({ error: 'Missing required field: provider' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate provider
|
|
56
|
+
if (!OAUTH_PROVIDERS[provider]) {
|
|
57
|
+
return res.status(400).json({ error: `Unknown provider: ${provider}` });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const providerConfig = getProviderConfig(provider);
|
|
61
|
+
const { state, authUrl } = startFlow(provider, channelId);
|
|
62
|
+
const callbackPort = providerConfig.callbackPort;
|
|
63
|
+
const callbackPath = providerConfig.callbackPath;
|
|
64
|
+
|
|
65
|
+
// Track state -> port mapping
|
|
66
|
+
stateToPort.set(state, callbackPort);
|
|
67
|
+
|
|
68
|
+
// Start callback server
|
|
69
|
+
await startCallbackServer(callbackPort, callbackPath, async (callbackData) => {
|
|
70
|
+
const { code, state: cbState, error, errorDescription } = callbackData;
|
|
71
|
+
|
|
72
|
+
if (error) {
|
|
73
|
+
failFlow(cbState, `${error}: ${errorDescription || 'Unknown error'}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!code) {
|
|
78
|
+
failFlow(cbState, 'No authorization code received');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Get pending flow to retrieve code verifier
|
|
83
|
+
const flow = getPendingFlow(cbState);
|
|
84
|
+
if (!flow) {
|
|
85
|
+
console.error('OAuth callback received for unknown state:', cbState);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Exchange code for token
|
|
91
|
+
const tokenResult = await exchangeCodeForToken(
|
|
92
|
+
flow.provider,
|
|
93
|
+
code,
|
|
94
|
+
flow.codeVerifier
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Calculate expiry time
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const expiresAt = tokenResult.expiresIn
|
|
100
|
+
? now + (tokenResult.expiresIn * 1000)
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
// Save token
|
|
104
|
+
const savedToken = saveToken({
|
|
105
|
+
provider: flow.provider,
|
|
106
|
+
channelId: flow.channelId,
|
|
107
|
+
accessToken: tokenResult.accessToken,
|
|
108
|
+
refreshToken: tokenResult.refreshToken,
|
|
109
|
+
idToken: tokenResult.idToken,
|
|
110
|
+
expiresAt,
|
|
111
|
+
scope: tokenResult.scope
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Mark flow as completed
|
|
115
|
+
completeFlow(cbState, savedToken.id);
|
|
116
|
+
console.log(`OAuth flow completed for provider: ${flow.provider}, tokenId: ${savedToken.id}`);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('Error exchanging code for token:', err);
|
|
119
|
+
failFlow(cbState, err.message);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Open browser if mode is 'browser'
|
|
124
|
+
if (mode === 'browser') {
|
|
125
|
+
try {
|
|
126
|
+
const open = require('open');
|
|
127
|
+
await open(authUrl);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('Failed to open browser:', err);
|
|
130
|
+
// Don't fail the request, user can still open URL manually
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
res.json({
|
|
135
|
+
success: true,
|
|
136
|
+
stateId: state,
|
|
137
|
+
authUrl,
|
|
138
|
+
callbackPort
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Error starting OAuth flow:', error);
|
|
142
|
+
res.status(500).json({ error: error.message });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// GET /api/oauth/status/:stateId - Get OAuth flow status
|
|
147
|
+
router.get('/status/:stateId', (req, res) => {
|
|
148
|
+
try {
|
|
149
|
+
const { stateId } = req.params;
|
|
150
|
+
const flow = getPendingFlow(stateId);
|
|
151
|
+
|
|
152
|
+
if (!flow) {
|
|
153
|
+
return res.status(404).json({
|
|
154
|
+
status: 'not_found',
|
|
155
|
+
error: 'OAuth flow not found or expired'
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const response = {
|
|
160
|
+
status: flow.status
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (flow.status === 'completed' && flow.tokenId) {
|
|
164
|
+
response.tokenId = flow.tokenId;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (flow.status === 'failed' && flow.error) {
|
|
168
|
+
response.error = flow.error;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
res.json(response);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Error fetching OAuth status:', error);
|
|
174
|
+
res.status(500).json({ error: error.message });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// POST /api/oauth/cancel/:stateId - Cancel OAuth flow
|
|
179
|
+
router.post('/cancel/:stateId', (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const { stateId } = req.params;
|
|
182
|
+
|
|
183
|
+
// Get port for this state
|
|
184
|
+
const port = stateToPort.get(stateId);
|
|
185
|
+
|
|
186
|
+
// Stop callback server if running
|
|
187
|
+
if (port && isServerRunning(port)) {
|
|
188
|
+
stopCallbackServer(port);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Cancel the flow
|
|
192
|
+
cancelFlow(stateId);
|
|
193
|
+
|
|
194
|
+
// Clean up state -> port mapping
|
|
195
|
+
stateToPort.delete(stateId);
|
|
196
|
+
|
|
197
|
+
res.json({ success: true });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Error cancelling OAuth flow:', error);
|
|
200
|
+
res.status(500).json({ error: error.message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// GET /api/oauth/tokens - List all tokens (masked)
|
|
205
|
+
router.get('/tokens', (req, res) => {
|
|
206
|
+
try {
|
|
207
|
+
const tokens = getAllTokens();
|
|
208
|
+
res.json({ tokens });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error('Error fetching OAuth tokens:', error);
|
|
211
|
+
res.status(500).json({ error: error.message });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// GET /api/oauth/tokens/:tokenId - Get single token (masked)
|
|
216
|
+
router.get('/tokens/:tokenId', (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const token = getToken(req.params.tokenId);
|
|
219
|
+
if (!token) {
|
|
220
|
+
return res.status(404).json({ error: 'Token not found' });
|
|
221
|
+
}
|
|
222
|
+
// Return masked token info
|
|
223
|
+
const masked = {
|
|
224
|
+
...token,
|
|
225
|
+
accessToken: token.accessToken ? token.accessToken.substring(0, 8) + '...' : null,
|
|
226
|
+
refreshToken: token.refreshToken ? token.refreshToken.substring(0, 8) + '...' : null
|
|
227
|
+
};
|
|
228
|
+
res.json(masked);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('Error fetching OAuth token:', error);
|
|
231
|
+
res.status(500).json({ error: error.message });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// DELETE /api/oauth/tokens/:tokenId - Delete a token
|
|
236
|
+
router.delete('/tokens/:tokenId', (req, res) => {
|
|
237
|
+
try {
|
|
238
|
+
const { tokenId } = req.params;
|
|
239
|
+
const result = deleteToken(tokenId);
|
|
240
|
+
res.json(result);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error.message === 'Token not found') {
|
|
243
|
+
return res.status(404).json({ error: 'Token not found' });
|
|
244
|
+
}
|
|
245
|
+
console.error('Error deleting OAuth token:', error);
|
|
246
|
+
res.status(500).json({ error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// POST /api/oauth/refresh/:tokenId - Refresh a token
|
|
251
|
+
router.post('/refresh/:tokenId', async (req, res) => {
|
|
252
|
+
try {
|
|
253
|
+
const { tokenId } = req.params;
|
|
254
|
+
const token = getToken(tokenId);
|
|
255
|
+
|
|
256
|
+
if (!token) {
|
|
257
|
+
return res.status(404).json({ error: 'Token not found' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!token.refreshToken) {
|
|
261
|
+
return res.status(400).json({ error: 'Token does not have a refresh token' });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Refresh the token
|
|
265
|
+
const refreshResult = await refreshToken(token.provider, token.refreshToken);
|
|
266
|
+
|
|
267
|
+
// Calculate new expiry time
|
|
268
|
+
const now = Date.now();
|
|
269
|
+
const expiresAt = refreshResult.expiresIn
|
|
270
|
+
? now + (refreshResult.expiresIn * 1000)
|
|
271
|
+
: null;
|
|
272
|
+
|
|
273
|
+
// Update token in storage
|
|
274
|
+
const updatedToken = updateToken(tokenId, {
|
|
275
|
+
accessToken: refreshResult.accessToken,
|
|
276
|
+
refreshToken: refreshResult.refreshToken || token.refreshToken,
|
|
277
|
+
expiresAt,
|
|
278
|
+
scope: refreshResult.scope || token.scope
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
res.json({
|
|
282
|
+
success: true,
|
|
283
|
+
expiresAt: updatedToken.expiresAt
|
|
284
|
+
});
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error.message === 'Token not found') {
|
|
287
|
+
return res.status(404).json({ error: 'Token not found' });
|
|
288
|
+
}
|
|
289
|
+
console.error('Error refreshing OAuth token:', error);
|
|
290
|
+
res.status(500).json({ error: error.message });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
module.exports = router;
|
|
@@ -10,7 +10,7 @@ const DEFAULT_CONFIG = require('../config/default');
|
|
|
10
10
|
const { resolvePricing } = require('./utils/pricing');
|
|
11
11
|
const { recordRequest: recordCodexRequest } = require('./services/codex-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
|
-
const { getEnabledChannels, writeCodexConfigForMultiChannel } = require('./services/codex-channels');
|
|
13
|
+
const { getEnabledChannels, writeCodexConfigForMultiChannel, getEffectiveApiKey } = require('./services/codex-channels');
|
|
14
14
|
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
15
15
|
|
|
16
16
|
let proxyServer = null;
|
|
@@ -257,7 +257,8 @@ async function startCodexProxyServer(options = {}) {
|
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
proxyReq.removeHeader('authorization');
|
|
260
|
-
|
|
260
|
+
const effectiveKey = getEffectiveApiKey(activeChannel);
|
|
261
|
+
proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
|
|
261
262
|
proxyReq.setHeader('openai-beta', 'responses=experimental');
|
|
262
263
|
if (!proxyReq.getHeader('content-type')) {
|
|
263
264
|
proxyReq.setHeader('content-type', 'application/json');
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const OAUTH_PROVIDERS = {
|
|
4
|
+
claude: {
|
|
5
|
+
name: 'Claude Pro',
|
|
6
|
+
authUrl: 'https://claude.ai/oauth/authorize',
|
|
7
|
+
tokenUrl: 'https://console.anthropic.com/v1/oauth/token',
|
|
8
|
+
clientId: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
|
|
9
|
+
redirectUri: 'http://localhost:54545/callback',
|
|
10
|
+
scopes: ['org:create_api_key', 'user:profile', 'user:inference'],
|
|
11
|
+
authType: 'pkce',
|
|
12
|
+
callbackPort: 54545,
|
|
13
|
+
callbackPath: '/callback'
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
codex: {
|
|
17
|
+
name: 'OpenAI Codex',
|
|
18
|
+
authUrl: 'https://auth.openai.com/oauth/authorize',
|
|
19
|
+
tokenUrl: 'https://auth.openai.com/oauth/token',
|
|
20
|
+
clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
|
|
21
|
+
redirectUri: 'http://localhost:1455/auth/callback',
|
|
22
|
+
scopes: ['openid', 'profile', 'email', 'offline_access'],
|
|
23
|
+
authType: 'pkce',
|
|
24
|
+
extraParams: {
|
|
25
|
+
codex_cli_simplified_flow: 'true',
|
|
26
|
+
originator: 'ctx'
|
|
27
|
+
},
|
|
28
|
+
tokenContentType: 'application/x-www-form-urlencoded',
|
|
29
|
+
callbackPort: 1455,
|
|
30
|
+
callbackPath: '/auth/callback'
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
gemini: {
|
|
34
|
+
name: 'Google Gemini',
|
|
35
|
+
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
36
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
37
|
+
clientId: '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com',
|
|
38
|
+
clientSecret: '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',
|
|
39
|
+
redirectUri: 'http://localhost:51121/callback',
|
|
40
|
+
scopes: [
|
|
41
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
42
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
43
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
44
|
+
'https://www.googleapis.com/auth/cclog',
|
|
45
|
+
'https://www.googleapis.com/auth/experimentsandconfigs'
|
|
46
|
+
],
|
|
47
|
+
authType: 'standard',
|
|
48
|
+
extraParams: {
|
|
49
|
+
access_type: 'offline',
|
|
50
|
+
prompt: 'consent'
|
|
51
|
+
},
|
|
52
|
+
callbackPort: 51121,
|
|
53
|
+
callbackPath: '/callback'
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function getProviderConfig(provider) {
|
|
58
|
+
const config = OAUTH_PROVIDERS[provider];
|
|
59
|
+
if (!config) {
|
|
60
|
+
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
61
|
+
}
|
|
62
|
+
return { ...config };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
OAUTH_PROVIDERS,
|
|
67
|
+
getProviderConfig
|
|
68
|
+
};
|
|
@@ -10,6 +10,7 @@ const DEFAULT_CONFIG = require('../config/default');
|
|
|
10
10
|
const { resolvePricing } = require('./utils/pricing');
|
|
11
11
|
const { recordRequest: recordGeminiRequest } = require('./services/gemini-statistics-service');
|
|
12
12
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
13
|
+
const { getEffectiveApiKey } = require('./services/gemini-channels');
|
|
13
14
|
|
|
14
15
|
let proxyServer = null;
|
|
15
16
|
let proxyApp = null;
|
|
@@ -152,7 +153,8 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
152
153
|
|
|
153
154
|
proxyReq.removeHeader('authorization');
|
|
154
155
|
proxyReq.removeHeader('x-goog-api-key');
|
|
155
|
-
|
|
156
|
+
const effectiveKey = getEffectiveApiKey(activeChannel);
|
|
157
|
+
proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
|
|
156
158
|
if (!proxyReq.getHeader('content-type')) {
|
|
157
159
|
proxyReq.setHeader('content-type', 'application/json');
|
|
158
160
|
}
|
package/src/server/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const { startProxyServer } = require('./proxy-server');
|
|
|
14
14
|
const { startCodexProxyServer } = require('./codex-proxy-server');
|
|
15
15
|
const { startGeminiProxyServer } = require('./gemini-proxy-server');
|
|
16
16
|
|
|
17
|
-
async function startServer(port) {
|
|
17
|
+
async function startServer(port, host = '127.0.0.1') {
|
|
18
18
|
const config = loadConfig();
|
|
19
19
|
// 使用配置的端口,如果没有传入参数
|
|
20
20
|
if (!port) {
|
|
@@ -154,6 +154,9 @@ async function startServer(port) {
|
|
|
154
154
|
// 配置同步 API
|
|
155
155
|
app.use('/api/config-sync', require('./api/config-sync'));
|
|
156
156
|
|
|
157
|
+
// OAuth API
|
|
158
|
+
app.use('/api/oauth', require('./api/oauth'));
|
|
159
|
+
|
|
157
160
|
// 配置注册表 API (集中管理 skills/commands/agents/rules 的启用/禁用)
|
|
158
161
|
app.use('/api/config-registry', require('./api/config-registry'));
|
|
159
162
|
|
|
@@ -170,9 +173,15 @@ async function startServer(port) {
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
// Start server
|
|
173
|
-
const server = app.listen(port, () => {
|
|
176
|
+
const server = app.listen(port, host, () => {
|
|
174
177
|
console.log(`\n🚀 Coding-Tool Web UI running at:`);
|
|
175
|
-
|
|
178
|
+
if (host === '0.0.0.0') {
|
|
179
|
+
console.log(chalk.yellow(` ⚠️ 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
|
|
180
|
+
console.log(` http://localhost:${port}`);
|
|
181
|
+
console.log(chalk.gray(` http://<your-ip>:${port} (LAN 访问)`));
|
|
182
|
+
} else {
|
|
183
|
+
console.log(` http://localhost:${port}`);
|
|
184
|
+
}
|
|
176
185
|
|
|
177
186
|
// 附加 WebSocket 服务器到同一个端口
|
|
178
187
|
attachWebSocketServer(server);
|
|
@@ -13,6 +13,7 @@ const { recordRequest } = require('./services/statistics-service');
|
|
|
13
13
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
14
14
|
const eventBus = require('../plugins/event-bus');
|
|
15
15
|
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
16
|
+
const { getEffectiveApiKey } = require('./services/channels');
|
|
16
17
|
|
|
17
18
|
let proxyServer = null;
|
|
18
19
|
let proxyApp = null;
|
|
@@ -185,9 +186,10 @@ async function startProxyServer(options = {}) {
|
|
|
185
186
|
});
|
|
186
187
|
|
|
187
188
|
proxyReq.removeHeader('x-api-key');
|
|
188
|
-
|
|
189
|
+
const effectiveKey = getEffectiveApiKey(selectedChannel);
|
|
190
|
+
proxyReq.setHeader('x-api-key', effectiveKey);
|
|
189
191
|
proxyReq.removeHeader('authorization');
|
|
190
|
-
proxyReq.setHeader('authorization', `Bearer ${
|
|
192
|
+
proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
|
|
191
193
|
|
|
192
194
|
if (!proxyReq.getHeader('anthropic-version')) {
|
|
193
195
|
proxyReq.setHeader('anthropic-version', '2023-06-01');
|
|
@@ -65,6 +65,11 @@ function applyChannelDefaults(channel) {
|
|
|
65
65
|
normalized.enabled = !!normalized.enabled;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// OAuth 字段默认值(向后兼容)
|
|
69
|
+
if (!normalized.authType) {
|
|
70
|
+
normalized.authType = 'apiKey';
|
|
71
|
+
}
|
|
72
|
+
|
|
68
73
|
normalized.weight = normalizeNumber(normalized.weight, 1, 100);
|
|
69
74
|
|
|
70
75
|
if (normalized.maxConcurrency === undefined ||
|
|
@@ -189,7 +194,11 @@ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
|
|
|
189
194
|
modelConfig: extraConfig.modelConfig || null,
|
|
190
195
|
modelRedirects: extraConfig.modelRedirects || [],
|
|
191
196
|
proxyUrl: extraConfig.proxyUrl || '',
|
|
192
|
-
speedTestModel: extraConfig.speedTestModel || null
|
|
197
|
+
speedTestModel: extraConfig.speedTestModel || null,
|
|
198
|
+
// OAuth 支持
|
|
199
|
+
authType: extraConfig.authType || 'apiKey',
|
|
200
|
+
oauthProvider: extraConfig.oauthProvider || null,
|
|
201
|
+
oauthTokenId: extraConfig.oauthTokenId || null
|
|
193
202
|
});
|
|
194
203
|
|
|
195
204
|
data.channels.push(newChannel);
|
|
@@ -381,6 +390,27 @@ function updateClaudeSettings(baseUrl, apiKey) {
|
|
|
381
390
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
382
391
|
}
|
|
383
392
|
|
|
393
|
+
/**
|
|
394
|
+
* 获取渠道的有效 API Key
|
|
395
|
+
* 如果渠道使用 OAuth 认证,返回有效的 OAuth 令牌;否则返回静态 API Key
|
|
396
|
+
*
|
|
397
|
+
* @param {Object} channel - 渠道对象
|
|
398
|
+
* @returns {string|null} 有效的 API Key,OAuth 令牌无效/过期时返回 null
|
|
399
|
+
*/
|
|
400
|
+
function getEffectiveApiKey(channel) {
|
|
401
|
+
if (channel.authType === 'oauth' && channel.oauthTokenId) {
|
|
402
|
+
const { getToken, isTokenExpired } = require('./oauth-token-storage');
|
|
403
|
+
const token = getToken(channel.oauthTokenId);
|
|
404
|
+
if (token && !isTokenExpired(token)) {
|
|
405
|
+
return token.accessToken;
|
|
406
|
+
}
|
|
407
|
+
// OAuth 令牌无效或已过期,返回 null(调用方应处理刷新或报错)
|
|
408
|
+
console.warn(`[Channels] OAuth token expired or not found for channel ${channel.name}`);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return channel.apiKey;
|
|
412
|
+
}
|
|
413
|
+
|
|
384
414
|
module.exports = {
|
|
385
415
|
getAllChannels,
|
|
386
416
|
getCurrentSettings,
|
|
@@ -390,5 +420,6 @@ module.exports = {
|
|
|
390
420
|
applyChannelToSettings,
|
|
391
421
|
getBestChannelForRestore,
|
|
392
422
|
updateClaudeSettings,
|
|
393
|
-
updateClaudeSettingsWithModelConfig
|
|
423
|
+
updateClaudeSettingsWithModelConfig,
|
|
424
|
+
getEffectiveApiKey
|
|
394
425
|
};
|
|
@@ -49,7 +49,8 @@ function loadChannels() {
|
|
|
49
49
|
weight: ch.weight || 1,
|
|
50
50
|
maxConcurrency: ch.maxConcurrency || null,
|
|
51
51
|
modelRedirects: ch.modelRedirects || [],
|
|
52
|
-
speedTestModel: ch.speedTestModel || null
|
|
52
|
+
speedTestModel: ch.speedTestModel || null,
|
|
53
|
+
authType: ch.authType || 'apiKey' // 默认 API Key 认证
|
|
53
54
|
}));
|
|
54
55
|
}
|
|
55
56
|
return data;
|
|
@@ -179,6 +180,9 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
|
|
|
179
180
|
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
180
181
|
modelRedirects: extraConfig.modelRedirects || [],
|
|
181
182
|
speedTestModel: extraConfig.speedTestModel || null,
|
|
183
|
+
authType: extraConfig.authType || 'apiKey',
|
|
184
|
+
oauthProvider: extraConfig.oauthProvider || null,
|
|
185
|
+
oauthTokenId: extraConfig.oauthTokenId || null,
|
|
182
186
|
createdAt: Date.now(),
|
|
183
187
|
updatedAt: Date.now()
|
|
184
188
|
};
|
|
@@ -649,6 +653,26 @@ try {
|
|
|
649
653
|
console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
|
|
650
654
|
}
|
|
651
655
|
|
|
656
|
+
/**
|
|
657
|
+
* 获取渠道的有效 API Key
|
|
658
|
+
* 如果渠道使用 OAuth 认证,返回 OAuth 令牌;否则返回静态 API Key
|
|
659
|
+
*
|
|
660
|
+
* @param {Object} channel - 渠道对象
|
|
661
|
+
* @returns {string|null} 有效的 API Key
|
|
662
|
+
*/
|
|
663
|
+
function getEffectiveApiKey(channel) {
|
|
664
|
+
if (channel.authType === 'oauth' && channel.oauthTokenId) {
|
|
665
|
+
const { getToken, isTokenExpired } = require('./oauth-token-storage');
|
|
666
|
+
const token = getToken(channel.oauthTokenId);
|
|
667
|
+
if (token && !isTokenExpired(token)) {
|
|
668
|
+
return token.accessToken;
|
|
669
|
+
}
|
|
670
|
+
// OAuth 令牌无效或已过期,返回 null
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
return channel.apiKey;
|
|
674
|
+
}
|
|
675
|
+
|
|
652
676
|
module.exports = {
|
|
653
677
|
getChannels,
|
|
654
678
|
createChannel,
|
|
@@ -658,5 +682,6 @@ module.exports = {
|
|
|
658
682
|
saveChannelOrder,
|
|
659
683
|
syncAllChannelEnvVars,
|
|
660
684
|
writeCodexConfigForMultiChannel,
|
|
661
|
-
applyChannelToSettings
|
|
685
|
+
applyChannelToSettings,
|
|
686
|
+
getEffectiveApiKey
|
|
662
687
|
};
|