codexmate 0.0.34 → 0.0.37
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/README.md +14 -5
- package/README.zh.md +14 -5
- package/cli.js +74 -61
- package/package.json +2 -1
- package/web-ui/app.js +32 -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 +20 -11
- 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 +111 -68
- package/web-ui/res/vue.runtime.global.prod.js +7 -0
- package/web-ui/res/web-ui-render.precompiled.js +7269 -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 +65 -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 +501 -336
- package/web-ui/styles/skills-market.css +294 -0
- package/web-ui/styles/titles-cards.css +14 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
[](https://www.npmjs.com/package/codexmate)
|
|
16
16
|
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
17
17
|
[](https://www.npmjs.com/package/codexmate)
|
|
18
|
+
[](#install-via-homebrew-macos--linux)
|
|
18
19
|
[](#quick-start)
|
|
19
20
|
[](https://nodejs.org/)
|
|
20
21
|
[](LICENSE)
|
|
@@ -70,11 +71,19 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**:
|
|
|
70
71
|
|
|
71
72
|
## Quick Start
|
|
72
73
|
|
|
74
|
+
### Install via Homebrew (macOS / Linux)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
brew tap SakuraByteCore/codexmate
|
|
78
|
+
brew install codexmate
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Requires [Node.js](https://nodejs.org/) (`brew install node` if not present).
|
|
82
|
+
|
|
73
83
|
### Install via npm
|
|
74
84
|
|
|
75
85
|
```bash
|
|
76
86
|
npm install -g codexmate
|
|
77
|
-
codexmate setup
|
|
78
87
|
codexmate run
|
|
79
88
|
```
|
|
80
89
|
|
|
@@ -102,7 +111,7 @@ flowchart TD
|
|
|
102
111
|
CLI[CLI]
|
|
103
112
|
WebUI[Web UI]
|
|
104
113
|
MCP[MCP Server]
|
|
105
|
-
|
|
114
|
+
|
|
106
115
|
subgraph Mate [Codex Mate Core]
|
|
107
116
|
API[HTTP API]
|
|
108
117
|
Config[Config Engine]
|
|
@@ -110,7 +119,7 @@ flowchart TD
|
|
|
110
119
|
Skills[Skills Market]
|
|
111
120
|
Tasks[Task Runner]
|
|
112
121
|
end
|
|
113
|
-
|
|
122
|
+
|
|
114
123
|
subgraph Local [Local Filesystem]
|
|
115
124
|
CodexDir[~/.codex]
|
|
116
125
|
ClaudeDir[~/.claude]
|
|
@@ -120,9 +129,9 @@ flowchart TD
|
|
|
120
129
|
|
|
121
130
|
User --> CLI & WebUI & MCP
|
|
122
131
|
CLI & WebUI & MCP --> API
|
|
123
|
-
|
|
132
|
+
|
|
124
133
|
API --> Config & Session & Skills & Tasks
|
|
125
|
-
|
|
134
|
+
|
|
126
135
|
Config --> CodexDir & ClaudeDir & ClawDir
|
|
127
136
|
Session --> State
|
|
128
137
|
Skills --> Local
|
package/README.zh.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
[](https://www.npmjs.com/package/codexmate)
|
|
16
16
|
[](https://github.com/SakuraByteCore/codexmate/actions/workflows/release.yml)
|
|
17
17
|
[](https://www.npmjs.com/package/codexmate)
|
|
18
|
+
[](#homebrew-安装macos--linux)
|
|
18
19
|
[](#快速开始)
|
|
19
20
|
[](https://nodejs.org/)
|
|
20
21
|
[](LICENSE)
|
|
@@ -70,11 +71,19 @@
|
|
|
70
71
|
|
|
71
72
|
## 快速开始
|
|
72
73
|
|
|
74
|
+
### Homebrew 安装(macOS / Linux)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
brew tap SakuraByteCore/codexmate
|
|
78
|
+
brew install codexmate
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
需要 [Node.js](https://nodejs.org/)(如未安装可执行 `brew install node`)。
|
|
82
|
+
|
|
73
83
|
### 通过 npm 安装
|
|
74
84
|
|
|
75
85
|
```bash
|
|
76
86
|
npm install -g codexmate
|
|
77
|
-
codexmate setup
|
|
78
87
|
codexmate run
|
|
79
88
|
```
|
|
80
89
|
|
|
@@ -102,7 +111,7 @@ flowchart TD
|
|
|
102
111
|
CLI[CLI 命令]
|
|
103
112
|
WebUI[Web 界面]
|
|
104
113
|
MCP[MCP 服务]
|
|
105
|
-
|
|
114
|
+
|
|
106
115
|
subgraph Mate [Codex Mate 核心]
|
|
107
116
|
API[HTTP API]
|
|
108
117
|
Config[配置引擎]
|
|
@@ -110,7 +119,7 @@ flowchart TD
|
|
|
110
119
|
Skills[Skills 市场]
|
|
111
120
|
Tasks[任务运行器]
|
|
112
121
|
end
|
|
113
|
-
|
|
122
|
+
|
|
114
123
|
subgraph Local [本地文件系统]
|
|
115
124
|
CodexDir[~/.codex]
|
|
116
125
|
ClaudeDir[~/.claude]
|
|
@@ -120,9 +129,9 @@ flowchart TD
|
|
|
120
129
|
|
|
121
130
|
User --> CLI & WebUI & MCP
|
|
122
131
|
CLI & WebUI & MCP --> API
|
|
123
|
-
|
|
132
|
+
|
|
124
133
|
API --> Config & Session & Skills & Tasks
|
|
125
|
-
|
|
134
|
+
|
|
126
135
|
Config --> CodexDir & ClaudeDir & ClawDir
|
|
127
136
|
Session --> State
|
|
128
137
|
Skills --> Local
|
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.37",
|
|
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,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
26
28
|
|
|
27
29
|
const { createApp } = Vue;
|
|
28
30
|
|
|
29
|
-
const
|
|
31
|
+
const appOptions = {
|
|
30
32
|
data() {
|
|
31
33
|
return {
|
|
32
34
|
lang: 'zh',
|
|
@@ -230,6 +232,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
230
232
|
sessionPreviewHeaderEl: null,
|
|
231
233
|
sessionPreviewHeaderResizeObserver: null,
|
|
232
234
|
sessionListRenderEnabled: false,
|
|
235
|
+
preserveSessionRenderOnTabLeave: true,
|
|
233
236
|
sessionListVisibleCount: 0,
|
|
234
237
|
sessionListInitialBatchSize: 40,
|
|
235
238
|
sessionListLoadStep: 80,
|
|
@@ -420,6 +423,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
420
423
|
},
|
|
421
424
|
|
|
422
425
|
mounted() {
|
|
426
|
+
// URL 规范化:将 /web-ui/* 重定向到根路径 /
|
|
427
|
+
try {
|
|
428
|
+
const pathname = window.location.pathname;
|
|
429
|
+
if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') {
|
|
430
|
+
const url = new URL(window.location.href);
|
|
431
|
+
url.pathname = '/';
|
|
432
|
+
// 移除查询参数和 hash,保持 URL 纯净
|
|
433
|
+
url.search = '';
|
|
434
|
+
url.hash = '';
|
|
435
|
+
window.location.replace(url.toString());
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// 清理任何查询参数和 hash,保持 URL 为 /
|
|
439
|
+
if (window.location.search || window.location.hash) {
|
|
440
|
+
const url = new URL(window.location.href);
|
|
441
|
+
url.search = '';
|
|
442
|
+
url.hash = '';
|
|
443
|
+
window.history.replaceState(null, '', url.toString());
|
|
444
|
+
}
|
|
445
|
+
} catch (_) {}
|
|
446
|
+
|
|
423
447
|
if (typeof this.initI18n === 'function') {
|
|
424
448
|
this.initI18n();
|
|
425
449
|
}
|
|
@@ -648,7 +672,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
648
672
|
|
|
649
673
|
computed: createAppComputed(),
|
|
650
674
|
methods: createAppMethods()
|
|
651
|
-
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
if (typeof window.__CODEXMATE_WEB_UI_RENDER__ === 'function') {
|
|
678
|
+
appOptions.render = window.__CODEXMATE_WEB_UI_RENDER__;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const app = createApp(appOptions);
|
|
652
682
|
|
|
653
683
|
app.mount('#app');
|
|
654
684
|
});
|
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 (_) {}
|