@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-DvcbKKdS.js} +1 -1
  3. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  4. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  5. package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-jy_4GVxI.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DRb1DuHV.js → ProjectList-Df1-NcNr.js} +1 -1
  7. package/dist/web/assets/{SessionList-lZ0LKzfT.js → SessionList-UWcZtC2r.js} +1 -1
  8. package/dist/web/assets/{SkillManager-C1xG5B4Q.js → SkillManager-IRdseMKB.js} +1 -1
  9. package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-BasTyDut.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Burx7XOo.js → WorkspaceManager-D-D2kK1V.js} +1 -1
  11. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  12. package/dist/web/assets/index-CryrSLv8.js +2 -0
  13. package/dist/web/index.html +2 -2
  14. package/package.json +1 -1
  15. package/src/config/default.js +2 -0
  16. package/src/config/model-metadata.js +415 -0
  17. package/src/config/model-pricing.js +23 -93
  18. package/src/server/api/opencode-channels.js +84 -6
  19. package/src/server/api/opencode-proxy.js +41 -32
  20. package/src/server/api/opencode-sessions.js +4 -62
  21. package/src/server/api/settings.js +111 -0
  22. package/src/server/codex-proxy-server.js +6 -4
  23. package/src/server/gemini-proxy-server.js +6 -4
  24. package/src/server/index.js +13 -4
  25. package/src/server/opencode-proxy-server.js +1197 -86
  26. package/src/server/proxy-server.js +6 -4
  27. package/src/server/services/codex-sessions.js +105 -6
  28. package/src/server/services/env-checker.js +24 -1
  29. package/src/server/services/env-manager.js +29 -1
  30. package/src/server/services/opencode-channels.js +3 -1
  31. package/src/server/services/opencode-sessions.js +486 -218
  32. package/src/server/services/opencode-settings-manager.js +172 -36
  33. package/src/server/services/response-decoder.js +21 -0
  34. package/src/server/websocket-server.js +24 -5
  35. package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
  36. package/dist/web/assets/Home-Di2qsylF.css +0 -1
  37. package/dist/web/assets/index-Ufv5rCa5.css +0 -1
  38. 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
- proxyRes.on('data', (chunk) => {
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
- proxyRes.on('end', finalize);
453
+ parsedStream.on('end', finalize);
452
454
 
453
- proxyRes.on('error', (err) => {
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 projects = getProjects();
637
- const sessions = scanSessionFiles();
638
- return {
639
- projectCount: projects.length,
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 { projectCount: 0, sessionCount: 0 };
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(cleanValue(varValue)),
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
  };