codexmate 0.0.34 → 0.0.36
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/cli.js +74 -61
- package/package.json +2 -1
- package/web-ui/app.js +33 -2
- package/web-ui/index.html +1 -1
- package/web-ui/logic.sessions.mjs +6 -5
- package/web-ui/modules/app.computed.dashboard.mjs +4 -0
- package/web-ui/modules/app.computed.session.mjs +147 -6
- package/web-ui/modules/app.methods.claude-config.mjs +4 -0
- package/web-ui/modules/app.methods.navigation.mjs +32 -16
- package/web-ui/modules/app.methods.session-browser.mjs +7 -0
- package/web-ui/modules/app.methods.session-trash.mjs +30 -0
- package/web-ui/modules/i18n.dict.mjs +5 -0
- package/web-ui/modules/sessions-filters-url.mjs +65 -12
- package/web-ui/modules/skills.methods.mjs +31 -0
- package/web-ui/partials/index/layout-header.html +17 -12
- package/web-ui/partials/index/panel-config-claude.html +5 -3
- package/web-ui/partials/index/panel-config-codex.html +1 -1
- package/web-ui/partials/index/panel-market.html +76 -149
- package/web-ui/partials/index/panel-sessions.html +2 -2
- package/web-ui/partials/index/panel-settings.html +4 -2
- package/web-ui/partials/index/panel-usage.html +115 -68
- package/web-ui/res/vue.runtime.global.prod.js +7 -0
- package/web-ui/res/web-ui-render.precompiled.js +7274 -0
- package/web-ui/session-helpers.mjs +15 -4
- package/web-ui/source-bundle.cjs +73 -1
- package/web-ui/styles/base-theme.css +10 -0
- package/web-ui/styles/layout-shell.css +66 -27
- package/web-ui/styles/navigation-panels.css +8 -0
- package/web-ui/styles/responsive.css +50 -9
- package/web-ui/styles/sessions-usage.css +336 -319
- package/web-ui/styles/skills-market.css +294 -0
- package/web-ui/styles/titles-cards.css +14 -0
package/cli.js
CHANGED
|
@@ -4879,27 +4879,29 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4879
4879
|
}
|
|
4880
4880
|
}
|
|
4881
4881
|
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4882
|
+
// 补充扫描未索引的 .jsonl 文件(包括 sessions-index.json 中遗漏的会话)
|
|
4883
|
+
const seenFilePaths = new Set(sessions.map((item) => item.filePath).filter(Boolean));
|
|
4884
|
+
const fallbackFiles = collectRecentJsonlFiles(claudeProjectsDir, {
|
|
4885
|
+
returnCount: scanCount,
|
|
4886
|
+
maxFilesScanned,
|
|
4887
|
+
ignoreSubPath: `${path.sep}subagents${path.sep}`
|
|
4888
|
+
});
|
|
4889
|
+
for (const filePath of fallbackFiles) {
|
|
4890
|
+
if (seenFilePaths.has(filePath)) continue;
|
|
4891
|
+
const summary = parseClaudeSessionSummary(filePath, {
|
|
4892
|
+
summaryReadBytes,
|
|
4893
|
+
titleReadBytes
|
|
4887
4894
|
});
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
});
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
...summary,
|
|
4896
|
-
derived: isDerivedSessionFile(filePath)
|
|
4897
|
-
}));
|
|
4898
|
-
}
|
|
4895
|
+
if (summary) {
|
|
4896
|
+
sessions.push(attachSessionNativeStatus({
|
|
4897
|
+
...summary,
|
|
4898
|
+
derived: isDerivedSessionFile(filePath)
|
|
4899
|
+
}));
|
|
4900
|
+
seenFilePaths.add(filePath);
|
|
4901
|
+
}
|
|
4899
4902
|
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
}
|
|
4903
|
+
if (sessions.length >= targetCount) {
|
|
4904
|
+
break;
|
|
4903
4905
|
}
|
|
4904
4906
|
}
|
|
4905
4907
|
|
|
@@ -10058,9 +10060,10 @@ function watchPathsForRestart(targets, onChange) {
|
|
|
10058
10060
|
}
|
|
10059
10061
|
// #endregion watchPathsForRestart
|
|
10060
10062
|
|
|
10061
|
-
function writeJsonResponse(res, statusCode, payload) {
|
|
10063
|
+
function writeJsonResponse(res, statusCode, payload, headers = {}) {
|
|
10062
10064
|
const body = JSON.stringify(payload, null, 2);
|
|
10063
10065
|
res.writeHead(statusCode, {
|
|
10066
|
+
...headers,
|
|
10064
10067
|
'Content-Type': 'application/json; charset=utf-8',
|
|
10065
10068
|
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
10066
10069
|
});
|
|
@@ -10620,7 +10623,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
10620
10623
|
const securityHeaders = {
|
|
10621
10624
|
'X-Content-Type-Options': 'nosniff',
|
|
10622
10625
|
'X-Frame-Options': 'DENY',
|
|
10623
|
-
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'
|
|
10626
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:"
|
|
10624
10627
|
};
|
|
10625
10628
|
const origWriteHead = res.writeHead.bind(res);
|
|
10626
10629
|
res.writeHead = function (statusCode, headers) {
|
|
@@ -10649,34 +10652,15 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
10649
10652
|
|| requestPath.startsWith('/download/')
|
|
10650
10653
|
) {
|
|
10651
10654
|
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
10652
|
-
const isLoopback = !remoteAddr
|
|
10653
|
-
|| remoteAddr === '127.0.0.1'
|
|
10654
|
-
|| remoteAddr === '::1'
|
|
10655
|
-
|| remoteAddr === '::ffff:127.0.0.1';
|
|
10655
|
+
const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
|
|
10656
10656
|
if (!isLoopback) {
|
|
10657
10657
|
const rateLimitKey = (remoteAddr || 'unknown') + ':' + requestPath;
|
|
10658
10658
|
if (!checkRateLimit(rateLimitKey)) {
|
|
10659
|
-
res
|
|
10660
|
-
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
|
|
10659
|
+
writeJsonResponse(res, 429, { error: 'Rate limit exceeded' }, { 'Retry-After': '60' });
|
|
10661
10660
|
return;
|
|
10662
10661
|
}
|
|
10663
|
-
const
|
|
10664
|
-
|
|
10665
|
-
: '';
|
|
10666
|
-
if (!expected) {
|
|
10667
|
-
sendJson(403, {
|
|
10668
|
-
error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN or use --host 127.0.0.1)'
|
|
10669
|
-
});
|
|
10670
|
-
return;
|
|
10671
|
-
}
|
|
10672
|
-
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
10673
|
-
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
10674
|
-
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
|
|
10675
|
-
const actual = match && match[1]
|
|
10676
|
-
? match[1].trim()
|
|
10677
|
-
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
|
|
10678
|
-
if (!actual || !safeTimingEqual(actual, expected)) {
|
|
10679
|
-
sendJson(401, { error: 'Unauthorized' });
|
|
10662
|
+
const auth = assertRequestAuthorized(req, res);
|
|
10663
|
+
if (!auth.ok) {
|
|
10680
10664
|
return;
|
|
10681
10665
|
}
|
|
10682
10666
|
}
|
|
@@ -11408,15 +11392,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11408
11392
|
res.end(errorBody, 'utf-8');
|
|
11409
11393
|
}
|
|
11410
11394
|
});
|
|
11411
|
-
} else if (requestPath === '/web-ui') {
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
11395
|
+
} else if (requestPath === '/web-ui/index.html') {
|
|
11396
|
+
const rawUrl = typeof req.url === 'string' ? req.url : '';
|
|
11397
|
+
const queryIndex = rawUrl.indexOf('?');
|
|
11398
|
+
const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : '';
|
|
11399
|
+
res.writeHead(302, {
|
|
11400
|
+
'Location': `/${query}`,
|
|
11401
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
11402
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11403
|
+
});
|
|
11404
|
+
res.end('Found');
|
|
11419
11405
|
} else if (requestPath.startsWith('/web-ui/')) {
|
|
11406
|
+
// Skip the /web-ui/ directory itself, which is handled above
|
|
11420
11407
|
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
11421
11408
|
const filePath = path.join(__dirname, normalized);
|
|
11422
11409
|
if (!isPathInside(filePath, webDir)) {
|
|
@@ -11425,11 +11412,22 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11425
11412
|
return;
|
|
11426
11413
|
}
|
|
11427
11414
|
const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/');
|
|
11415
|
+
|
|
11416
|
+
// Empty relativePath means direct /web-ui/ access - return 404
|
|
11417
|
+
if (relativePath === '' || relativePath === 'index.html') {
|
|
11418
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
11419
|
+
res.end('Not Found');
|
|
11420
|
+
return;
|
|
11421
|
+
}
|
|
11422
|
+
|
|
11428
11423
|
const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
|
|
11429
11424
|
if (dynamicAsset) {
|
|
11430
11425
|
try {
|
|
11431
11426
|
const assetBody = dynamicAsset.reader(filePath);
|
|
11432
|
-
res.writeHead(200, {
|
|
11427
|
+
res.writeHead(200, {
|
|
11428
|
+
'Content-Type': dynamicAsset.mime,
|
|
11429
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11430
|
+
});
|
|
11433
11431
|
res.end(assetBody, 'utf-8');
|
|
11434
11432
|
} catch (error) {
|
|
11435
11433
|
writeWebUiAssetError(res, requestPath, error);
|
|
@@ -11456,7 +11454,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11456
11454
|
: ext === '.json'
|
|
11457
11455
|
? 'application/json; charset=utf-8'
|
|
11458
11456
|
: 'application/octet-stream';
|
|
11459
|
-
res.writeHead(200, {
|
|
11457
|
+
res.writeHead(200, {
|
|
11458
|
+
'Content-Type': mime,
|
|
11459
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11460
|
+
});
|
|
11460
11461
|
fs.createReadStream(filePath).pipe(res);
|
|
11461
11462
|
} else if (requestPath.startsWith('/download/')) {
|
|
11462
11463
|
const fileName = requestPath.slice('/download/'.length);
|
|
@@ -11517,15 +11518,27 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11517
11518
|
: ext === '.json'
|
|
11518
11519
|
? 'application/json; charset=utf-8'
|
|
11519
11520
|
: 'application/octet-stream';
|
|
11520
|
-
res.writeHead(200, {
|
|
11521
|
+
res.writeHead(200, {
|
|
11522
|
+
'Content-Type': mime,
|
|
11523
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11524
|
+
});
|
|
11521
11525
|
fs.createReadStream(filePath).pipe(res);
|
|
11522
11526
|
} else {
|
|
11523
|
-
|
|
11524
|
-
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
|
|
11527
|
+
// Only serve HTML for root path; /web-ui returns 404.
|
|
11528
|
+
if (requestPath === '/') {
|
|
11529
|
+
try {
|
|
11530
|
+
const html = readBundledWebUiHtml(htmlPath);
|
|
11531
|
+
res.writeHead(200, {
|
|
11532
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
11533
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11534
|
+
});
|
|
11535
|
+
res.end(html);
|
|
11536
|
+
} catch (error) {
|
|
11537
|
+
writeWebUiAssetError(res, requestPath, error);
|
|
11538
|
+
}
|
|
11539
|
+
} else {
|
|
11540
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
11541
|
+
res.end('Not Found');
|
|
11529
11542
|
}
|
|
11530
11543
|
}
|
|
11531
11544
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codexmate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.36",
|
|
4
4
|
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@iarna/toml": "^2.2.5",
|
|
49
|
+
"@vue/compiler-dom": "^3.5.30",
|
|
49
50
|
"json5": "^2.2.3",
|
|
50
51
|
"yauzl": "^3.2.1",
|
|
51
52
|
"zip-lib": "^1.2.1"
|
package/web-ui/app.js
CHANGED
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
import { createAppComputed } from './modules/app.computed.index.mjs';
|
|
8
8
|
import { createAppMethods } from './modules/app.methods.index.mjs';
|
|
9
9
|
import { loadConfigTemplateDiffConfirmEnabledFromStorage } from './modules/config-template-confirm-pref.mjs';
|
|
10
|
+
import { installWebUiUrlCanonicalization } from './modules/sessions-filters-url.mjs';
|
|
10
11
|
|
|
11
12
|
document.addEventListener('DOMContentLoaded', () => {
|
|
13
|
+
installWebUiUrlCanonicalization();
|
|
12
14
|
if (typeof Vue === 'undefined') {
|
|
13
15
|
console.error('Vue 库未能在 DOMContentLoaded 触发前加载完成。');
|
|
14
16
|
const fallbackTarget = document.querySelector('#app') || document.querySelector('[v-cloak]');
|
|
@@ -26,9 +28,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
26
28
|
|
|
27
29
|
const { createApp } = Vue;
|
|
28
30
|
|
|
29
|
-
const
|
|
31
|
+
const appOptions = {
|
|
30
32
|
data() {
|
|
31
33
|
return {
|
|
34
|
+
brandHovered: false,
|
|
32
35
|
lang: 'zh',
|
|
33
36
|
appVersion: '',
|
|
34
37
|
mainTab: 'dashboard',
|
|
@@ -230,6 +233,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
230
233
|
sessionPreviewHeaderEl: null,
|
|
231
234
|
sessionPreviewHeaderResizeObserver: null,
|
|
232
235
|
sessionListRenderEnabled: false,
|
|
236
|
+
preserveSessionRenderOnTabLeave: true,
|
|
233
237
|
sessionListVisibleCount: 0,
|
|
234
238
|
sessionListInitialBatchSize: 40,
|
|
235
239
|
sessionListLoadStep: 80,
|
|
@@ -420,6 +424,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
420
424
|
},
|
|
421
425
|
|
|
422
426
|
mounted() {
|
|
427
|
+
// URL 规范化:将 /web-ui/* 重定向到根路径 /
|
|
428
|
+
try {
|
|
429
|
+
const pathname = window.location.pathname;
|
|
430
|
+
if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') {
|
|
431
|
+
const url = new URL(window.location.href);
|
|
432
|
+
url.pathname = '/';
|
|
433
|
+
// 移除查询参数和 hash,保持 URL 纯净
|
|
434
|
+
url.search = '';
|
|
435
|
+
url.hash = '';
|
|
436
|
+
window.location.replace(url.toString());
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// 清理任何查询参数和 hash,保持 URL 为 /
|
|
440
|
+
if (window.location.search || window.location.hash) {
|
|
441
|
+
const url = new URL(window.location.href);
|
|
442
|
+
url.search = '';
|
|
443
|
+
url.hash = '';
|
|
444
|
+
window.history.replaceState(null, '', url.toString());
|
|
445
|
+
}
|
|
446
|
+
} catch (_) {}
|
|
447
|
+
|
|
423
448
|
if (typeof this.initI18n === 'function') {
|
|
424
449
|
this.initI18n();
|
|
425
450
|
}
|
|
@@ -648,7 +673,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
648
673
|
|
|
649
674
|
computed: createAppComputed(),
|
|
650
675
|
methods: createAppMethods()
|
|
651
|
-
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
if (typeof window.__CODEXMATE_WEB_UI_RENDER__ === 'function') {
|
|
679
|
+
appOptions.render = window.__CODEXMATE_WEB_UI_RENDER__;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const app = createApp(appOptions);
|
|
652
683
|
|
|
653
684
|
app.mount('#app');
|
|
654
685
|
});
|
package/web-ui/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Codex Mate</title>
|
|
7
7
|
<link rel="icon" type="image/webp" href="/res/logo-pack.webp">
|
|
8
8
|
<link rel="apple-touch-icon" href="/res/logo-pack.webp">
|
|
9
|
-
<script src="/res/vue.global.prod.js"></script>
|
|
9
|
+
<script src="/res/vue.runtime.global.prod.js"></script>
|
|
10
10
|
<script src="/res/json5.min.js"></script>
|
|
11
11
|
<link rel="stylesheet" href="/web-ui/styles.css">
|
|
12
12
|
</head>
|
|
@@ -180,12 +180,9 @@ export function formatSessionTimelineTimestamp(timestamp) {
|
|
|
180
180
|
if (!value) return '';
|
|
181
181
|
|
|
182
182
|
const matched = value.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2}))?/);
|
|
183
|
-
if (matched)
|
|
184
|
-
const second = matched[6] || '00';
|
|
185
|
-
return `${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}:${second}`;
|
|
186
|
-
}
|
|
183
|
+
if (!matched) return value;
|
|
187
184
|
|
|
188
|
-
return
|
|
185
|
+
return `${matched[1]}-${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}`;
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
function normalizeUsageRange(range) {
|
|
@@ -549,11 +546,15 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
|
|
|
549
546
|
String(messageCount),
|
|
550
547
|
String(sessionIndex)
|
|
551
548
|
].join(':'),
|
|
549
|
+
sessionId: session.sessionId || '',
|
|
550
|
+
filePath: session.filePath || '',
|
|
552
551
|
title: normalizedTitle,
|
|
553
552
|
source,
|
|
554
553
|
sourceLabel,
|
|
555
554
|
cwd,
|
|
556
555
|
messageCount,
|
|
556
|
+
totalTokens: sessionTotalTokens,
|
|
557
|
+
contextWindow: sessionContextWindow,
|
|
557
558
|
updatedAt: session.updatedAt || '',
|
|
558
559
|
updatedAtMs,
|
|
559
560
|
updatedAtLabel: formatSessionTimelineTimestamp(session.updatedAt || ''),
|
|
@@ -92,6 +92,10 @@ export function createDashboardComputed() {
|
|
|
92
92
|
return list;
|
|
93
93
|
},
|
|
94
94
|
|
|
95
|
+
isLocalProviderDisabled() {
|
|
96
|
+
return this.configMode === 'codex';
|
|
97
|
+
},
|
|
98
|
+
|
|
95
99
|
displayProviderUrl() {
|
|
96
100
|
return (provider) => {
|
|
97
101
|
if (provider && provider.name === 'local') return '';
|
|
@@ -602,11 +602,10 @@ export function createSessionComputed() {
|
|
|
602
602
|
const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
|
|
603
603
|
? this.sessionUsageCharts.filteredSessions
|
|
604
604
|
: this.sessionsUsageList;
|
|
605
|
-
const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
|
|
606
605
|
const rangeDays = this.sessionsUsageTimeRange === '30d' ? 30 : 7;
|
|
607
606
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
608
607
|
const baseMs = Date.parse(`${dayKey}T00:00:00.000Z`);
|
|
609
|
-
const prevKey =
|
|
608
|
+
const prevKey = Number.isFinite(baseMs)
|
|
610
609
|
? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10)
|
|
611
610
|
: '';
|
|
612
611
|
let sessionCount = 0;
|
|
@@ -636,7 +635,7 @@ export function createSessionComputed() {
|
|
|
636
635
|
} else if (isPrev) {
|
|
637
636
|
prevTokenTotal += sessionTokens;
|
|
638
637
|
}
|
|
639
|
-
|
|
638
|
+
const model = typeof session.model === 'string' ? session.model.trim() : '';
|
|
640
639
|
if (isCurrent && model) {
|
|
641
640
|
modelMap.set(model, (modelMap.get(model) || 0) + 1);
|
|
642
641
|
}
|
|
@@ -667,20 +666,162 @@ export function createSessionComputed() {
|
|
|
667
666
|
}));
|
|
668
667
|
return {
|
|
669
668
|
dayKey,
|
|
670
|
-
compareEnabled,
|
|
671
669
|
prevKey,
|
|
672
670
|
sessionCount,
|
|
673
671
|
messageCount,
|
|
674
672
|
tokenTotal,
|
|
675
673
|
tokenLabel: formatUsageSummaryNumber(tokenTotal),
|
|
676
674
|
prevTokenTotal,
|
|
677
|
-
prevTokenLabel:
|
|
678
|
-
deltaTokenLabel:
|
|
675
|
+
prevTokenLabel: prevKey ? formatUsageSummaryNumber(prevTokenTotal) : null,
|
|
676
|
+
deltaTokenLabel: prevKey ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : null,
|
|
679
677
|
topSessions,
|
|
680
678
|
topModels
|
|
681
679
|
};
|
|
682
680
|
},
|
|
683
681
|
|
|
682
|
+
usageHeroMainValue() {
|
|
683
|
+
const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
|
|
684
|
+
? this.sessionUsageCharts.summary
|
|
685
|
+
: null;
|
|
686
|
+
if (!summary) return '0';
|
|
687
|
+
return formatCompactUsageSummaryNumber(summary.totalTokens || 0);
|
|
688
|
+
},
|
|
689
|
+
|
|
690
|
+
usageHeroSubLabel() {
|
|
691
|
+
const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
|
|
692
|
+
? this.sessionUsageCharts.summary
|
|
693
|
+
: null;
|
|
694
|
+
if (!summary) return '';
|
|
695
|
+
const t = typeof this.t === 'function' ? this.t : null;
|
|
696
|
+
const sessionCount = summary.totalSessions || 0;
|
|
697
|
+
const rangeLabel = this.sessionsUsageTimeRange === '30d' ? '30天' : (this.sessionsUsageTimeRange === 'all' ? '全部' : '7天');
|
|
698
|
+
const rangeText = t ? t('usage.range.' + this.sessionsUsageTimeRange) : rangeLabel;
|
|
699
|
+
return `${formatUsageSummaryNumber(sessionCount)} sessions · ${rangeText}`;
|
|
700
|
+
},
|
|
701
|
+
|
|
702
|
+
usageHeroDelta() {
|
|
703
|
+
const range = this.sessionsUsageTimeRange;
|
|
704
|
+
if (range === 'all') return null;
|
|
705
|
+
const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
|
|
706
|
+
? this.sessionUsageCharts.summary
|
|
707
|
+
: null;
|
|
708
|
+
if (!summary || !summary.totalTokens) return null;
|
|
709
|
+
|
|
710
|
+
const rangeDays = range === '30d' ? 30 : 7;
|
|
711
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
712
|
+
const nowMs = Date.now();
|
|
713
|
+
const prevStartMs = nowMs - (rangeDays * 2 * dayMs);
|
|
714
|
+
const prevEndMs = nowMs - (rangeDays * dayMs);
|
|
715
|
+
|
|
716
|
+
let prevTokens = 0;
|
|
717
|
+
for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) {
|
|
718
|
+
if (!session || typeof session !== 'object') continue;
|
|
719
|
+
const updatedAtMs = Date.parse(session.updatedAt || '');
|
|
720
|
+
if (!Number.isFinite(updatedAtMs)) continue;
|
|
721
|
+
if (updatedAtMs >= prevStartMs && updatedAtMs < prevEndMs) {
|
|
722
|
+
const sessionTokens = Number.isFinite(Number(session.totalTokens))
|
|
723
|
+
? Math.max(0, Math.floor(Number(session.totalTokens)))
|
|
724
|
+
: 0;
|
|
725
|
+
prevTokens += sessionTokens;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (prevTokens === 0) return null;
|
|
730
|
+
const currentTokens = summary.totalTokens;
|
|
731
|
+
const delta = currentTokens - prevTokens;
|
|
732
|
+
const deltaPercent = prevTokens > 0 ? Math.round((delta / prevTokens) * 100) : 0;
|
|
733
|
+
const arrow = delta > 0 ? '↑' : (delta < 0 ? '↓' : '–');
|
|
734
|
+
const sign = delta >= 0 ? '+' : '';
|
|
735
|
+
return `${arrow} ${sign}${deltaPercent}%`;
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
usageHeroDeltaClass() {
|
|
739
|
+
const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
|
|
740
|
+
? this.sessionUsageCharts.summary
|
|
741
|
+
: null;
|
|
742
|
+
if (!summary || !summary.totalTokens) return '';
|
|
743
|
+
|
|
744
|
+
const range = this.sessionsUsageTimeRange;
|
|
745
|
+
if (range === 'all') return '';
|
|
746
|
+
|
|
747
|
+
const rangeDays = range === '30d' ? 30 : 7;
|
|
748
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
749
|
+
const nowMs = Date.now();
|
|
750
|
+
const prevStartMs = nowMs - (rangeDays * 2 * dayMs);
|
|
751
|
+
const prevEndMs = nowMs - (rangeDays * dayMs);
|
|
752
|
+
|
|
753
|
+
let prevTokens = 0;
|
|
754
|
+
for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) {
|
|
755
|
+
if (!session || typeof session !== 'object') continue;
|
|
756
|
+
const updatedAtMs = Date.parse(session.updatedAt || '');
|
|
757
|
+
if (!Number.isFinite(updatedAtMs)) continue;
|
|
758
|
+
if (updatedAtMs >= prevStartMs && updatedAtMs < prevEndMs) {
|
|
759
|
+
const sessionTokens = Number.isFinite(Number(session.totalTokens))
|
|
760
|
+
? Math.max(0, Math.floor(Number(session.totalTokens)))
|
|
761
|
+
: 0;
|
|
762
|
+
prevTokens += sessionTokens;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (prevTokens === 0) return '';
|
|
767
|
+
const currentTokens = summary.totalTokens;
|
|
768
|
+
return currentTokens >= prevTokens ? 'delta-up' : 'delta-down';
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
sessionsUsageSelectedDay() {
|
|
772
|
+
return this.sessionsUsageSelectedDayKey || '';
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
sessionUsageWave() {
|
|
776
|
+
const daily = this.sessionUsageDaily && typeof this.sessionUsageDaily === 'object'
|
|
777
|
+
? this.sessionUsageDaily
|
|
778
|
+
: null;
|
|
779
|
+
if (!daily || !Array.isArray(daily.rows) || daily.rows.length === 0) {
|
|
780
|
+
return { points: [], labels: [], linePath: '', areaPath: '', width: 800, maxTokens: 0 };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const rows = daily.rows;
|
|
784
|
+
const maxTokens = daily.maxTokens || 1;
|
|
785
|
+
const width = 800;
|
|
786
|
+
const height = 140;
|
|
787
|
+
const padding = { top: 10, bottom: 30, left: 0, right: 0 };
|
|
788
|
+
const chartWidth = width - padding.left - padding.right;
|
|
789
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
790
|
+
|
|
791
|
+
const points = rows.map((row, index) => {
|
|
792
|
+
const x = padding.left + (index / (rows.length - 1 || 1)) * chartWidth;
|
|
793
|
+
const normalizedValue = maxTokens > 0 ? (row.tokenTotal / maxTokens) : 0;
|
|
794
|
+
const y = padding.top + chartHeight - (normalizedValue * chartHeight);
|
|
795
|
+
return { x, y, key: row.key, value: row.tokenTotal, label: row.label };
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const linePath = points.length > 1
|
|
799
|
+
? `M ${points.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' L ')}`
|
|
800
|
+
: '';
|
|
801
|
+
|
|
802
|
+
const areaPath = points.length > 1
|
|
803
|
+
? `${linePath} L ${points[points.length - 1].x.toFixed(1)},${(padding.top + chartHeight).toFixed(1)} L ${points[0].x.toFixed(1)},${(padding.top + chartHeight).toFixed(1)} Z`
|
|
804
|
+
: '';
|
|
805
|
+
|
|
806
|
+
const selectedKey = this.sessionsUsageSelectedDayKey;
|
|
807
|
+
const selectedPoint = points.find(p => p.key === selectedKey) || points[points.length - 1] || null;
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
points,
|
|
811
|
+
labels: rows.map((row, index) => ({
|
|
812
|
+
key: row.key,
|
|
813
|
+
text: row.label
|
|
814
|
+
})),
|
|
815
|
+
linePath,
|
|
816
|
+
areaPath,
|
|
817
|
+
width,
|
|
818
|
+
height,
|
|
819
|
+
maxTokens,
|
|
820
|
+
hoverX: selectedPoint ? selectedPoint.x : 0,
|
|
821
|
+
hoverY: selectedPoint ? selectedPoint.y : 0
|
|
822
|
+
};
|
|
823
|
+
},
|
|
824
|
+
|
|
684
825
|
visibleSessionTrashItems() {
|
|
685
826
|
const items = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
|
|
686
827
|
const visibleCount = Number(this.sessionTrashVisibleCount);
|
|
@@ -266,6 +266,10 @@ export function createClaudeConfigMethods(options = {}) {
|
|
|
266
266
|
return this.claudeLocalBridgeCandidateProviders().some(p => p.hasKey);
|
|
267
267
|
},
|
|
268
268
|
|
|
269
|
+
isClaudeLocalBridgeDisabled() {
|
|
270
|
+
return this.configMode === 'claude';
|
|
271
|
+
},
|
|
272
|
+
|
|
269
273
|
async applyClaudeLocalBridge() {
|
|
270
274
|
this.currentClaudeConfig = 'claude-local';
|
|
271
275
|
try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {}
|
|
@@ -56,6 +56,32 @@
|
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
|
+
|
|
60
|
+
const canonicalizeWebUiRuntimeUrl = () => {
|
|
61
|
+
if (typeof window === 'undefined' || !window.location) return;
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(window.location.href);
|
|
64
|
+
if (url.pathname === '/session') return;
|
|
65
|
+
let pathname = url.pathname;
|
|
66
|
+
let previousPathname = '';
|
|
67
|
+
do {
|
|
68
|
+
previousPathname = pathname;
|
|
69
|
+
pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/, '/web-ui/');
|
|
70
|
+
} while (pathname !== previousPathname);
|
|
71
|
+
if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') {
|
|
72
|
+
pathname = '/';
|
|
73
|
+
}
|
|
74
|
+
url.pathname = pathname;
|
|
75
|
+
url.search = '';
|
|
76
|
+
url.hash = '';
|
|
77
|
+
const nextUrl = url.toString();
|
|
78
|
+
if (nextUrl === window.location.href) return;
|
|
79
|
+
if (window.history && typeof window.history.replaceState === 'function') {
|
|
80
|
+
window.history.replaceState(null, '', nextUrl);
|
|
81
|
+
}
|
|
82
|
+
} catch (_) {}
|
|
83
|
+
};
|
|
84
|
+
|
|
59
85
|
const persistNavState = (vm, overrides = null) => {
|
|
60
86
|
if (!vm || vm.__navStateRestoring) return;
|
|
61
87
|
if (typeof localStorage === 'undefined') return;
|
|
@@ -138,6 +164,7 @@
|
|
|
138
164
|
const normalizedMode = typeof mode === 'string'
|
|
139
165
|
? mode.trim().toLowerCase()
|
|
140
166
|
: '';
|
|
167
|
+
canonicalizeWebUiRuntimeUrl();
|
|
141
168
|
this.cancelTouchNavIntentReset();
|
|
142
169
|
if (typeof this.ensureMainTabSwitchState === 'function') {
|
|
143
170
|
this.ensureMainTabSwitchState().pendingConfigMode = '';
|
|
@@ -412,6 +439,7 @@
|
|
|
412
439
|
: '';
|
|
413
440
|
const targetTab = normalizedTab || tab;
|
|
414
441
|
if (!targetTab) return;
|
|
442
|
+
canonicalizeWebUiRuntimeUrl();
|
|
415
443
|
if (targetTab === 'orchestration' && this.taskOrchestrationTabEnabled !== true) {
|
|
416
444
|
return this.switchMainTab('config');
|
|
417
445
|
}
|
|
@@ -419,20 +447,7 @@
|
|
|
419
447
|
mainTab: targetTab,
|
|
420
448
|
configMode: targetTab === 'config' ? this.configMode : this.configMode
|
|
421
449
|
});
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
const url = new URL(window.location.href);
|
|
425
|
-
if (url.pathname !== '/session') {
|
|
426
|
-
url.searchParams.delete('s_source');
|
|
427
|
-
url.searchParams.delete('s_path');
|
|
428
|
-
url.searchParams.delete('s_query');
|
|
429
|
-
url.searchParams.delete('s_role');
|
|
430
|
-
url.searchParams.delete('s_time');
|
|
431
|
-
url.searchParams.delete('tab');
|
|
432
|
-
window.history.replaceState(null, '', url.toString());
|
|
433
|
-
}
|
|
434
|
-
} catch (_) {}
|
|
435
|
-
}
|
|
450
|
+
// URL 保持静态,不写入任何状态
|
|
436
451
|
this.cancelTouchNavIntentReset();
|
|
437
452
|
if (targetTab === 'sessions') {
|
|
438
453
|
this.cancelScheduledSessionTabDeferredTeardown();
|
|
@@ -474,9 +489,10 @@
|
|
|
474
489
|
return;
|
|
475
490
|
}
|
|
476
491
|
const isLeavingSessions = previousTab === 'sessions' && targetTab !== 'sessions';
|
|
477
|
-
const
|
|
492
|
+
const shouldPreserveSessionRender = isLeavingSessions && this.preserveSessionRenderOnTabLeave === true;
|
|
493
|
+
const shouldDeferApply = isLeavingSessions && !shouldPreserveSessionRender;
|
|
478
494
|
if (isLeavingSessions && !this.isSessionPanelFastHidden()) {
|
|
479
|
-
this.setSessionPanelFastHidden(
|
|
495
|
+
this.setSessionPanelFastHidden(!shouldPreserveSessionRender);
|
|
480
496
|
}
|
|
481
497
|
if (shouldDeferApply && typeof this.suspendSessionTabRender === 'function') {
|
|
482
498
|
this.suspendSessionTabRender();
|
|
@@ -264,6 +264,13 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
264
264
|
});
|
|
265
265
|
if (urlState) {
|
|
266
266
|
applySessionsFilterUrlState(this, urlState);
|
|
267
|
+
// 清理 URL,保持静态
|
|
268
|
+
try {
|
|
269
|
+
const url = new URL(window.location.href);
|
|
270
|
+
url.search = '';
|
|
271
|
+
url.hash = '';
|
|
272
|
+
window.history.replaceState(null, '', url.toString());
|
|
273
|
+
} catch (_) {}
|
|
267
274
|
try {
|
|
268
275
|
const sortCache = localStorage.getItem('codexmateSessionSortMode');
|
|
269
276
|
this.sessionSortMode = normalizeSortMode(sortCache);
|