coding-tool-x 3.3.7 → 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 +5 -0
- package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-DLpoDZ2M.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BPtkTMSc.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-BGx9MSDV.js → PluginManager-JXsyym1s.js} +1 -1
- package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DZWSeb-q.js} +1 -1
- package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-Cs624DR3.js} +1 -1
- package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-bEliz7qz.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CHtgMfKc.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/toggle-proxy.js +100 -5
- 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/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 +1 -0
- 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 +23 -2
- package/src/server/services/gemini-channels.js +11 -9
- package/src/server/services/mcp-service.js +33 -16
- 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/dist/web/assets/Home-BsSioaaB.css +0 -1
- package/dist/web/assets/Home-obifg_9E.js +0 -1
- package/dist/web/assets/index-C7LPdVsN.js +0 -2
- package/dist/web/assets/index-eEmjZKWP.css +0 -1
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { PATHS } = require('../../config/paths');
|
|
5
|
+
const claudeSettingsManager = require('./settings-manager');
|
|
6
|
+
const codexSettingsManager = require('./codex-settings-manager');
|
|
7
|
+
const geminiSettingsManager = require('./gemini-settings-manager');
|
|
8
|
+
const opencodeSettingsManager = require('./opencode-settings-manager');
|
|
9
|
+
const {
|
|
10
|
+
SUPPORTED_TOOLS,
|
|
11
|
+
fingerprintFor,
|
|
12
|
+
inspectTool,
|
|
13
|
+
readAllNativeOAuth,
|
|
14
|
+
clearNativeOAuth,
|
|
15
|
+
applyOAuthCredential
|
|
16
|
+
} = require('./native-oauth-adapters');
|
|
17
|
+
const { maskToken, decodeJwtPayload, removeFileIfExists } = require('./oauth-utils');
|
|
18
|
+
|
|
19
|
+
function createEmptyStore() {
|
|
20
|
+
return {
|
|
21
|
+
version: 1,
|
|
22
|
+
tools: Object.fromEntries(SUPPORTED_TOOLS.map((tool) => [tool, {
|
|
23
|
+
defaultCredentialId: null,
|
|
24
|
+
credentials: []
|
|
25
|
+
}]))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureStoreDir() {
|
|
30
|
+
fs.mkdirSync(path.dirname(PATHS.oauthCredentials), { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readStore() {
|
|
34
|
+
ensureStoreDir();
|
|
35
|
+
if (!fs.existsSync(PATHS.oauthCredentials)) {
|
|
36
|
+
return createEmptyStore();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const payload = JSON.parse(fs.readFileSync(PATHS.oauthCredentials, 'utf8'));
|
|
41
|
+
const next = createEmptyStore();
|
|
42
|
+
if (payload && typeof payload === 'object' && payload.tools && typeof payload.tools === 'object') {
|
|
43
|
+
SUPPORTED_TOOLS.forEach((tool) => {
|
|
44
|
+
const rawToolData = payload.tools[tool];
|
|
45
|
+
if (!rawToolData || typeof rawToolData !== 'object') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
next.tools[tool] = {
|
|
49
|
+
defaultCredentialId: rawToolData.defaultCredentialId || null,
|
|
50
|
+
credentials: Array.isArray(rawToolData.credentials) ? rawToolData.credentials : []
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return next;
|
|
55
|
+
} catch {
|
|
56
|
+
return createEmptyStore();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeStore(store) {
|
|
61
|
+
ensureStoreDir();
|
|
62
|
+
fs.writeFileSync(PATHS.oauthCredentials, JSON.stringify(store, null, 2), 'utf8');
|
|
63
|
+
if (process.platform !== 'win32') {
|
|
64
|
+
try {
|
|
65
|
+
fs.chmodSync(PATHS.oauthCredentials, 0o600);
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore chmod failures
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertSupportedTool(tool) {
|
|
73
|
+
if (!SUPPORTED_TOOLS.includes(tool)) {
|
|
74
|
+
throw new Error(`Unsupported OAuth tool: ${tool}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function safeString(value) {
|
|
79
|
+
return String(value || '').trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function safeNumber(value) {
|
|
83
|
+
const num = Number(value);
|
|
84
|
+
return Number.isFinite(num) && num > 0 ? num : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function tryParseJson(text) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(text);
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function extractEnvValue(text, key) {
|
|
96
|
+
const pattern = new RegExp(`${key}\\s*=\\s*([^\\n\\r]+)`);
|
|
97
|
+
const match = String(text || '').match(pattern);
|
|
98
|
+
return match ? safeString(match[1]).replace(/^['"]|['"]$/g, '') : '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseClaudeImport(rawText) {
|
|
102
|
+
const text = safeString(rawText);
|
|
103
|
+
const parsed = tryParseJson(text);
|
|
104
|
+
const payload = parsed?.claudeAiOauth && typeof parsed.claudeAiOauth === 'object'
|
|
105
|
+
? parsed.claudeAiOauth
|
|
106
|
+
: parsed;
|
|
107
|
+
|
|
108
|
+
if (payload && typeof payload === 'object') {
|
|
109
|
+
const accessToken = safeString(
|
|
110
|
+
payload.accessToken
|
|
111
|
+
|| payload.access_token
|
|
112
|
+
|| payload.authToken
|
|
113
|
+
|| payload.token
|
|
114
|
+
);
|
|
115
|
+
if (!accessToken) {
|
|
116
|
+
throw new Error('Claude OAuth 导入缺少 accessToken。');
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
accessToken,
|
|
120
|
+
refreshToken: safeString(payload.refreshToken || payload.refresh_token),
|
|
121
|
+
expiresAt: safeNumber(payload.expiresAt || payload.expiry_date || payload.expiryDate),
|
|
122
|
+
primaryToken: accessToken
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const envToken = extractEnvValue(text, 'ANTHROPIC_AUTH_TOKEN')
|
|
127
|
+
|| extractEnvValue(text, 'CLAUDE_CODE_OAUTH_TOKEN');
|
|
128
|
+
if (envToken) {
|
|
129
|
+
return {
|
|
130
|
+
accessToken: envToken,
|
|
131
|
+
refreshToken: '',
|
|
132
|
+
expiresAt: null,
|
|
133
|
+
primaryToken: envToken
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!text.includes('\n') && !text.includes(' ')) {
|
|
138
|
+
return {
|
|
139
|
+
accessToken: text,
|
|
140
|
+
refreshToken: '',
|
|
141
|
+
expiresAt: null,
|
|
142
|
+
primaryToken: text
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error('无法识别 Claude OAuth 导入格式。');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseCodexImport(rawText) {
|
|
150
|
+
const parsed = tryParseJson(rawText);
|
|
151
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
152
|
+
throw new Error('Codex OAuth 仅支持 JSON 导入。');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const authPayload = parsed.tokens ? parsed : {
|
|
156
|
+
auth_mode: parsed.auth_mode || 'chatgpt',
|
|
157
|
+
tokens: parsed
|
|
158
|
+
};
|
|
159
|
+
const tokens = authPayload.tokens && typeof authPayload.tokens === 'object'
|
|
160
|
+
? authPayload.tokens
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
if (!tokens?.access_token) {
|
|
164
|
+
throw new Error('Codex OAuth 导入缺少 tokens.access_token。');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const idTokenPayload = decodeJwtPayload(tokens.id_token);
|
|
168
|
+
return {
|
|
169
|
+
authMode: safeString(authPayload.auth_mode || 'chatgpt') || 'chatgpt',
|
|
170
|
+
accessToken: safeString(tokens.access_token),
|
|
171
|
+
refreshToken: safeString(tokens.refresh_token),
|
|
172
|
+
idToken: safeString(tokens.id_token),
|
|
173
|
+
accountId: safeString(tokens.account_id),
|
|
174
|
+
accountEmail: safeString(idTokenPayload?.email),
|
|
175
|
+
lastRefresh: authPayload.last_refresh || null,
|
|
176
|
+
primaryToken: safeString(tokens.access_token)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseGeminiImport(rawText) {
|
|
181
|
+
const parsed = tryParseJson(rawText);
|
|
182
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
183
|
+
throw new Error('Gemini OAuth 仅支持 JSON 导入。');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const payload = parsed.token && typeof parsed.token === 'object'
|
|
187
|
+
? parsed
|
|
188
|
+
: parsed.access_token
|
|
189
|
+
? {
|
|
190
|
+
token: {
|
|
191
|
+
accessToken: parsed.access_token,
|
|
192
|
+
refreshToken: parsed.refresh_token || '',
|
|
193
|
+
tokenType: parsed.token_type || 'Bearer',
|
|
194
|
+
scope: parsed.scope || '',
|
|
195
|
+
expiresAt: parsed.expiry_date || null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
: null;
|
|
199
|
+
|
|
200
|
+
if (!payload?.token?.accessToken) {
|
|
201
|
+
throw new Error('Gemini OAuth 导入缺少 access_token。');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
accessToken: safeString(payload.token.accessToken),
|
|
206
|
+
refreshToken: safeString(payload.token.refreshToken),
|
|
207
|
+
tokenType: safeString(payload.token.tokenType || 'Bearer') || 'Bearer',
|
|
208
|
+
scope: safeString(payload.token.scope),
|
|
209
|
+
expiresAt: safeNumber(payload.token.expiresAt),
|
|
210
|
+
accountEmail: safeString(parsed.accountEmail || parsed.email),
|
|
211
|
+
primaryToken: safeString(payload.token.accessToken)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseOpenCodeImport(rawText) {
|
|
216
|
+
const parsed = tryParseJson(rawText);
|
|
217
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
218
|
+
throw new Error('OpenCode OAuth 仅支持 JSON 导入。');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const oauthEntry = parsed.openai && typeof parsed.openai === 'object'
|
|
222
|
+
? ['openai', parsed.openai]
|
|
223
|
+
: Object.entries(parsed).find(([, value]) => value && typeof value === 'object' && value.type === 'oauth')
|
|
224
|
+
|| [parsed.providerId || 'openai', parsed];
|
|
225
|
+
const providerId = safeString(oauthEntry[0]) || 'openai';
|
|
226
|
+
const payload = oauthEntry[1];
|
|
227
|
+
if (payload.type !== 'oauth' && !payload.access) {
|
|
228
|
+
throw new Error('OpenCode OAuth 导入缺少 access 或 openai.oauth 结构。');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
providerId,
|
|
233
|
+
accessToken: safeString(payload.access),
|
|
234
|
+
refreshToken: safeString(payload.refresh),
|
|
235
|
+
expiresAt: safeNumber(payload.expires),
|
|
236
|
+
accountId: safeString(payload.accountId),
|
|
237
|
+
enterpriseUrl: safeString(payload.enterpriseUrl),
|
|
238
|
+
primaryToken: safeString(payload.access)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function parseCredentialInput(tool, rawText) {
|
|
243
|
+
switch (tool) {
|
|
244
|
+
case 'claude':
|
|
245
|
+
return parseClaudeImport(rawText);
|
|
246
|
+
case 'codex':
|
|
247
|
+
return parseCodexImport(rawText);
|
|
248
|
+
case 'gemini':
|
|
249
|
+
return parseGeminiImport(rawText);
|
|
250
|
+
case 'opencode':
|
|
251
|
+
return parseOpenCodeImport(rawText);
|
|
252
|
+
default:
|
|
253
|
+
throw new Error(`Unsupported OAuth tool: ${tool}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function buildCredentialName(tool, metadata, providedName = '') {
|
|
258
|
+
const explicit = safeString(providedName);
|
|
259
|
+
if (explicit) {
|
|
260
|
+
return explicit;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (tool === 'opencode' && safeString(metadata.providerId)) {
|
|
264
|
+
const accountLabel = safeString(metadata.accountId || metadata.accountEmail);
|
|
265
|
+
return accountLabel
|
|
266
|
+
? `${tool} - ${metadata.providerId} - ${accountLabel}`
|
|
267
|
+
: `${tool} - ${metadata.providerId}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const accountLabel = safeString(metadata.accountId || metadata.accountEmail);
|
|
271
|
+
if (accountLabel) {
|
|
272
|
+
return `${tool} - ${accountLabel}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return `${tool} - ${new Date().toISOString().slice(0, 10)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function sanitizeCredential(entry, defaultCredentialId) {
|
|
279
|
+
const primaryToken = entry?.secrets?.primaryToken
|
|
280
|
+
|| entry?.secrets?.accessToken
|
|
281
|
+
|| entry?.secrets?.token
|
|
282
|
+
|| '';
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
id: entry.id,
|
|
286
|
+
tool: entry.tool,
|
|
287
|
+
name: entry.name,
|
|
288
|
+
source: entry.source,
|
|
289
|
+
storage: entry.storage || '',
|
|
290
|
+
providerId: entry.providerId || '',
|
|
291
|
+
accountId: entry.accountId || '',
|
|
292
|
+
accountEmail: entry.accountEmail || '',
|
|
293
|
+
expiresAt: entry.expiresAt || null,
|
|
294
|
+
lastRefresh: entry.lastRefresh || null,
|
|
295
|
+
lastUsedAt: entry.lastUsedAt || null,
|
|
296
|
+
createdAt: entry.createdAt,
|
|
297
|
+
updatedAt: entry.updatedAt,
|
|
298
|
+
tokenPreview: maskToken(primaryToken),
|
|
299
|
+
isDefault: defaultCredentialId === entry.id
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function sanitizeToolSummary(tool, toolStore) {
|
|
304
|
+
const credentials = (toolStore.credentials || [])
|
|
305
|
+
.map((entry) => sanitizeCredential(entry, toolStore.defaultCredentialId))
|
|
306
|
+
.sort((a, b) => {
|
|
307
|
+
const aTime = a.lastUsedAt || 0;
|
|
308
|
+
const bTime = b.lastUsedAt || 0;
|
|
309
|
+
if (aTime !== bTime) return bTime - aTime;
|
|
310
|
+
return (b.createdAt || 0) - (a.createdAt || 0);
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
tool,
|
|
314
|
+
defaultCredentialId: toolStore.defaultCredentialId || null,
|
|
315
|
+
credentials,
|
|
316
|
+
nativeState: inspectTool(tool)
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getToolStore(store, tool) {
|
|
321
|
+
assertSupportedTool(tool);
|
|
322
|
+
if (!store.tools[tool]) {
|
|
323
|
+
store.tools[tool] = { defaultCredentialId: null, credentials: [] };
|
|
324
|
+
}
|
|
325
|
+
return store.tools[tool];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function extractSecrets(tool, metadata) {
|
|
329
|
+
// 只保留真正的 secret 字段,不污染非敏感数据
|
|
330
|
+
switch (tool) {
|
|
331
|
+
case 'claude':
|
|
332
|
+
return {
|
|
333
|
+
accessToken: metadata.accessToken || '',
|
|
334
|
+
refreshToken: metadata.refreshToken || '',
|
|
335
|
+
expiresAt: metadata.expiresAt || null,
|
|
336
|
+
primaryToken: metadata.primaryToken || metadata.accessToken || ''
|
|
337
|
+
};
|
|
338
|
+
case 'codex':
|
|
339
|
+
return {
|
|
340
|
+
authMode: metadata.authMode || 'chatgpt',
|
|
341
|
+
accessToken: metadata.accessToken || '',
|
|
342
|
+
refreshToken: metadata.refreshToken || '',
|
|
343
|
+
idToken: metadata.idToken || '',
|
|
344
|
+
accountId: metadata.accountId || '',
|
|
345
|
+
lastRefresh: metadata.lastRefresh || null,
|
|
346
|
+
primaryToken: metadata.primaryToken || metadata.accessToken || ''
|
|
347
|
+
};
|
|
348
|
+
case 'gemini':
|
|
349
|
+
return {
|
|
350
|
+
accessToken: metadata.accessToken || '',
|
|
351
|
+
refreshToken: metadata.refreshToken || '',
|
|
352
|
+
tokenType: metadata.tokenType || 'Bearer',
|
|
353
|
+
scope: metadata.scope || '',
|
|
354
|
+
expiresAt: metadata.expiresAt || null,
|
|
355
|
+
primaryToken: metadata.primaryToken || metadata.accessToken || ''
|
|
356
|
+
};
|
|
357
|
+
case 'opencode':
|
|
358
|
+
return {
|
|
359
|
+
accessToken: metadata.accessToken || '',
|
|
360
|
+
refreshToken: metadata.refreshToken || '',
|
|
361
|
+
expiresAt: metadata.expiresAt || null,
|
|
362
|
+
accountId: metadata.accountId || '',
|
|
363
|
+
enterpriseUrl: metadata.enterpriseUrl || '',
|
|
364
|
+
primaryToken: metadata.primaryToken || metadata.accessToken || ''
|
|
365
|
+
};
|
|
366
|
+
default:
|
|
367
|
+
return { primaryToken: metadata.primaryToken || metadata.accessToken || '' };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function stableFingerprintValue(tool, metadata) {
|
|
372
|
+
// 优先使用稳定标识符,避免 access token 轮换导致重复记录
|
|
373
|
+
const stableId = metadata.accountEmail
|
|
374
|
+
|| metadata.accountId
|
|
375
|
+
|| (tool === 'opencode' ? metadata.providerId : '')
|
|
376
|
+
|| metadata.refreshToken
|
|
377
|
+
|| metadata.primaryToken
|
|
378
|
+
|| metadata.accessToken
|
|
379
|
+
|| '';
|
|
380
|
+
return stableId;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function upsertCredential(tool, metadata, options = {}) {
|
|
384
|
+
const store = readStore();
|
|
385
|
+
const toolStore = getToolStore(store, tool);
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
const primaryToken = metadata.primaryToken || metadata.accessToken || '';
|
|
388
|
+
const fingerprint = fingerprintFor(tool, stableFingerprintValue(tool, metadata));
|
|
389
|
+
const existingIndex = toolStore.credentials.findIndex((item) => item.fingerprint === fingerprint);
|
|
390
|
+
const existing = existingIndex >= 0 ? toolStore.credentials[existingIndex] : null;
|
|
391
|
+
|
|
392
|
+
const entry = {
|
|
393
|
+
id: existing?.id || crypto.randomUUID(),
|
|
394
|
+
tool,
|
|
395
|
+
name: buildCredentialName(tool, metadata, options.name),
|
|
396
|
+
source: options.source || existing?.source || 'manual',
|
|
397
|
+
storage: metadata.storage || existing?.storage || '',
|
|
398
|
+
providerId: metadata.providerId || existing?.providerId || '',
|
|
399
|
+
accountId: metadata.accountId || existing?.accountId || '',
|
|
400
|
+
accountEmail: metadata.accountEmail || existing?.accountEmail || '',
|
|
401
|
+
expiresAt: metadata.expiresAt || existing?.expiresAt || null,
|
|
402
|
+
lastRefresh: metadata.lastRefresh || existing?.lastRefresh || null,
|
|
403
|
+
createdAt: existing?.createdAt || now,
|
|
404
|
+
updatedAt: now,
|
|
405
|
+
fingerprint,
|
|
406
|
+
secrets: extractSecrets(tool, { ...existing?.secrets, ...metadata, primaryToken })
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
if (existingIndex >= 0) {
|
|
410
|
+
toolStore.credentials.splice(existingIndex, 1, entry);
|
|
411
|
+
} else {
|
|
412
|
+
toolStore.credentials.unshift(entry);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!toolStore.defaultCredentialId) {
|
|
416
|
+
toolStore.defaultCredentialId = entry.id;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
writeStore(store);
|
|
420
|
+
return sanitizeCredential(entry, toolStore.defaultCredentialId);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getAllToolSummaries() {
|
|
424
|
+
const store = readStore();
|
|
425
|
+
return Object.fromEntries(SUPPORTED_TOOLS.map((tool) => {
|
|
426
|
+
const toolStore = getToolStore(store, tool);
|
|
427
|
+
return [tool, sanitizeToolSummary(tool, toolStore)];
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function getToolSummary(tool) {
|
|
432
|
+
const store = readStore();
|
|
433
|
+
const toolStore = getToolStore(store, tool);
|
|
434
|
+
return sanitizeToolSummary(tool, toolStore);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function importCredential(tool, payload = {}) {
|
|
438
|
+
assertSupportedTool(tool);
|
|
439
|
+
const raw = safeString(payload.raw);
|
|
440
|
+
if (!raw) {
|
|
441
|
+
throw new Error('缺少导入内容。');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const metadata = parseCredentialInput(tool, raw);
|
|
445
|
+
return upsertCredential(tool, metadata, {
|
|
446
|
+
name: payload.name,
|
|
447
|
+
source: payload.source || 'manual'
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function syncLocalCredential(tool) {
|
|
452
|
+
assertSupportedTool(tool);
|
|
453
|
+
const nativeCredentials = readAllNativeOAuth(tool);
|
|
454
|
+
if (!nativeCredentials.length) {
|
|
455
|
+
throw new Error('当前本地未检测到可同步的 OAuth 凭证。');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const credentials = nativeCredentials.map((metadata) => upsertCredential(tool, metadata, {
|
|
459
|
+
source: 'synced-local'
|
|
460
|
+
}));
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
credential: credentials[0] || null,
|
|
464
|
+
credentials,
|
|
465
|
+
summary: getToolSummary(tool)
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function setDefaultCredential(tool, credentialId) {
|
|
470
|
+
const store = readStore();
|
|
471
|
+
const toolStore = getToolStore(store, tool);
|
|
472
|
+
const target = toolStore.credentials.find((item) => item.id === credentialId);
|
|
473
|
+
if (!target) {
|
|
474
|
+
throw new Error('OAuth 凭证不存在。');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
toolStore.defaultCredentialId = credentialId;
|
|
478
|
+
writeStore(store);
|
|
479
|
+
return sanitizeToolSummary(tool, toolStore);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function deleteCredential(tool, credentialId) {
|
|
483
|
+
const store = readStore();
|
|
484
|
+
const toolStore = getToolStore(store, tool);
|
|
485
|
+
const nextCredentials = toolStore.credentials.filter((item) => item.id !== credentialId);
|
|
486
|
+
if (nextCredentials.length === toolStore.credentials.length) {
|
|
487
|
+
throw new Error('OAuth 凭证不存在。');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
toolStore.credentials = nextCredentials;
|
|
491
|
+
if (toolStore.defaultCredentialId === credentialId) {
|
|
492
|
+
toolStore.defaultCredentialId = nextCredentials[0]?.id || null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
writeStore(store);
|
|
496
|
+
return sanitizeToolSummary(tool, toolStore);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function findStoredCredential(tool, credentialId) {
|
|
500
|
+
const store = readStore();
|
|
501
|
+
const toolStore = getToolStore(store, tool);
|
|
502
|
+
const entry = toolStore.credentials.find((item) => item.id === credentialId);
|
|
503
|
+
if (!entry) {
|
|
504
|
+
throw new Error('OAuth 凭证不存在。');
|
|
505
|
+
}
|
|
506
|
+
return entry;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function cleanupManagedArtifacts(tool) {
|
|
510
|
+
removeFileIfExists(PATHS.activeChannel?.[tool]);
|
|
511
|
+
|
|
512
|
+
if (tool === 'claude') {
|
|
513
|
+
claudeSettingsManager.deleteBackup?.();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (tool === 'codex') {
|
|
518
|
+
codexSettingsManager.deleteBackup?.();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (tool === 'gemini') {
|
|
523
|
+
geminiSettingsManager.deleteBackup?.();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (tool === 'opencode') {
|
|
528
|
+
opencodeSettingsManager.deleteBackup?.();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function stopProxyIfRunning(tool) {
|
|
533
|
+
switch (tool) {
|
|
534
|
+
case 'claude': {
|
|
535
|
+
const { stopProxyServer } = require('../proxy-server');
|
|
536
|
+
const { getProxyStatus } = require('../proxy-server');
|
|
537
|
+
if (getProxyStatus().running) {
|
|
538
|
+
await stopProxyServer();
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
case 'codex': {
|
|
544
|
+
const { stopCodexProxyServer, getCodexProxyStatus } = require('../codex-proxy-server');
|
|
545
|
+
if (getCodexProxyStatus().running) {
|
|
546
|
+
await stopCodexProxyServer();
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
case 'gemini': {
|
|
552
|
+
const { stopGeminiProxyServer, getGeminiProxyStatus } = require('../gemini-proxy-server');
|
|
553
|
+
if (getGeminiProxyStatus().running) {
|
|
554
|
+
await stopGeminiProxyServer();
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
case 'opencode': {
|
|
560
|
+
const { stopOpenCodeProxyServer, getOpenCodeProxyStatus } = require('../opencode-proxy-server');
|
|
561
|
+
if (getOpenCodeProxyStatus().running) {
|
|
562
|
+
await stopOpenCodeProxyServer();
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
default:
|
|
568
|
+
throw new Error(`Unsupported OAuth tool: ${tool}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function disableAllChannelsForTool(tool) {
|
|
573
|
+
try {
|
|
574
|
+
switch (tool) {
|
|
575
|
+
case 'claude': {
|
|
576
|
+
const { disableAllChannels } = require('./channels');
|
|
577
|
+
disableAllChannels();
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case 'codex': {
|
|
581
|
+
const { disableAllChannels } = require('./codex-channels');
|
|
582
|
+
disableAllChannels();
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case 'gemini': {
|
|
586
|
+
const { disableAllChannels } = require('./gemini-channels');
|
|
587
|
+
disableAllChannels();
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
case 'opencode': {
|
|
591
|
+
const { disableAllChannels } = require('./opencode-channels');
|
|
592
|
+
disableAllChannels();
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
} catch (err) {
|
|
597
|
+
console.warn(`[OAuth] Failed to disable channels for ${tool}:`, err.message);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function applyStoredCredential(tool, credentialId) {
|
|
602
|
+
const entry = findStoredCredential(tool, credentialId);
|
|
603
|
+
const proxyStopped = await stopProxyIfRunning(tool);
|
|
604
|
+
cleanupManagedArtifacts(tool);
|
|
605
|
+
disableAllChannelsForTool(tool);
|
|
606
|
+
applyOAuthCredential(tool, entry.secrets);
|
|
607
|
+
|
|
608
|
+
// 记录最近使用时间
|
|
609
|
+
const store = readStore();
|
|
610
|
+
const toolStore = getToolStore(store, tool);
|
|
611
|
+
const stored = toolStore.credentials.find((item) => item.id === credentialId);
|
|
612
|
+
if (stored) {
|
|
613
|
+
stored.lastUsedAt = Date.now();
|
|
614
|
+
writeStore(store);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return {
|
|
618
|
+
proxyStopped,
|
|
619
|
+
credential: sanitizeCredential(entry, readStore().tools[tool]?.defaultCredentialId || null),
|
|
620
|
+
toolSummary: getToolSummary(tool)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function clearNativeOAuthState(tool) {
|
|
625
|
+
assertSupportedTool(tool);
|
|
626
|
+
clearNativeOAuth(tool);
|
|
627
|
+
return inspectTool(tool);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function httpGet(url, headers = {}) {
|
|
631
|
+
return new Promise((resolve, reject) => {
|
|
632
|
+
const https = require('https');
|
|
633
|
+
const urlObj = new URL(url);
|
|
634
|
+
const options = {
|
|
635
|
+
hostname: urlObj.hostname,
|
|
636
|
+
path: urlObj.pathname + urlObj.search,
|
|
637
|
+
method: 'GET',
|
|
638
|
+
headers,
|
|
639
|
+
timeout: 15000
|
|
640
|
+
};
|
|
641
|
+
const req = https.request(options, (res) => {
|
|
642
|
+
let body = '';
|
|
643
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
644
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body }));
|
|
645
|
+
});
|
|
646
|
+
req.on('error', reject);
|
|
647
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
|
|
648
|
+
req.end();
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function httpPost(url, body, headers = {}) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
const https = require('https');
|
|
655
|
+
const urlObj = new URL(url);
|
|
656
|
+
const options = {
|
|
657
|
+
hostname: urlObj.hostname,
|
|
658
|
+
path: urlObj.pathname + urlObj.search,
|
|
659
|
+
method: 'POST',
|
|
660
|
+
headers: { ...headers, 'Content-Length': Buffer.byteLength(body) },
|
|
661
|
+
timeout: 15000
|
|
662
|
+
};
|
|
663
|
+
const req = https.request(options, (res) => {
|
|
664
|
+
let data = '';
|
|
665
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
666
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
|
|
667
|
+
});
|
|
668
|
+
req.on('error', reject);
|
|
669
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('请求超时')); });
|
|
670
|
+
req.write(body);
|
|
671
|
+
req.end();
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
async function fetchClaudeUsage(accessToken) {
|
|
676
|
+
try {
|
|
677
|
+
const result = await httpGet('https://api.anthropic.com/api/oauth/usage', {
|
|
678
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
679
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
680
|
+
'anthropic-version': '2023-06-01',
|
|
681
|
+
'User-Agent': 'claude-cli/1.0'
|
|
682
|
+
});
|
|
683
|
+
const data = JSON.parse(result.body);
|
|
684
|
+
return { raw: data, provider: 'claude', statusCode: result.statusCode };
|
|
685
|
+
} catch (err) {
|
|
686
|
+
return { error: err.message, provider: 'claude' };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function fetchCodexUsage(accessToken) {
|
|
691
|
+
// Codex uses JWT id_token; decode it to extract user info directly
|
|
692
|
+
try {
|
|
693
|
+
const { decodeJwtPayload } = require('./oauth-utils');
|
|
694
|
+
const payload = decodeJwtPayload(accessToken);
|
|
695
|
+
if (payload && (payload.email || payload.sub)) {
|
|
696
|
+
return {
|
|
697
|
+
raw: {
|
|
698
|
+
email: payload.email || '',
|
|
699
|
+
accountId: payload.sub || '',
|
|
700
|
+
name: payload.name || ''
|
|
701
|
+
},
|
|
702
|
+
provider: 'codex',
|
|
703
|
+
statusCode: 200
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
} catch (_) {
|
|
707
|
+
// fall through to API call
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const result = await httpGet('https://api.openai.com/v1/me', {
|
|
711
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
712
|
+
'User-Agent': 'openai-node/4.0.0'
|
|
713
|
+
});
|
|
714
|
+
const data = JSON.parse(result.body);
|
|
715
|
+
return { raw: data, provider: 'codex', statusCode: result.statusCode };
|
|
716
|
+
} catch (err) {
|
|
717
|
+
return { error: err.message, provider: 'codex' };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function fetchGeminiUsage(accessToken) {
|
|
722
|
+
try {
|
|
723
|
+
const body = JSON.stringify({
|
|
724
|
+
metadata: {
|
|
725
|
+
ideType: 'ANTIGRAVITY',
|
|
726
|
+
platform: 'PLATFORM_UNSPECIFIED',
|
|
727
|
+
pluginType: 'GEMINI'
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
const result = await httpPost('https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist', body, {
|
|
731
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
732
|
+
'Content-Type': 'application/json',
|
|
733
|
+
'User-Agent': 'google-api-nodejs-client/9.15.1',
|
|
734
|
+
'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
|
|
735
|
+
'Client-Metadata': '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}'
|
|
736
|
+
});
|
|
737
|
+
const data = JSON.parse(result.body);
|
|
738
|
+
return { raw: data, provider: 'gemini', statusCode: result.statusCode };
|
|
739
|
+
} catch (err) {
|
|
740
|
+
return { error: err.message, provider: 'gemini' };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function fetchCredentialUsage(tool, credentialId) {
|
|
745
|
+
const entry = findStoredCredential(tool, credentialId);
|
|
746
|
+
const secrets = entry.secrets || {};
|
|
747
|
+
const accessToken = secrets.accessToken || secrets.primaryToken || '';
|
|
748
|
+
|
|
749
|
+
if (!accessToken) {
|
|
750
|
+
return { error: '无有效 token' };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
switch (tool) {
|
|
754
|
+
case 'claude':
|
|
755
|
+
return await fetchClaudeUsage(accessToken);
|
|
756
|
+
case 'codex':
|
|
757
|
+
return await fetchCodexUsage(secrets.idToken || accessToken);
|
|
758
|
+
case 'gemini':
|
|
759
|
+
return await fetchGeminiUsage(accessToken);
|
|
760
|
+
case 'opencode': {
|
|
761
|
+
const providerId = entry.providerId || 'openai';
|
|
762
|
+
if (providerId.includes('claude') || providerId.includes('anthropic')) {
|
|
763
|
+
return await fetchClaudeUsage(accessToken);
|
|
764
|
+
} else if (providerId.includes('gemini') || providerId.includes('google')) {
|
|
765
|
+
return await fetchGeminiUsage(accessToken);
|
|
766
|
+
} else {
|
|
767
|
+
return await fetchCodexUsage(accessToken);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
default:
|
|
771
|
+
return { error: `不支持的工具: ${tool}` };
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
module.exports = {
|
|
776
|
+
SUPPORTED_TOOLS,
|
|
777
|
+
getAllToolSummaries,
|
|
778
|
+
getToolSummary,
|
|
779
|
+
importCredential,
|
|
780
|
+
syncLocalCredential,
|
|
781
|
+
setDefaultCredential,
|
|
782
|
+
deleteCredential,
|
|
783
|
+
applyStoredCredential,
|
|
784
|
+
clearNativeOAuthState,
|
|
785
|
+
fetchCredentialUsage
|
|
786
|
+
};
|