coding-tool-x 3.3.7 → 3.3.9
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 +20 -0
- package/README.md +253 -326
- package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/Home-D7KX7iF8.js +1 -0
- package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
- package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
- package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
- package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
- package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
- package/dist/web/assets/index-CL-qpoJ_.js +2 -0
- package/dist/web/assets/index-D_5dRFOL.css +1 -0
- package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
- package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
- package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
- package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
- package/dist/web/index.html +7 -7
- package/docs/home.png +0 -0
- package/package.json +14 -5
- package/src/commands/daemon.js +3 -2
- package/src/commands/security.js +1 -2
- package/src/commands/toggle-proxy.js +100 -5
- package/src/config/paths.js +718 -90
- package/src/server/api/agents.js +1 -1
- package/src/server/api/channels.js +9 -0
- package/src/server/api/claude-hooks.js +13 -8
- package/src/server/api/codex-channels.js +9 -0
- package/src/server/api/codex-proxy.js +27 -15
- package/src/server/api/gemini-proxy.js +22 -11
- package/src/server/api/hooks.js +45 -0
- package/src/server/api/oauth-credentials.js +163 -0
- package/src/server/api/opencode-proxy.js +22 -10
- package/src/server/api/plugins.js +2 -1
- package/src/server/api/proxy.js +39 -44
- package/src/server/api/skills.js +91 -13
- package/src/server/api/ui-config.js +5 -0
- package/src/server/codex-proxy-server.js +90 -70
- package/src/server/gemini-proxy-server.js +107 -88
- package/src/server/index.js +2 -0
- package/src/server/opencode-proxy-server.js +381 -225
- package/src/server/proxy-server.js +86 -60
- package/src/server/services/alias.js +3 -3
- package/src/server/services/channels.js +21 -24
- package/src/server/services/codex-channels.js +158 -255
- package/src/server/services/codex-config.js +2 -5
- package/src/server/services/codex-env-manager.js +423 -0
- package/src/server/services/codex-settings-manager.js +21 -357
- package/src/server/services/codex-statistics-service.js +3 -27
- package/src/server/services/config-export-service.js +43 -9
- package/src/server/services/config-registry-service.js +3 -2
- package/src/server/services/config-sync-manager.js +1 -1
- package/src/server/services/favorites.js +4 -3
- package/src/server/services/gemini-channels.js +14 -12
- package/src/server/services/gemini-statistics-service.js +3 -25
- package/src/server/services/mcp-service.js +35 -19
- package/src/server/services/model-detector.js +4 -3
- package/src/server/services/native-keychain.js +243 -0
- package/src/server/services/native-oauth-adapters.js +891 -0
- package/src/server/services/network-access.js +39 -1
- package/src/server/services/notification-hooks.js +951 -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 +19 -15
- package/src/server/services/opencode-sessions.js +2 -2
- package/src/server/services/opencode-settings-manager.js +169 -16
- package/src/server/services/opencode-statistics-service.js +3 -27
- package/src/server/services/plugins-service.js +115 -15
- package/src/server/services/prompts-service.js +2 -3
- package/src/server/services/proxy-log-helper.js +242 -0
- package/src/server/services/proxy-runtime.js +6 -4
- package/src/server/services/repo-scanner-base.js +12 -4
- package/src/server/services/request-logger.js +7 -7
- package/src/server/services/security-config.js +4 -4
- package/src/server/services/session-cache.js +2 -2
- package/src/server/services/sessions.js +2 -2
- package/src/server/services/settings-manager.js +13 -0
- package/src/server/services/skill-service.js +867 -368
- package/src/server/services/statistics-service.js +5 -5
- package/src/server/services/ui-config.js +4 -3
- package/src/server/services/workspace-service.js +1 -1
- package/src/server/websocket-server.js +5 -4
- 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
- package/docs/bannel.png +0 -0
- package/docs/model-redirection.md +0 -251
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
function maskToken(value) {
|
|
5
|
+
const token = String(value || '').trim();
|
|
6
|
+
if (!token) {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
if (token.length <= 8) {
|
|
10
|
+
return `${token.slice(0, 2)}***`;
|
|
11
|
+
}
|
|
12
|
+
return `${token.slice(0, 4)}...${token.slice(-4)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function decodeJwtPayload(token) {
|
|
16
|
+
if (typeof token !== 'string' || !token.includes('.')) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const payload = token.split('.')[1];
|
|
22
|
+
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
23
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
|
|
24
|
+
return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function removeFileIfExists(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
33
|
+
fs.unlinkSync(filePath);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore cleanup failures
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sha256(value) {
|
|
41
|
+
return crypto.createHash('sha256').update(String(value || ''), 'utf8').digest('hex');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
maskToken,
|
|
46
|
+
decodeJwtPayload,
|
|
47
|
+
removeFileIfExists,
|
|
48
|
+
sha256
|
|
49
|
+
};
|
|
@@ -2,6 +2,8 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { PATHS } = require('../../config/paths');
|
|
5
|
+
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
6
|
+
const { setChannelConfig } = require('./opencode-settings-manager');
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* OpenCode 渠道管理服务
|
|
@@ -41,17 +43,17 @@ function normalizeChannelName(value) {
|
|
|
41
43
|
|
|
42
44
|
// 获取渠道存储文件路径
|
|
43
45
|
function getChannelsFilePath() {
|
|
44
|
-
const
|
|
45
|
-
if (!fs.existsSync(
|
|
46
|
-
fs.mkdirSync(
|
|
46
|
+
const channelsDir = path.dirname(PATHS.channels.opencode);
|
|
47
|
+
if (!fs.existsSync(channelsDir)) {
|
|
48
|
+
fs.mkdirSync(channelsDir, { recursive: true });
|
|
47
49
|
}
|
|
48
50
|
return PATHS.channels.opencode;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
function getCodexChannelsFilePath() {
|
|
52
|
-
const
|
|
53
|
-
if (!fs.existsSync(
|
|
54
|
-
fs.mkdirSync(
|
|
54
|
+
const channelsDir = path.dirname(PATHS.channels.codex);
|
|
55
|
+
if (!fs.existsSync(channelsDir)) {
|
|
56
|
+
fs.mkdirSync(channelsDir, { recursive: true });
|
|
55
57
|
}
|
|
56
58
|
return PATHS.channels.codex;
|
|
57
59
|
}
|
|
@@ -189,14 +191,6 @@ function updateChannel(channelId, updates) {
|
|
|
189
191
|
console.log(`[OpenCode Single-channel mode] Enabled "${merged.name}", disabled all others`);
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
// Prevent disabling last enabled channel when proxy is OFF
|
|
193
|
-
if (!isProxyRunning && !merged.enabled && oldChannel.enabled) {
|
|
194
|
-
const enabledCount = data.channels.filter(ch => ch.enabled).length;
|
|
195
|
-
if (enabledCount === 0) {
|
|
196
|
-
throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
194
|
saveChannels(data);
|
|
201
195
|
return data.channels[index];
|
|
202
196
|
}
|
|
@@ -259,6 +253,9 @@ function applyChannelToSettings(channelId) {
|
|
|
259
253
|
});
|
|
260
254
|
saveChannels(data);
|
|
261
255
|
|
|
256
|
+
clearNativeOAuth('opencode');
|
|
257
|
+
setChannelConfig(channel);
|
|
258
|
+
|
|
262
259
|
return channel;
|
|
263
260
|
}
|
|
264
261
|
|
|
@@ -371,6 +368,12 @@ async function getEffectiveApiKey(channel) {
|
|
|
371
368
|
return candidates[0] || null;
|
|
372
369
|
}
|
|
373
370
|
|
|
371
|
+
function disableAllChannels() {
|
|
372
|
+
const data = loadChannels();
|
|
373
|
+
data.channels.forEach(ch => { ch.enabled = false; });
|
|
374
|
+
saveChannels(data);
|
|
375
|
+
}
|
|
376
|
+
|
|
374
377
|
module.exports = {
|
|
375
378
|
getChannels,
|
|
376
379
|
createChannel,
|
|
@@ -380,5 +383,6 @@ module.exports = {
|
|
|
380
383
|
saveChannelOrder,
|
|
381
384
|
applyChannelToSettings,
|
|
382
385
|
getEffectiveApiKey,
|
|
383
|
-
getEffectiveApiKeyCandidates
|
|
386
|
+
getEffectiveApiKeyCandidates,
|
|
387
|
+
disableAllChannels
|
|
384
388
|
};
|
|
@@ -9,8 +9,8 @@ const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
|
9
9
|
* 读取 OpenCode SQLite 会话数据
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const PROJECT_ORDER_FILE =
|
|
13
|
-
const SESSION_ORDER_FILE =
|
|
12
|
+
const PROJECT_ORDER_FILE = PATHS.opencodeProjectOrder;
|
|
13
|
+
const SESSION_ORDER_FILE = PATHS.opencodeSessionOrder;
|
|
14
14
|
const OPENCODE_DB_PATH = path.join(NATIVE_PATHS.opencode.data, 'opencode.db');
|
|
15
15
|
const COUNTS_CACHE_TTL_MS = 30 * 1000;
|
|
16
16
|
const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
|
|
@@ -169,6 +169,11 @@ function isManagedProxyProvider(provider) {
|
|
|
169
169
|
return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
function isManagedChannelProvider(provider) {
|
|
173
|
+
if (!provider || typeof provider !== 'object') return false;
|
|
174
|
+
return provider?.[MANAGED_PROVIDER_MARKER] === true;
|
|
175
|
+
}
|
|
176
|
+
|
|
172
177
|
function isManagedProxyConfig(config) {
|
|
173
178
|
if (!config || typeof config !== 'object') return false;
|
|
174
179
|
// Check legacy single-provider format
|
|
@@ -244,6 +249,92 @@ function resolveProxyBaseUrl(config) {
|
|
|
244
249
|
return '';
|
|
245
250
|
}
|
|
246
251
|
|
|
252
|
+
function collectChannelModelCandidates(channel = {}) {
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const models = [];
|
|
255
|
+
const add = (value) => {
|
|
256
|
+
if (typeof value !== 'string') return;
|
|
257
|
+
const trimmed = value.trim();
|
|
258
|
+
if (!trimmed) return;
|
|
259
|
+
const key = trimmed.toLowerCase();
|
|
260
|
+
if (seen.has(key)) return;
|
|
261
|
+
seen.add(key);
|
|
262
|
+
models.push(trimmed);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
add(channel.model);
|
|
266
|
+
add(channel.speedTestModel);
|
|
267
|
+
|
|
268
|
+
if (Array.isArray(channel.allowedModels)) {
|
|
269
|
+
channel.allowedModels.forEach(add);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (channel.modelConfig && typeof channel.modelConfig === 'object') {
|
|
273
|
+
add(channel.modelConfig.model);
|
|
274
|
+
add(channel.modelConfig.opusModel);
|
|
275
|
+
add(channel.modelConfig.sonnetModel);
|
|
276
|
+
add(channel.modelConfig.haikuModel);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (Array.isArray(channel.modelRedirects)) {
|
|
280
|
+
channel.modelRedirects.forEach((rule) => {
|
|
281
|
+
add(rule?.from);
|
|
282
|
+
add(rule?.to);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return models;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function clearManagedProviders(config) {
|
|
290
|
+
const removedProviderKeys = [];
|
|
291
|
+
|
|
292
|
+
if (!config?.provider || typeof config.provider !== 'object') {
|
|
293
|
+
return removedProviderKeys;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isLegacyProxyProvider(config.provider[LEGACY_PROVIDER_ID])) {
|
|
297
|
+
delete config.provider[LEGACY_PROVIDER_ID];
|
|
298
|
+
removedProviderKeys.push(LEGACY_PROVIDER_ID);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (isManagedProxyProvider(config.provider[PROXY_PROVIDER_ID])) {
|
|
302
|
+
delete config.provider[PROXY_PROVIDER_ID];
|
|
303
|
+
removedProviderKeys.push(PROXY_PROVIDER_ID);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
Object.keys(config.provider).forEach((key) => {
|
|
307
|
+
const provider = config.provider[key];
|
|
308
|
+
if (isManagedProxyProvider(provider) || isManagedChannelProvider(provider)) {
|
|
309
|
+
delete config.provider[key];
|
|
310
|
+
removedProviderKeys.push(key);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (Object.keys(config.provider).length === 0) {
|
|
315
|
+
delete config.provider;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return removedProviderKeys;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function clearManagedModelRef(config, removedProviderKeys = []) {
|
|
322
|
+
const modelRef = String(config?.model || '').trim();
|
|
323
|
+
if (!modelRef) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (isOldManagedModelRef(modelRef)) {
|
|
328
|
+
delete config.model;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const providerId = modelRef.includes('/') ? modelRef.split('/')[0] : '';
|
|
333
|
+
if (providerId && removedProviderKeys.includes(providerId)) {
|
|
334
|
+
delete config.model;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
247
338
|
function backupConfig(filePath) {
|
|
248
339
|
ensureConfigDir();
|
|
249
340
|
const backupPath = getBackupPath(filePath);
|
|
@@ -324,23 +415,13 @@ function setProxyConfig(proxyPort, options = {}) {
|
|
|
324
415
|
if (!next.provider || typeof next.provider !== 'object') {
|
|
325
416
|
next.provider = {};
|
|
326
417
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
delete next.provider[LEGACY_PROVIDER_ID];
|
|
330
|
-
}
|
|
331
|
-
if (Object.prototype.hasOwnProperty.call(next.provider[LEGACY_PROVIDER_ID] || {}, 'model')) {
|
|
332
|
-
delete next.provider[LEGACY_PROVIDER_ID].model;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Remove old single ctx-proxy provider (superseded by per-channel providers)
|
|
336
|
-
delete next.provider[PROXY_PROVIDER_ID];
|
|
418
|
+
const removedProviderKeys = clearManagedProviders(next);
|
|
419
|
+
clearManagedModelRef(next, removedProviderKeys);
|
|
337
420
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
});
|
|
421
|
+
// clearManagedProviders 可能在清空所有 provider 后 delete next.provider,重新确保存在
|
|
422
|
+
if (!next.provider || typeof next.provider !== 'object') {
|
|
423
|
+
next.provider = {};
|
|
424
|
+
}
|
|
344
425
|
|
|
345
426
|
const channels = Array.isArray(options.channels) ? options.channels : null;
|
|
346
427
|
|
|
@@ -427,6 +508,73 @@ function setProxyConfig(proxyPort, options = {}) {
|
|
|
427
508
|
return { success: true, port: proxyPort, path: filePath };
|
|
428
509
|
}
|
|
429
510
|
|
|
511
|
+
function setChannelConfig(channel = {}) {
|
|
512
|
+
const filePath = selectConfigPath();
|
|
513
|
+
backupConfig(filePath);
|
|
514
|
+
|
|
515
|
+
const current = readConfig(filePath);
|
|
516
|
+
const next = (current && typeof current === 'object') ? current : {};
|
|
517
|
+
|
|
518
|
+
if (!next.provider || typeof next.provider !== 'object') {
|
|
519
|
+
next.provider = {};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const removedProviderKeys = clearManagedProviders(next);
|
|
523
|
+
clearManagedModelRef(next, removedProviderKeys);
|
|
524
|
+
|
|
525
|
+
const baseProviderKey = sanitizeProviderKey(channel.providerKey || channel.name || 'ctx-channel');
|
|
526
|
+
let providerKey = baseProviderKey;
|
|
527
|
+
let suffix = 2;
|
|
528
|
+
while (next.provider[providerKey] && !isManagedChannelProvider(next.provider[providerKey])) {
|
|
529
|
+
providerKey = `${baseProviderKey}-${suffix}`;
|
|
530
|
+
suffix += 1;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const modelCandidates = collectChannelModelCandidates(channel);
|
|
534
|
+
const fallbackModel = String(channel.model || channel.speedTestModel || modelCandidates[0] || '').trim();
|
|
535
|
+
const modelsMap = buildModelsMap(modelCandidates, fallbackModel);
|
|
536
|
+
const modelIds = Object.keys(modelsMap);
|
|
537
|
+
|
|
538
|
+
if (modelIds.length === 0) {
|
|
539
|
+
throw new Error('OpenCode 渠道缺少可写入的模型,请至少配置默认模型或可用模型。');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
next.provider[providerKey] = {
|
|
543
|
+
[MANAGED_PROVIDER_MARKER]: true,
|
|
544
|
+
npm: '@ai-sdk/openai-compatible',
|
|
545
|
+
name: channel.name || providerKey,
|
|
546
|
+
options: {
|
|
547
|
+
baseURL: String(channel.baseUrl || '').trim(),
|
|
548
|
+
apiKey: String(channel.apiKey || '').trim()
|
|
549
|
+
},
|
|
550
|
+
models: modelsMap
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const topModel = normalizeOpenCodeModel(fallbackModel || modelIds[0], providerKey);
|
|
554
|
+
if (topModel) {
|
|
555
|
+
next.model = topModel;
|
|
556
|
+
} else {
|
|
557
|
+
clearManagedModelRef(next, [providerKey]);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
writeConfig(filePath, next);
|
|
561
|
+
return { success: true, path: filePath, providerKey, model: next.model || null };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function clearManagedChannelConfig() {
|
|
565
|
+
const filePath = selectConfigPath();
|
|
566
|
+
if (!fs.existsSync(filePath)) {
|
|
567
|
+
return { success: true, path: filePath };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const current = readConfig(filePath);
|
|
571
|
+
const next = (current && typeof current === 'object') ? current : {};
|
|
572
|
+
const removedProviderKeys = clearManagedProviders(next);
|
|
573
|
+
clearManagedModelRef(next, removedProviderKeys);
|
|
574
|
+
writeConfig(filePath, next);
|
|
575
|
+
return { success: true, path: filePath };
|
|
576
|
+
}
|
|
577
|
+
|
|
430
578
|
function isOldManagedModelRef(modelRef) {
|
|
431
579
|
const s = String(modelRef || '');
|
|
432
580
|
return s.startsWith(`${PROXY_PROVIDER_ID}/`) || s.startsWith(`${LEGACY_PROVIDER_ID}/`);
|
|
@@ -485,7 +633,12 @@ function getCurrentProxyPort() {
|
|
|
485
633
|
module.exports = {
|
|
486
634
|
configExists,
|
|
487
635
|
hasBackup,
|
|
636
|
+
readConfig,
|
|
637
|
+
writeConfig,
|
|
638
|
+
selectConfigPath,
|
|
488
639
|
setProxyConfig,
|
|
640
|
+
setChannelConfig,
|
|
641
|
+
clearManagedChannelConfig,
|
|
489
642
|
restoreSettings,
|
|
490
643
|
deleteBackup,
|
|
491
644
|
isProxyConfig,
|
|
@@ -4,36 +4,12 @@ const {
|
|
|
4
4
|
getDailyStatistics: getSharedDailyStatistics,
|
|
5
5
|
getTodayStatistics: getSharedTodayStatistics
|
|
6
6
|
} = require('./statistics-service');
|
|
7
|
+
const { normalizeUsageTokens, toNumber } = require('./proxy-log-helper');
|
|
7
8
|
|
|
8
9
|
const TOOL_TYPE = 'opencode';
|
|
9
10
|
|
|
10
|
-
function toNumber(value) {
|
|
11
|
-
const num = Number(value);
|
|
12
|
-
return Number.isFinite(num) ? num : 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function normalizeToolTokens(tokens = {}) {
|
|
16
|
-
const input = toNumber(tokens.input);
|
|
17
|
-
const output = toNumber(tokens.output);
|
|
18
|
-
const reasoning = toNumber(tokens.reasoning);
|
|
19
|
-
const cached = toNumber(tokens.cached);
|
|
20
|
-
const cacheCreation = toNumber(tokens.cacheCreation);
|
|
21
|
-
const cacheRead = toNumber(tokens.cacheRead || cached);
|
|
22
|
-
const total = toNumber(tokens.total) || (input + output + reasoning);
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
input,
|
|
26
|
-
output,
|
|
27
|
-
reasoning,
|
|
28
|
-
cached,
|
|
29
|
-
cacheCreation,
|
|
30
|
-
cacheRead,
|
|
31
|
-
total
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
11
|
function toLegacyEntryShape(entry = {}, includeName = false) {
|
|
36
|
-
const normalized =
|
|
12
|
+
const normalized = normalizeUsageTokens(TOOL_TYPE, entry.tokens || {});
|
|
37
13
|
const result = {
|
|
38
14
|
requests: toNumber(entry.requests),
|
|
39
15
|
tokens: {
|
|
@@ -126,7 +102,7 @@ function buildDailyStatistics(sharedDaily = {}, fallbackDate) {
|
|
|
126
102
|
}
|
|
127
103
|
|
|
128
104
|
function recordRequest(requestData = {}) {
|
|
129
|
-
const normalizedTokens =
|
|
105
|
+
const normalizedTokens = normalizeUsageTokens(TOOL_TYPE, requestData.tokens || {});
|
|
130
106
|
return recordSharedRequest({
|
|
131
107
|
...requestData,
|
|
132
108
|
toolType: TOOL_TYPE,
|
|
@@ -10,7 +10,7 @@ const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('
|
|
|
10
10
|
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
11
11
|
const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
|
|
12
12
|
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
13
|
-
const { NATIVE_PATHS,
|
|
13
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
14
14
|
|
|
15
15
|
const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
|
|
16
16
|
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
@@ -106,9 +106,77 @@ function stripJsonComments(input = '') {
|
|
|
106
106
|
class PluginsService {
|
|
107
107
|
constructor(platform = 'claude') {
|
|
108
108
|
this.platform = ['claude', 'opencode'].includes(platform) ? platform : 'claude';
|
|
109
|
-
this.ccToolConfigDir = path.
|
|
109
|
+
this.ccToolConfigDir = path.dirname(PATHS.pluginRepos.claude);
|
|
110
110
|
this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
|
|
111
111
|
this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
|
|
112
|
+
this.marketCachePath = this.platform === 'opencode'
|
|
113
|
+
? PATHS.pluginMarketCache.opencode
|
|
114
|
+
: PATHS.pluginMarketCache.claude;
|
|
115
|
+
this._marketCache = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
clearMarketCache({ removeFile = true } = {}) {
|
|
119
|
+
this._marketCache = null;
|
|
120
|
+
if (removeFile) {
|
|
121
|
+
try {
|
|
122
|
+
if (fs.existsSync(this.marketCachePath)) {
|
|
123
|
+
fs.unlinkSync(this.marketCachePath);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// ignore cache deletion errors
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
loadMarketCacheFromFile() {
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(this.marketCachePath)) {
|
|
134
|
+
const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
|
|
135
|
+
if (Array.isArray(data.plugins)) {
|
|
136
|
+
return data.plugins;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// ignore cache read errors
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
saveMarketCacheToFile(plugins) {
|
|
146
|
+
try {
|
|
147
|
+
this._ensureDir(path.dirname(this.marketCachePath));
|
|
148
|
+
fs.writeFileSync(this.marketCachePath, JSON.stringify({ plugins }), 'utf-8');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// ignore cache write errors
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
prepareMarketPlugins(plugins = []) {
|
|
155
|
+
const preparedPlugins = Array.isArray(plugins)
|
|
156
|
+
? plugins.map(plugin => ({ ...plugin }))
|
|
157
|
+
: [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
const installedPlugins = this.listPlugins().plugins;
|
|
160
|
+
const installedNames = new Set(installedPlugins.map(p => p.name));
|
|
161
|
+
|
|
162
|
+
const deduped = [];
|
|
163
|
+
for (const plugin of preparedPlugins) {
|
|
164
|
+
const key = [
|
|
165
|
+
plugin.name || '',
|
|
166
|
+
plugin.repoOwner || '',
|
|
167
|
+
plugin.repoName || '',
|
|
168
|
+
plugin.directory || plugin.installSource || ''
|
|
169
|
+
].join('::');
|
|
170
|
+
if (seen.has(key)) continue;
|
|
171
|
+
seen.add(key);
|
|
172
|
+
deduped.push({
|
|
173
|
+
...plugin,
|
|
174
|
+
isInstalled: installedNames.has(plugin.name)
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
deduped.sort((a, b) => (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase()));
|
|
179
|
+
return deduped;
|
|
112
180
|
}
|
|
113
181
|
|
|
114
182
|
_ensureDir(dirPath) {
|
|
@@ -777,11 +845,9 @@ class PluginsService {
|
|
|
777
845
|
* @returns {string} Config file path
|
|
778
846
|
*/
|
|
779
847
|
getReposConfigPath() {
|
|
780
|
-
this.
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
return path.join(this.ccToolConfigDir, 'plugin-repos.json');
|
|
848
|
+
const filePath = this._isOpenCode() ? PATHS.pluginRepos.opencode : PATHS.pluginRepos.claude;
|
|
849
|
+
this._ensureDir(path.dirname(filePath));
|
|
850
|
+
return filePath;
|
|
785
851
|
}
|
|
786
852
|
|
|
787
853
|
_getDefaultRepos() {
|
|
@@ -933,6 +999,7 @@ class PluginsService {
|
|
|
933
999
|
|
|
934
1000
|
config.repos.push(newRepo);
|
|
935
1001
|
this.saveReposConfig(config);
|
|
1002
|
+
this.clearMarketCache();
|
|
936
1003
|
|
|
937
1004
|
return config.repos;
|
|
938
1005
|
}
|
|
@@ -947,6 +1014,7 @@ class PluginsService {
|
|
|
947
1014
|
const config = this.loadReposConfig();
|
|
948
1015
|
config.repos = config.repos.filter(r => !(r.owner === owner && r.name === name));
|
|
949
1016
|
this.saveReposConfig(config);
|
|
1017
|
+
this.clearMarketCache();
|
|
950
1018
|
return config.repos;
|
|
951
1019
|
}
|
|
952
1020
|
|
|
@@ -965,6 +1033,7 @@ class PluginsService {
|
|
|
965
1033
|
}
|
|
966
1034
|
repo.enabled = enabled;
|
|
967
1035
|
this.saveReposConfig(config);
|
|
1036
|
+
this.clearMarketCache();
|
|
968
1037
|
return config.repos;
|
|
969
1038
|
}
|
|
970
1039
|
|
|
@@ -1153,9 +1222,30 @@ class PluginsService {
|
|
|
1153
1222
|
* Get market plugins from configured repositories
|
|
1154
1223
|
* @returns {Promise<Array>} List of available market plugins
|
|
1155
1224
|
*/
|
|
1156
|
-
async getMarketPlugins() {
|
|
1225
|
+
async getMarketPlugins(forceRefresh = false) {
|
|
1226
|
+
if (forceRefresh) {
|
|
1227
|
+
this.clearMarketCache({ removeFile: false });
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const fileCache = this.loadMarketCacheFromFile();
|
|
1231
|
+
|
|
1232
|
+
if (!forceRefresh && Array.isArray(this._marketCache) && this._marketCache.length > 0) {
|
|
1233
|
+
if (Array.isArray(fileCache) && fileCache.length > this._marketCache.length) {
|
|
1234
|
+
this._marketCache = this.prepareMarketPlugins(fileCache);
|
|
1235
|
+
return this._marketCache;
|
|
1236
|
+
}
|
|
1237
|
+
this._marketCache = this.prepareMarketPlugins(this._marketCache);
|
|
1238
|
+
return this._marketCache;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (!forceRefresh && Array.isArray(fileCache) && fileCache.length > 0) {
|
|
1242
|
+
this._marketCache = this.prepareMarketPlugins(fileCache);
|
|
1243
|
+
return this._marketCache;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1157
1246
|
const repos = this.getRepos().filter(r => r.enabled);
|
|
1158
1247
|
const marketPlugins = [];
|
|
1248
|
+
let repoFailureCount = 0;
|
|
1159
1249
|
|
|
1160
1250
|
for (const repo of repos) {
|
|
1161
1251
|
try {
|
|
@@ -1248,19 +1338,29 @@ class PluginsService {
|
|
|
1248
1338
|
}
|
|
1249
1339
|
}
|
|
1250
1340
|
} catch (err) {
|
|
1341
|
+
repoFailureCount++;
|
|
1251
1342
|
console.error(`[PluginsService] Failed to fetch plugins from ${repo.owner}/${repo.name}:`, err.message);
|
|
1252
1343
|
}
|
|
1253
1344
|
}
|
|
1254
1345
|
|
|
1255
|
-
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1346
|
+
const preparedPlugins = this.prepareMarketPlugins(marketPlugins);
|
|
1347
|
+
const preparedFileCache = Array.isArray(fileCache) && fileCache.length > 0
|
|
1348
|
+
? this.prepareMarketPlugins(fileCache)
|
|
1349
|
+
: null;
|
|
1350
|
+
const shouldUseStaleFileCache = preparedFileCache && (
|
|
1351
|
+
(repos.length > 0 && repoFailureCount === repos.length) ||
|
|
1352
|
+
(repoFailureCount > 0 && preparedFileCache.length > preparedPlugins.length)
|
|
1353
|
+
);
|
|
1258
1354
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1355
|
+
if (shouldUseStaleFileCache) {
|
|
1356
|
+
this._marketCache = preparedFileCache;
|
|
1357
|
+
return this._marketCache;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
this._marketCache = preparedPlugins;
|
|
1361
|
+
this.saveMarketCacheToFile(preparedPlugins);
|
|
1262
1362
|
|
|
1263
|
-
return
|
|
1363
|
+
return preparedPlugins;
|
|
1264
1364
|
}
|
|
1265
1365
|
}
|
|
1266
1366
|
|
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
|
-
const { NATIVE_PATHS } = require('../../config/paths');
|
|
10
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
11
11
|
const { resolvePreferredHomeDir } = require('../../utils/home-dir');
|
|
12
12
|
|
|
13
13
|
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
14
14
|
|
|
15
15
|
// Prompts 配置文件路径
|
|
16
|
-
const
|
|
17
|
-
const PROMPTS_FILE = path.join(CC_TOOL_DIR, 'prompts.json');
|
|
16
|
+
const PROMPTS_FILE = PATHS.prompts;
|
|
18
17
|
|
|
19
18
|
// 各平台提示词文件路径
|
|
20
19
|
const CLAUDE_PROMPT_PATH = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
|