@adversity/coding-tool-x 3.1.0 → 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 +39 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
- 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-ROyoZ-6m.css +1 -0
- package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
- package/dist/web/assets/Terminal-BasTyDut.js +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +41 -2
- package/src/config/loader.js +74 -8
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +497 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +345 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/settings.js +111 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +36 -22
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +21 -7
- package/src/server/index.js +174 -58
- package/src/server/opencode-proxy-server.js +5486 -0
- package/src/server/proxy-server.js +33 -22
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +50 -13
- package/src/server/services/env-manager.js +155 -19
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +208 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +156 -8
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -20,9 +20,17 @@ const { injectEnvToShell, removeEnvFromShell, isProxyConfig } = require('./codex
|
|
|
20
20
|
* - 使用 weight 和 maxConcurrency 控制负载均衡
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
function normalizeGatewaySourceType(value, fallback = 'codex') {
|
|
24
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
25
|
+
if (normalized === 'claude') return 'claude';
|
|
26
|
+
if (normalized === 'codex') return 'codex';
|
|
27
|
+
if (normalized === 'gemini') return 'gemini';
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
// 获取渠道存储文件路径
|
|
24
32
|
function getChannelsFilePath() {
|
|
25
|
-
const ccToolDir = path.join(os.homedir(), '.
|
|
33
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
26
34
|
if (!fs.existsSync(ccToolDir)) {
|
|
27
35
|
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
28
36
|
}
|
|
@@ -43,15 +51,17 @@ function loadChannels() {
|
|
|
43
51
|
const data = JSON.parse(content);
|
|
44
52
|
// 确保渠道有 enabled 字段(兼容旧数据)
|
|
45
53
|
if (data.channels) {
|
|
46
|
-
data.channels = data.channels.map(ch =>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
data.channels = data.channels.map(ch => {
|
|
55
|
+
return {
|
|
56
|
+
...ch,
|
|
57
|
+
enabled: ch.enabled !== false, // 默认启用
|
|
58
|
+
weight: ch.weight || 1,
|
|
59
|
+
maxConcurrency: ch.maxConcurrency || null,
|
|
60
|
+
modelRedirects: ch.modelRedirects || [],
|
|
61
|
+
speedTestModel: ch.speedTestModel || null,
|
|
62
|
+
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'codex')
|
|
63
|
+
};
|
|
64
|
+
});
|
|
55
65
|
}
|
|
56
66
|
return data;
|
|
57
67
|
} catch (err) {
|
|
@@ -111,6 +121,7 @@ function initializeFromConfig() {
|
|
|
111
121
|
enabled: config.model_provider === providerKey, // 当前激活的渠道启用
|
|
112
122
|
weight: 1,
|
|
113
123
|
maxConcurrency: null,
|
|
124
|
+
gatewaySourceType: 'codex',
|
|
114
125
|
createdAt: Date.now(),
|
|
115
126
|
updatedAt: Date.now()
|
|
116
127
|
});
|
|
@@ -180,9 +191,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
|
|
|
180
191
|
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
181
192
|
modelRedirects: extraConfig.modelRedirects || [],
|
|
182
193
|
speedTestModel: extraConfig.speedTestModel || null,
|
|
183
|
-
|
|
184
|
-
oauthProvider: extraConfig.oauthProvider || null,
|
|
185
|
-
oauthTokenId: extraConfig.oauthTokenId || null,
|
|
194
|
+
gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'codex'),
|
|
186
195
|
createdAt: Date.now(),
|
|
187
196
|
updatedAt: Date.now()
|
|
188
197
|
};
|
|
@@ -191,8 +200,8 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
|
|
|
191
200
|
saveChannels(data);
|
|
192
201
|
|
|
193
202
|
// 注入该渠道的环境变量(用于直接使用 codex 命令)
|
|
194
|
-
if (apiKey && envKey) {
|
|
195
|
-
const injectResult = injectEnvToShell(envKey, apiKey);
|
|
203
|
+
if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
|
|
204
|
+
const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
|
|
196
205
|
if (injectResult.success) {
|
|
197
206
|
console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
|
|
198
207
|
} else {
|
|
@@ -232,6 +241,7 @@ function updateChannel(channelId, updates) {
|
|
|
232
241
|
createdAt: oldChannel.createdAt, // 保持创建时间
|
|
233
242
|
modelRedirects: merged.modelRedirects || [],
|
|
234
243
|
speedTestModel: merged.speedTestModel !== undefined ? merged.speedTestModel : (oldChannel.speedTestModel || null),
|
|
244
|
+
gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'codex'),
|
|
235
245
|
updatedAt: Date.now()
|
|
236
246
|
};
|
|
237
247
|
|
|
@@ -273,19 +283,23 @@ function updateChannel(channelId, updates) {
|
|
|
273
283
|
// 如果 envKey 或 apiKey 变化,需要更新环境变量
|
|
274
284
|
const oldEnvKey = oldChannel.envKey;
|
|
275
285
|
const newEnvKey = newChannel.envKey;
|
|
276
|
-
const oldApiKey = oldChannel.apiKey;
|
|
277
286
|
const newApiKey = newChannel.apiKey;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
287
|
+
const shouldRemoveOldEnv =
|
|
288
|
+
!!oldEnvKey && (
|
|
289
|
+
oldEnvKey !== newEnvKey ||
|
|
290
|
+
!newApiKey ||
|
|
291
|
+
newChannel.enabled === false
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// 禁用或 key 变化时都要清理旧环境变量,避免残留
|
|
295
|
+
if (shouldRemoveOldEnv) {
|
|
281
296
|
const removeResult = removeEnvFromShell(oldEnvKey);
|
|
282
297
|
if (removeResult.success) {
|
|
283
298
|
console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
|
|
284
299
|
}
|
|
285
300
|
}
|
|
286
301
|
|
|
287
|
-
|
|
288
|
-
if (newApiKey && newEnvKey) {
|
|
302
|
+
if (newChannel.enabled !== false && newApiKey && newEnvKey) {
|
|
289
303
|
const injectResult = injectEnvToShell(newEnvKey, newApiKey);
|
|
290
304
|
if (injectResult.success) {
|
|
291
305
|
console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
|
|
@@ -299,7 +313,7 @@ function updateChannel(channelId, updates) {
|
|
|
299
313
|
}
|
|
300
314
|
|
|
301
315
|
// 删除渠道
|
|
302
|
-
function deleteChannel(channelId) {
|
|
316
|
+
async function deleteChannel(channelId) {
|
|
303
317
|
const data = loadChannels();
|
|
304
318
|
|
|
305
319
|
const index = data.channels.findIndex(c => c.id === channelId);
|
|
@@ -401,7 +415,7 @@ function writeCodexConfigForMultiChannel(allChannels) {
|
|
|
401
415
|
// 回退默认的代理配置(使用默认端口),确保 provider 存在
|
|
402
416
|
config.model_providers['cc-proxy'] = {
|
|
403
417
|
name: 'cc-proxy',
|
|
404
|
-
base_url: 'http://127.0.0.1:
|
|
418
|
+
base_url: 'http://127.0.0.1:20089/v1',
|
|
405
419
|
wire_api: 'responses',
|
|
406
420
|
env_key: 'CC_PROXY_KEY'
|
|
407
421
|
};
|
|
@@ -452,6 +466,12 @@ ${tomlContent}`;
|
|
|
452
466
|
}
|
|
453
467
|
|
|
454
468
|
// 更新所有渠道的 API Key
|
|
469
|
+
for (const channel of allChannels) {
|
|
470
|
+
if (channel.envKey && !channel.apiKey) {
|
|
471
|
+
delete auth[channel.envKey];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
455
475
|
for (const channel of allChannels) {
|
|
456
476
|
if (channel.apiKey) {
|
|
457
477
|
auth[channel.envKey] = channel.apiKey;
|
|
@@ -512,7 +532,10 @@ function syncAllChannelEnvVars() {
|
|
|
512
532
|
const results = [];
|
|
513
533
|
|
|
514
534
|
for (const channel of channels) {
|
|
515
|
-
if (channel.
|
|
535
|
+
if (!channel.envKey) continue;
|
|
536
|
+
|
|
537
|
+
const shouldInject = channel.enabled !== false && !!channel.apiKey;
|
|
538
|
+
if (shouldInject) {
|
|
516
539
|
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
517
540
|
if (injectResult.success) {
|
|
518
541
|
syncedCount++;
|
|
@@ -520,7 +543,11 @@ function syncAllChannelEnvVars() {
|
|
|
520
543
|
} else {
|
|
521
544
|
results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
|
|
522
545
|
}
|
|
546
|
+
continue;
|
|
523
547
|
}
|
|
548
|
+
|
|
549
|
+
// 清理已停用或缺失 key 的渠道环境变量,避免残留
|
|
550
|
+
removeEnvFromShell(channel.envKey);
|
|
524
551
|
}
|
|
525
552
|
|
|
526
553
|
console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
|
|
@@ -624,19 +651,21 @@ ${tomlContent}`;
|
|
|
624
651
|
}
|
|
625
652
|
}
|
|
626
653
|
|
|
627
|
-
// 添加当前渠道的 API Key
|
|
628
654
|
if (channel.apiKey && channel.envKey) {
|
|
629
655
|
auth[channel.envKey] = channel.apiKey;
|
|
656
|
+
} else if (channel.envKey) {
|
|
657
|
+
delete auth[channel.envKey];
|
|
630
658
|
}
|
|
631
659
|
|
|
632
660
|
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
|
|
633
661
|
|
|
634
|
-
// 注入环境变量到 shell 配置文件
|
|
635
662
|
if (channel.apiKey && channel.envKey) {
|
|
636
663
|
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
637
664
|
if (injectResult.success) {
|
|
638
665
|
console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
|
|
639
666
|
}
|
|
667
|
+
} else if (channel.envKey) {
|
|
668
|
+
removeEnvFromShell(channel.envKey);
|
|
640
669
|
}
|
|
641
670
|
|
|
642
671
|
return channel;
|
|
@@ -653,24 +682,8 @@ try {
|
|
|
653
682
|
console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
|
|
654
683
|
}
|
|
655
684
|
|
|
656
|
-
/**
|
|
657
|
-
* 获取渠道的有效 API Key
|
|
658
|
-
* 如果渠道使用 OAuth 认证,返回 OAuth 令牌;否则返回静态 API Key
|
|
659
|
-
*
|
|
660
|
-
* @param {Object} channel - 渠道对象
|
|
661
|
-
* @returns {string|null} 有效的 API Key
|
|
662
|
-
*/
|
|
663
685
|
function getEffectiveApiKey(channel) {
|
|
664
|
-
|
|
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;
|
|
686
|
+
return channel.apiKey || null;
|
|
674
687
|
}
|
|
675
688
|
|
|
676
689
|
module.exports = {
|
|
@@ -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
|
|