@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.
@@ -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-D2VfwJBa.js"></script>
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-BxudHPiX.js">
12
- <link rel="modulepreload" crossorigin href="/assets/naive-ui-DT-Uur8K.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-oXBzu0bd.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adversity/coding-tool-x",
3
- "version": "3.0.6",
3
+ "version": "3.1.0",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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: ['ui', '--daemon'],
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(' - 查看服务状态'));
@@ -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
- proxyReq.setHeader('authorization', `Bearer ${activeChannel.apiKey}`);
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
- proxyReq.setHeader('authorization', `Bearer ${activeChannel.apiKey}`);
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
  }
@@ -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
- console.log(` http://localhost:${port}`);
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
- proxyReq.setHeader('x-api-key', selectedChannel.apiKey);
189
+ const effectiveKey = getEffectiveApiKey(selectedChannel);
190
+ proxyReq.setHeader('x-api-key', effectiveKey);
189
191
  proxyReq.removeHeader('authorization');
190
- proxyReq.setHeader('authorization', `Bearer ${selectedChannel.apiKey}`);
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
  };