codexmate 0.0.40 → 0.0.42
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 +12 -0
- package/README.zh.md +12 -0
- package/cli.js +159 -6
- package/package.json +1 -1
- package/web-ui/modules/app.methods.session-actions.mjs +26 -0
- package/web-ui/modules/i18n/locales/en.mjs +1 -0
- package/web-ui/modules/i18n/locales/ja.mjs +2 -0
- package/web-ui/modules/i18n/locales/zh.mjs +1 -0
- package/web-ui/partials/index/panel-sessions.html +6 -0
- package/web-ui/res/web-ui-render.precompiled.js +6 -1
package/README.md
CHANGED
|
@@ -87,6 +87,18 @@ npm install -g codexmate
|
|
|
87
87
|
codexmate run
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
If the default Web UI port `3737` is unavailable, Codex Mate automatically tries the next ports (`3738`, `3739`, ...). To force a fixed port, set `CODEXMATE_PORT`:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
CODEXMATE_PORT=8080 codexmate run
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Windows PowerShell:
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
$env:CODEXMATE_PORT=8080; codexmate run
|
|
100
|
+
```
|
|
101
|
+
|
|
90
102
|
### Install via curl (Standalone)
|
|
91
103
|
|
|
92
104
|
```bash
|
package/README.zh.md
CHANGED
|
@@ -87,6 +87,18 @@ npm install -g codexmate
|
|
|
87
87
|
codexmate run
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
如果默认 Web UI 端口 `3737` 不可用,Codex Mate 会自动尝试后续端口(`3738`、`3739` ...)。如需固定端口,可以指定 `CODEXMATE_PORT`:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
CODEXMATE_PORT=8080 codexmate run
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Windows PowerShell:
|
|
97
|
+
|
|
98
|
+
```powershell
|
|
99
|
+
$env:CODEXMATE_PORT=8080; codexmate run
|
|
100
|
+
```
|
|
101
|
+
|
|
90
102
|
### 通过 curl 安装 (独立包)
|
|
91
103
|
|
|
92
104
|
```bash
|
package/cli.js
CHANGED
|
@@ -359,6 +359,91 @@ function resolveWebPort() {
|
|
|
359
359
|
return parsed;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
function isWebPortExplicit() {
|
|
363
|
+
return typeof process.env.CODEXMATE_PORT === 'string' && process.env.CODEXMATE_PORT.trim().length > 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function resolveAvailableWebPort(port, host, options = {}) {
|
|
367
|
+
const explicitPort = !!options.explicitPort;
|
|
368
|
+
const maxAttemptsRaw = Number.isFinite(options.maxAttempts) ? options.maxAttempts : parseInt(options.maxAttempts, 10);
|
|
369
|
+
const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 20;
|
|
370
|
+
const netModule = options.net || net;
|
|
371
|
+
const requestedPort = parseInt(String(port), 10);
|
|
372
|
+
if (!Number.isFinite(requestedPort) || requestedPort <= 0 || explicitPort) {
|
|
373
|
+
return {
|
|
374
|
+
port,
|
|
375
|
+
requestedPort: port,
|
|
376
|
+
explicitPort,
|
|
377
|
+
changed: false,
|
|
378
|
+
attempts: []
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const attempts = [];
|
|
383
|
+
const checkPort = (candidatePort) => new Promise((resolve) => {
|
|
384
|
+
const tester = netModule.createServer();
|
|
385
|
+
let settled = false;
|
|
386
|
+
const finish = (result) => {
|
|
387
|
+
if (settled) return;
|
|
388
|
+
settled = true;
|
|
389
|
+
resolve(result);
|
|
390
|
+
};
|
|
391
|
+
tester.once('error', (error) => {
|
|
392
|
+
finish({
|
|
393
|
+
available: false,
|
|
394
|
+
code: error && error.code ? String(error.code) : '',
|
|
395
|
+
message: error && error.message ? String(error.message) : ''
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
tester.once('listening', () => {
|
|
399
|
+
tester.close(() => finish({ available: true, code: '', message: '' }));
|
|
400
|
+
});
|
|
401
|
+
try {
|
|
402
|
+
tester.listen(candidatePort, host);
|
|
403
|
+
} catch (error) {
|
|
404
|
+
finish({
|
|
405
|
+
available: false,
|
|
406
|
+
code: error && error.code ? String(error.code) : '',
|
|
407
|
+
message: error && error.message ? String(error.message) : String(error)
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const lastPort = Math.min(65535, requestedPort + maxAttempts - 1);
|
|
413
|
+
for (let candidatePort = requestedPort; candidatePort <= lastPort; candidatePort += 1) {
|
|
414
|
+
const result = await checkPort(candidatePort);
|
|
415
|
+
attempts.push({ port: candidatePort, available: !!result.available, code: result.code || '' });
|
|
416
|
+
if (result.available) {
|
|
417
|
+
return {
|
|
418
|
+
port: candidatePort,
|
|
419
|
+
requestedPort,
|
|
420
|
+
explicitPort: false,
|
|
421
|
+
changed: candidatePort !== requestedPort,
|
|
422
|
+
attempts
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (result.code !== 'EADDRINUSE' && result.code !== 'EACCES') {
|
|
426
|
+
return {
|
|
427
|
+
port: requestedPort,
|
|
428
|
+
requestedPort,
|
|
429
|
+
explicitPort: false,
|
|
430
|
+
changed: false,
|
|
431
|
+
attempts,
|
|
432
|
+
error: result.message || result.code || 'port probe failed'
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
port: requestedPort,
|
|
439
|
+
requestedPort,
|
|
440
|
+
explicitPort: false,
|
|
441
|
+
changed: false,
|
|
442
|
+
attempts,
|
|
443
|
+
error: `no available port found from ${requestedPort} to ${lastPort}`
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
362
447
|
// #region releaseRunPortIfNeeded
|
|
363
448
|
function releaseRunPortIfNeeded(port, host, deps = {}) {
|
|
364
449
|
const numericPort = parseInt(String(port), 10);
|
|
@@ -10327,8 +10412,20 @@ function extractRequestToken(req) {
|
|
|
10327
10412
|
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
|
|
10328
10413
|
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
|
|
10329
10414
|
if (rawAuth) {
|
|
10330
|
-
const
|
|
10331
|
-
if (
|
|
10415
|
+
const bearerMatch = rawAuth.match(/^bearer\s+(.+)$/i);
|
|
10416
|
+
if (bearerMatch && bearerMatch[1]) return bearerMatch[1].trim();
|
|
10417
|
+
const basicMatch = rawAuth.match(/^basic\s+(.+)$/i);
|
|
10418
|
+
if (basicMatch && basicMatch[1]) {
|
|
10419
|
+
try {
|
|
10420
|
+
const decoded = Buffer.from(basicMatch[1].trim(), 'base64').toString('utf-8');
|
|
10421
|
+
const separatorIndex = decoded.indexOf(':');
|
|
10422
|
+
if (separatorIndex >= 0) {
|
|
10423
|
+
const password = decoded.slice(separatorIndex + 1).trim();
|
|
10424
|
+
if (password) return password;
|
|
10425
|
+
}
|
|
10426
|
+
if (decoded.trim()) return decoded.trim();
|
|
10427
|
+
} catch (_) { }
|
|
10428
|
+
}
|
|
10332
10429
|
return rawAuth;
|
|
10333
10430
|
}
|
|
10334
10431
|
const raw = typeof headers['x-codexmate-token'] === 'string' ? headers['x-codexmate-token'].trim() : '';
|
|
@@ -10354,12 +10451,21 @@ function assertRequestAuthorized(req, res) {
|
|
|
10354
10451
|
}
|
|
10355
10452
|
const actual = extractRequestToken(req);
|
|
10356
10453
|
if (!actual || !safeTimingEqual(actual, expected)) {
|
|
10357
|
-
writeJsonResponse(res, 401, { error: 'Unauthorized' }
|
|
10454
|
+
writeJsonResponse(res, 401, { error: 'Unauthorized' }, {
|
|
10455
|
+
'WWW-Authenticate': 'Basic realm="codexmate"'
|
|
10456
|
+
});
|
|
10358
10457
|
return { ok: false, mode: 'unauthorized' };
|
|
10359
10458
|
}
|
|
10360
10459
|
return { ok: true, mode: 'token' };
|
|
10361
10460
|
}
|
|
10362
10461
|
|
|
10462
|
+
function isProtectedWebSurfacePath(requestPath) {
|
|
10463
|
+
return requestPath === '/'
|
|
10464
|
+
|| requestPath === '/web-ui/index.html'
|
|
10465
|
+
|| requestPath.startsWith('/web-ui/')
|
|
10466
|
+
|| requestPath.startsWith('/res/');
|
|
10467
|
+
}
|
|
10468
|
+
|
|
10363
10469
|
const g_webhookDeliveryCache = new Map();
|
|
10364
10470
|
|
|
10365
10471
|
function pruneWebhookDeliveryCache() {
|
|
@@ -10857,6 +10963,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
10857
10963
|
if (typeof openaiBridgeHandler === 'function' && openaiBridgeHandler(req, res)) {
|
|
10858
10964
|
return;
|
|
10859
10965
|
}
|
|
10966
|
+
if (isProtectedWebSurfacePath(requestPath)) {
|
|
10967
|
+
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
10968
|
+
const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
|
|
10969
|
+
if (!isLoopback) {
|
|
10970
|
+
const rateLimitKey = (remoteAddr || 'unknown') + ':' + requestPath;
|
|
10971
|
+
if (!checkRateLimit(rateLimitKey)) {
|
|
10972
|
+
writeJsonResponse(res, 429, { error: 'Rate limit exceeded' }, { 'Retry-After': '60' });
|
|
10973
|
+
return;
|
|
10974
|
+
}
|
|
10975
|
+
const auth = assertRequestAuthorized(req, res);
|
|
10976
|
+
if (!auth.ok) {
|
|
10977
|
+
return;
|
|
10978
|
+
}
|
|
10979
|
+
}
|
|
10980
|
+
}
|
|
10860
10981
|
if (
|
|
10861
10982
|
requestPath === '/api'
|
|
10862
10983
|
|| requestPath.startsWith('/api/import-')
|
|
@@ -11792,10 +11913,22 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11792
11913
|
socket.on('close', () => connections.delete(socket));
|
|
11793
11914
|
});
|
|
11794
11915
|
|
|
11916
|
+
const printPortOverrideHint = () => {
|
|
11917
|
+
const examplePort = port === 8080 ? 8081 : 8080;
|
|
11918
|
+
console.error(` 临时换端口(macOS/Linux): CODEXMATE_PORT=${examplePort} codexmate run`);
|
|
11919
|
+
console.error(` 临时换端口(Windows PowerShell): $env:CODEXMATE_PORT=${examplePort}; codexmate run`);
|
|
11920
|
+
console.error(` 临时换端口(Windows CMD): set CODEXMATE_PORT=${examplePort} && codexmate run`);
|
|
11921
|
+
};
|
|
11922
|
+
|
|
11795
11923
|
server.once('error', (err) => {
|
|
11796
11924
|
if (err && err.code === 'EADDRINUSE') {
|
|
11797
11925
|
console.error(`! 启动失败: 端口 ${port} 已被占用,可能有残留的 codexmate run 实例。`);
|
|
11798
11926
|
console.error(' 请先停止旧实例或更换端口后重试。');
|
|
11927
|
+
printPortOverrideHint();
|
|
11928
|
+
} else if (err && err.code === 'EACCES') {
|
|
11929
|
+
console.error(`! 启动失败: 没有权限监听 ${host}:${port}。`);
|
|
11930
|
+
console.error(' 请检查系统/安全软件限制,或更换端口后重试。');
|
|
11931
|
+
printPortOverrideHint();
|
|
11799
11932
|
} else {
|
|
11800
11933
|
console.error('! 启动 Web UI 失败:', err && err.message ? err.message : err);
|
|
11801
11934
|
}
|
|
@@ -11916,7 +12049,7 @@ async function restartWebUiServerAfterFrontendChange({
|
|
|
11916
12049
|
// #endregion restartWebUiServerAfterFrontendChange
|
|
11917
12050
|
|
|
11918
12051
|
// 打开 Web UI
|
|
11919
|
-
function cmdStart(options = {}) {
|
|
12052
|
+
async function cmdStart(options = {}) {
|
|
11920
12053
|
const webDir = path.join(__dirname, 'web-ui');
|
|
11921
12054
|
const newHtmlPath = path.join(webDir, 'index.html');
|
|
11922
12055
|
const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
|
|
@@ -11927,9 +12060,29 @@ function cmdStart(options = {}) {
|
|
|
11927
12060
|
process.exit(1);
|
|
11928
12061
|
}
|
|
11929
12062
|
|
|
11930
|
-
|
|
12063
|
+
let port = resolveWebPort();
|
|
12064
|
+
const explicitPort = isWebPortExplicit();
|
|
11931
12065
|
const host = resolveWebHost(options);
|
|
11932
12066
|
releaseRunPortIfNeeded(port, host);
|
|
12067
|
+
const selectedPort = await resolveAvailableWebPort(port, host, { explicitPort });
|
|
12068
|
+
if (selectedPort.error) {
|
|
12069
|
+
console.error(`! 启动失败: ${selectedPort.error}`);
|
|
12070
|
+
console.error(` 已尝试端口: ${selectedPort.attempts.map((attempt) => attempt.port).join(', ')}`);
|
|
12071
|
+
console.error(' 请设置 CODEXMATE_PORT 指定可用端口后重试。');
|
|
12072
|
+
process.exit(1);
|
|
12073
|
+
}
|
|
12074
|
+
if (selectedPort.changed) {
|
|
12075
|
+
const failed = selectedPort.attempts
|
|
12076
|
+
.filter((attempt) => !attempt.available)
|
|
12077
|
+
.map((attempt) => `${attempt.port}${attempt.code ? `(${attempt.code})` : ''}`)
|
|
12078
|
+
.join(', ');
|
|
12079
|
+
console.warn(`! 默认端口 ${selectedPort.requestedPort} 不可用,已自动切换到 ${selectedPort.port}。`);
|
|
12080
|
+
if (failed) {
|
|
12081
|
+
console.warn(` 跳过端口: ${failed}`);
|
|
12082
|
+
}
|
|
12083
|
+
console.warn(' 如需固定端口,请设置 CODEXMATE_PORT 后重新启动。');
|
|
12084
|
+
}
|
|
12085
|
+
port = selectedPort.port;
|
|
11933
12086
|
|
|
11934
12087
|
const isDev = process.env.NODE_ENV === 'development'
|
|
11935
12088
|
|| process.env.CODEXMATE_DEV === '1'
|
|
@@ -16301,7 +16454,7 @@ async function main() {
|
|
|
16301
16454
|
case 'workflow': await cmdWorkflow(args.slice(1)); break;
|
|
16302
16455
|
case 'task': await cmdTask(args.slice(1)); break;
|
|
16303
16456
|
case 'analytics': await cmdAnalytics(args.slice(1)); break;
|
|
16304
|
-
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
16457
|
+
case 'run': await cmdStart(parseStartOptions(args.slice(1))); break;
|
|
16305
16458
|
case 'update': await cmdToolUpdate(args.slice(1)); break;
|
|
16306
16459
|
case 'start':
|
|
16307
16460
|
console.error('错误: 命令已更名为 "run",请使用: codexmate run');
|
package/package.json
CHANGED
|
@@ -129,6 +129,32 @@ export function createSessionActionMethods(options = {}) {
|
|
|
129
129
|
this.showMessage('复制失败', 'error');
|
|
130
130
|
},
|
|
131
131
|
|
|
132
|
+
getSessionFilePath(session) {
|
|
133
|
+
const filePath = typeof session?.filePath === 'string' ? session.filePath.trim() : '';
|
|
134
|
+
return filePath;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
async copySessionPath(session) {
|
|
138
|
+
const filePath = this.getSessionFilePath(session);
|
|
139
|
+
if (!filePath) {
|
|
140
|
+
this.showMessage('无本地文件路径', 'error');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const ok = this.fallbackCopyText(filePath);
|
|
144
|
+
if (ok) {
|
|
145
|
+
this.showMessage('已复制路径', 'success');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
150
|
+
await navigator.clipboard.writeText(filePath);
|
|
151
|
+
this.showMessage('已复制路径', 'success');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
} catch (_) {}
|
|
155
|
+
this.showMessage('复制失败', 'error');
|
|
156
|
+
},
|
|
157
|
+
|
|
132
158
|
getSessionExportKey(session) {
|
|
133
159
|
return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
|
|
134
160
|
},
|
|
@@ -566,6 +566,7 @@ const en = Object.freeze({
|
|
|
566
566
|
'sessions.preview.importNative.failed': 'Import failed',
|
|
567
567
|
'sessions.preview.importNative.failedWithReason': 'Import to native failed: {reason}',
|
|
568
568
|
'sessions.preview.copyLink': 'Copy link',
|
|
569
|
+
'sessions.preview.copyPath': 'Copy path',
|
|
569
570
|
'sessions.preview.loadingBody': 'Loading session content...',
|
|
570
571
|
'sessions.preview.emptyMsgs': 'No messages to display',
|
|
571
572
|
'sessions.preview.rendering': 'Rendering session content...',
|
|
@@ -554,6 +554,8 @@ const ja = Object.freeze({
|
|
|
554
554
|
'sessions.preview.converting': '変換中...',
|
|
555
555
|
'sessions.preview.convert.loadedOnly': '読み込み済みのみ変換',
|
|
556
556
|
'sessions.preview.openStandalone': 'スタンドアロンで開く',
|
|
557
|
+
'sessions.preview.copyLink': 'リンクをコピー',
|
|
558
|
+
'sessions.preview.copyPath': 'パスをコピー',
|
|
557
559
|
'sessions.preview.loadingBody': 'メッセージ読み込み中...',
|
|
558
560
|
'sessions.preview.emptyMsgs': 'メッセージがありません',
|
|
559
561
|
'sessions.preview.rendering': 'レンダリング中...',
|
|
@@ -565,6 +565,7 @@ const zh = Object.freeze({
|
|
|
565
565
|
'sessions.preview.importNative.failed': '导入失败',
|
|
566
566
|
'sessions.preview.importNative.failedWithReason': '导入原生目录失败:{reason}',
|
|
567
567
|
'sessions.preview.copyLink': '复制链接',
|
|
568
|
+
'sessions.preview.copyPath': '复制路径',
|
|
568
569
|
'sessions.preview.loadingBody': '正在加载会话内容...',
|
|
569
570
|
'sessions.preview.emptyMsgs': '当前会话暂无可展示消息',
|
|
570
571
|
'sessions.preview.rendering': '正在渲染会话内容...',
|
|
@@ -241,6 +241,12 @@
|
|
|
241
241
|
:disabled="!activeSession">
|
|
242
242
|
{{ t('sessions.preview.copyLink') }}
|
|
243
243
|
</button>
|
|
244
|
+
<button
|
|
245
|
+
class="btn-session-open"
|
|
246
|
+
@click="copySessionPath(activeSession)"
|
|
247
|
+
:disabled="!activeSession || !getSessionFilePath(activeSession)">
|
|
248
|
+
{{ t('sessions.preview.copyPath') }}
|
|
249
|
+
</button>
|
|
244
250
|
</div>
|
|
245
251
|
</div>
|
|
246
252
|
|
|
@@ -2604,7 +2604,12 @@ return function render(_ctx, _cache) {
|
|
|
2604
2604
|
class: "btn-session-open",
|
|
2605
2605
|
onClick: $event => (_ctx.copySessionLink(_ctx.activeSession)),
|
|
2606
2606
|
disabled: !_ctx.activeSession
|
|
2607
|
-
}, _toDisplayString(_ctx.t('sessions.preview.copyLink')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])
|
|
2607
|
+
}, _toDisplayString(_ctx.t('sessions.preview.copyLink')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]),
|
|
2608
|
+
_createElementVNode("button", {
|
|
2609
|
+
class: "btn-session-open",
|
|
2610
|
+
onClick: $event => (_ctx.copySessionPath(_ctx.activeSession)),
|
|
2611
|
+
disabled: !_ctx.activeSession || !_ctx.getSessionFilePath(_ctx.activeSession)
|
|
2612
|
+
}, _toDisplayString(_ctx.t('sessions.preview.copyPath')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])
|
|
2608
2613
|
])
|
|
2609
2614
|
], 512 /* NEED_PATCH */),
|
|
2610
2615
|
(_ctx.sessionDetailLoading && !_ctx.sessionPreviewLoadingMore)
|