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 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 match = rawAuth.match(/^bearer\s+(.+)$/i);
10331
- if (match && match[1]) return match[1].trim();
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
- const port = resolveWebPort();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -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)