@adversity/coding-tool-x 3.1.1 → 3.1.2
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 +24 -0
- package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-DvcbKKdS.js} +1 -1
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-jy_4GVxI.js} +1 -1
- package/dist/web/assets/{ProjectList-DRb1DuHV.js → ProjectList-Df1-NcNr.js} +1 -1
- package/dist/web/assets/{SessionList-lZ0LKzfT.js → SessionList-UWcZtC2r.js} +1 -1
- package/dist/web/assets/{SkillManager-C1xG5B4Q.js → SkillManager-IRdseMKB.js} +1 -1
- package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-BasTyDut.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Burx7XOo.js → WorkspaceManager-D-D2kK1V.js} +1 -1
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/config/default.js +2 -0
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/server/api/opencode-channels.js +84 -6
- package/src/server/api/opencode-proxy.js +41 -32
- package/src/server/api/opencode-sessions.js +4 -62
- package/src/server/api/settings.js +111 -0
- package/src/server/codex-proxy-server.js +6 -4
- package/src/server/gemini-proxy-server.js +6 -4
- package/src/server/index.js +13 -4
- package/src/server/opencode-proxy-server.js +1197 -86
- package/src/server/proxy-server.js +6 -4
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/env-checker.js +24 -1
- package/src/server/services/env-manager.js +29 -1
- package/src/server/services/opencode-channels.js +3 -1
- package/src/server/services/opencode-sessions.js +486 -218
- package/src/server/services/opencode-settings-manager.js +172 -36
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/websocket-server.js +24 -5
- package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
- package/dist/web/assets/Home-Di2qsylF.css +0 -1
- package/dist/web/assets/index-Ufv5rCa5.css +0 -1
- package/dist/web/assets/index-lAkrRC3h.js +0 -2
|
@@ -11,6 +11,7 @@ const DEFAULT_CONFIG = require('../config/default');
|
|
|
11
11
|
const { resolvePricing, resolveModelPricing } = require('./utils/pricing');
|
|
12
12
|
const { recordRequest } = require('./services/statistics-service');
|
|
13
13
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
14
|
+
const { createDecodedStream } = require('./services/response-decoder');
|
|
14
15
|
const eventBus = require('../plugins/event-bus');
|
|
15
16
|
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
16
17
|
const { getEffectiveApiKey } = require('./services/channels');
|
|
@@ -334,11 +335,12 @@ async function startProxyServer(options = {}) {
|
|
|
334
335
|
cacheRead: 0,
|
|
335
336
|
model: ''
|
|
336
337
|
};
|
|
338
|
+
const parsedStream = createDecodedStream(proxyRes);
|
|
337
339
|
|
|
338
|
-
|
|
340
|
+
parsedStream.on('data', (chunk) => {
|
|
339
341
|
if (isResponseClosed) return;
|
|
340
342
|
|
|
341
|
-
buffer += chunk.toString();
|
|
343
|
+
buffer += chunk.toString('utf8');
|
|
342
344
|
|
|
343
345
|
const events = buffer.split('\n\n');
|
|
344
346
|
buffer = events.pop() || '';
|
|
@@ -448,9 +450,9 @@ async function startProxyServer(options = {}) {
|
|
|
448
450
|
}
|
|
449
451
|
};
|
|
450
452
|
|
|
451
|
-
|
|
453
|
+
parsedStream.on('end', finalize);
|
|
452
454
|
|
|
453
|
-
|
|
455
|
+
parsedStream.on('error', (err) => {
|
|
454
456
|
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
|
|
455
457
|
console.error('Proxy response error:', err);
|
|
456
458
|
}
|
|
@@ -3,6 +3,15 @@ const path = require('path');
|
|
|
3
3
|
const { getCodexDir } = require('./codex-config');
|
|
4
4
|
const { parseSession, parseSessionMeta, extractSessionMeta, readJSONL } = require('./codex-parser');
|
|
5
5
|
|
|
6
|
+
const COUNTS_CACHE_TTL_MS = 30 * 1000;
|
|
7
|
+
const FAST_META_READ_BYTES = 64 * 1024;
|
|
8
|
+
const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
|
|
9
|
+
|
|
10
|
+
let countsCache = {
|
|
11
|
+
expiresAt: 0,
|
|
12
|
+
value: EMPTY_COUNTS
|
|
13
|
+
};
|
|
14
|
+
|
|
6
15
|
/**
|
|
7
16
|
* 获取会话目录
|
|
8
17
|
*/
|
|
@@ -437,6 +446,7 @@ function deleteProject(projectName) {
|
|
|
437
446
|
console.error('[Codex] Failed to clean session order:', err.message);
|
|
438
447
|
}
|
|
439
448
|
|
|
449
|
+
invalidateProjectAndSessionCountsCache();
|
|
440
450
|
return { success: true, deletedCount };
|
|
441
451
|
}
|
|
442
452
|
|
|
@@ -524,6 +534,7 @@ function deleteSession(sessionId) {
|
|
|
524
534
|
// 忽略别名不存在的错误
|
|
525
535
|
}
|
|
526
536
|
|
|
537
|
+
invalidateProjectAndSessionCountsCache();
|
|
527
538
|
return { success: true };
|
|
528
539
|
}
|
|
529
540
|
|
|
@@ -577,6 +588,7 @@ function forkSession(sessionId) {
|
|
|
577
588
|
forkRelations[newSessionId] = sessionId;
|
|
578
589
|
saveForkRelations(forkRelations);
|
|
579
590
|
|
|
591
|
+
invalidateProjectAndSessionCountsCache();
|
|
580
592
|
return {
|
|
581
593
|
newSessionId,
|
|
582
594
|
forkedFrom: sessionId,
|
|
@@ -628,19 +640,106 @@ function saveProjectOrder(order) {
|
|
|
628
640
|
saveClaudeProjectOrder({ projectsDir: getCodexDir() }, order);
|
|
629
641
|
}
|
|
630
642
|
|
|
643
|
+
function invalidateProjectAndSessionCountsCache() {
|
|
644
|
+
countsCache.expiresAt = 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractCodexProjectNameFromMeta(metaPayload = {}) {
|
|
648
|
+
const repoUrl = metaPayload?.git?.repository_url || metaPayload?.git?.repositoryUrl;
|
|
649
|
+
if (typeof repoUrl === 'string' && repoUrl.trim()) {
|
|
650
|
+
const parsedName = repoUrl.split('/').pop();
|
|
651
|
+
if (parsedName) {
|
|
652
|
+
const normalized = parsedName.replace(/\.git$/i, '').trim();
|
|
653
|
+
if (normalized) return normalized;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const cwd = metaPayload?.cwd;
|
|
658
|
+
if (typeof cwd === 'string' && cwd.trim()) {
|
|
659
|
+
return path.basename(cwd.trim());
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return '';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function readSessionMetaPayloadFast(filePath) {
|
|
666
|
+
let fd;
|
|
667
|
+
try {
|
|
668
|
+
fd = fs.openSync(filePath, 'r');
|
|
669
|
+
const buffer = Buffer.alloc(FAST_META_READ_BYTES);
|
|
670
|
+
const bytesRead = fs.readSync(fd, buffer, 0, FAST_META_READ_BYTES, 0);
|
|
671
|
+
if (bytesRead <= 0) return null;
|
|
672
|
+
|
|
673
|
+
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
674
|
+
const lines = chunk.split('\n');
|
|
675
|
+
|
|
676
|
+
for (const line of lines) {
|
|
677
|
+
const trimmed = line.trim();
|
|
678
|
+
if (!trimmed) continue;
|
|
679
|
+
let parsed;
|
|
680
|
+
try {
|
|
681
|
+
parsed = JSON.parse(trimmed);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (parsed?.type === 'session_meta' && parsed?.payload && typeof parsed.payload === 'object') {
|
|
686
|
+
return parsed.payload;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} catch (err) {
|
|
690
|
+
return null;
|
|
691
|
+
} finally {
|
|
692
|
+
if (fd !== undefined) {
|
|
693
|
+
try {
|
|
694
|
+
fs.closeSync(fd);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
// ignore close errors
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function calculateProjectAndSessionCounts() {
|
|
705
|
+
const sessions = scanSessionFiles();
|
|
706
|
+
if (sessions.length === 0) {
|
|
707
|
+
return EMPTY_COUNTS;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const projectNames = new Set();
|
|
711
|
+
sessions.forEach((session) => {
|
|
712
|
+
const payload = readSessionMetaPayloadFast(session.filePath);
|
|
713
|
+
const projectName = extractCodexProjectNameFromMeta(payload || {});
|
|
714
|
+
if (projectName) {
|
|
715
|
+
projectNames.add(projectName);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
projectCount: projectNames.size,
|
|
721
|
+
sessionCount: sessions.length
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
631
725
|
/**
|
|
632
726
|
* 获取 Codex 项目与会话数量(用于仪表盘轻量统计)
|
|
633
727
|
*/
|
|
634
728
|
function getProjectAndSessionCounts() {
|
|
729
|
+
const now = Date.now();
|
|
730
|
+
if (countsCache.expiresAt > now) {
|
|
731
|
+
return countsCache.value;
|
|
732
|
+
}
|
|
733
|
+
|
|
635
734
|
try {
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
sessionCount: sessions.length
|
|
735
|
+
const counts = calculateProjectAndSessionCounts();
|
|
736
|
+
countsCache = {
|
|
737
|
+
value: counts,
|
|
738
|
+
expiresAt: now + COUNTS_CACHE_TTL_MS
|
|
641
739
|
};
|
|
740
|
+
return counts;
|
|
642
741
|
} catch (err) {
|
|
643
|
-
return
|
|
742
|
+
return countsCache.value || EMPTY_COUNTS;
|
|
644
743
|
}
|
|
645
744
|
}
|
|
646
745
|
|
|
@@ -47,8 +47,11 @@ const SHELL_CONFIG_FILES = process.platform === 'win32'
|
|
|
47
47
|
? [
|
|
48
48
|
'.bashrc',
|
|
49
49
|
'.bash_profile',
|
|
50
|
+
'.bash_login',
|
|
50
51
|
'.zshrc',
|
|
52
|
+
'.zshenv',
|
|
51
53
|
'.zprofile',
|
|
54
|
+
'.zlogin',
|
|
52
55
|
'.profile',
|
|
53
56
|
path.join('Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
54
57
|
path.join('Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
|
|
@@ -56,8 +59,11 @@ const SHELL_CONFIG_FILES = process.platform === 'win32'
|
|
|
56
59
|
: [
|
|
57
60
|
'.bashrc',
|
|
58
61
|
'.bash_profile',
|
|
62
|
+
'.bash_login',
|
|
59
63
|
'.zshrc',
|
|
64
|
+
'.zshenv',
|
|
60
65
|
'.zprofile',
|
|
66
|
+
'.zlogin',
|
|
61
67
|
'.profile'
|
|
62
68
|
];
|
|
63
69
|
|
|
@@ -108,6 +114,10 @@ function checkProcessEnv(keywords) {
|
|
|
108
114
|
const conflicts = [];
|
|
109
115
|
|
|
110
116
|
for (const [key, value] of Object.entries(process.env)) {
|
|
117
|
+
if (!hasNonEmptyValue(value)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
111
121
|
if (matchesKeywords(key, keywords)) {
|
|
112
122
|
conflicts.push({
|
|
113
123
|
varName: key,
|
|
@@ -183,11 +193,17 @@ function parseConfigFile(filePath, keywords) {
|
|
|
183
193
|
|
|
184
194
|
if (matched) {
|
|
185
195
|
const [, varName, varValue] = matched;
|
|
196
|
+
const normalizedValue = cleanValue(varValue);
|
|
197
|
+
|
|
198
|
+
// 空值通常是占位或已清理状态,不再视为冲突
|
|
199
|
+
if (!hasNonEmptyValue(normalizedValue)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
186
202
|
|
|
187
203
|
if (matchesKeywords(varName, keywords)) {
|
|
188
204
|
conflicts.push({
|
|
189
205
|
varName,
|
|
190
|
-
varValue: maskSensitiveValue(
|
|
206
|
+
varValue: maskSensitiveValue(normalizedValue),
|
|
191
207
|
sourceType: 'file',
|
|
192
208
|
sourcePath: `${filePath}:${i + 1}`,
|
|
193
209
|
filePath,
|
|
@@ -266,6 +282,13 @@ function cleanValue(value) {
|
|
|
266
282
|
return cleaned;
|
|
267
283
|
}
|
|
268
284
|
|
|
285
|
+
/**
|
|
286
|
+
* 判断值是否为非空
|
|
287
|
+
*/
|
|
288
|
+
function hasNonEmptyValue(value) {
|
|
289
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
290
|
+
}
|
|
291
|
+
|
|
269
292
|
/**
|
|
270
293
|
* 遮蔽敏感值
|
|
271
294
|
*/
|
|
@@ -124,11 +124,15 @@ function deleteEnvVars(conflicts) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// 同步清理当前服务进程中的同名变量,避免“删除后立即复检仍提示冲突”
|
|
128
|
+
const clearedProcessVars = clearProcessEnvVars(conflicts);
|
|
129
|
+
|
|
127
130
|
return {
|
|
128
131
|
backupPath: backupInfo.backupPath,
|
|
129
132
|
timestamp: backupInfo.timestamp,
|
|
130
133
|
results,
|
|
131
|
-
processConflictsSkipped: processConflicts.length
|
|
134
|
+
processConflictsSkipped: processConflicts.length,
|
|
135
|
+
clearedProcessVars
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
138
|
|
|
@@ -220,6 +224,30 @@ function groupByFile(conflicts) {
|
|
|
220
224
|
return groups;
|
|
221
225
|
}
|
|
222
226
|
|
|
227
|
+
/**
|
|
228
|
+
* 清理当前 Node 进程中的环境变量
|
|
229
|
+
*/
|
|
230
|
+
function clearProcessEnvVars(conflicts) {
|
|
231
|
+
const names = new Set();
|
|
232
|
+
|
|
233
|
+
for (const conflict of conflicts || []) {
|
|
234
|
+
const varName = String(conflict?.varName || '').trim();
|
|
235
|
+
if (varName) {
|
|
236
|
+
names.add(varName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const cleared = [];
|
|
241
|
+
for (const varName of names) {
|
|
242
|
+
if (Object.prototype.hasOwnProperty.call(process.env, varName)) {
|
|
243
|
+
delete process.env[varName];
|
|
244
|
+
cleared.push(varName);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return cleared;
|
|
249
|
+
}
|
|
250
|
+
|
|
223
251
|
/**
|
|
224
252
|
* 从文件中移除环境变量
|
|
225
253
|
*/
|
|
@@ -46,7 +46,8 @@ function loadChannels() {
|
|
|
46
46
|
modelRedirects: ch.modelRedirects || [],
|
|
47
47
|
speedTestModel: ch.speedTestModel || null,
|
|
48
48
|
wireApi: ch.wireApi || 'openai', // OpenCode 默认使用 OpenAI 兼容格式
|
|
49
|
-
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType)
|
|
49
|
+
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType),
|
|
50
|
+
allowedModels: ch.allowedModels || []
|
|
50
51
|
};
|
|
51
52
|
normalized.providerKey = deriveProviderKey(normalized);
|
|
52
53
|
return normalized;
|
|
@@ -101,6 +102,7 @@ function createChannel(name, baseUrl, apiKey, extraConfig = {}) {
|
|
|
101
102
|
providerKey: extraConfig.providerKey || null,
|
|
102
103
|
presetId: extraConfig.presetId || null,
|
|
103
104
|
websiteUrl: extraConfig.websiteUrl || '',
|
|
105
|
+
allowedModels: extraConfig.allowedModels || [],
|
|
104
106
|
createdAt: Date.now(),
|
|
105
107
|
updatedAt: Date.now()
|
|
106
108
|
};
|