codexmate 0.0.33 → 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/agents-files.js +6 -0
- package/cli/archive-helpers.js +11 -4
- package/cli/local-bridge.js +9 -4
- package/cli/openai-bridge.js +1 -1
- package/cli/update.js +11 -2
- package/cli.js +133 -64
- package/lib/cli-webhook.js +29 -1
- package/package.json +2 -1
- package/web-ui/app.js +37 -2
- package/web-ui/index.html +2 -1
- package/web-ui/logic.claude.mjs +4 -0
- 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 +41 -0
- package/web-ui/modules/app.methods.codex-config.mjs +11 -3
- package/web-ui/modules/app.methods.navigation.mjs +32 -2
- package/web-ui/modules/app.methods.session-browser.mjs +12 -6
- package/web-ui/modules/app.methods.session-trash.mjs +30 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +9 -0
- package/web-ui/modules/app.methods.webhook.mjs +8 -0
- package/web-ui/modules/i18n.dict.mjs +8 -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/modal-webhook.html +42 -0
- package/web-ui/partials/index/modals-basic.html +50 -0
- package/web-ui/partials/index/panel-config-claude.html +13 -22
- package/web-ui/partials/index/panel-config-codex.html +8 -22
- 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 +119 -149
- 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/bridge-pool.css +69 -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/settings-panel.css +300 -234
- package/web-ui/styles/skills-market.css +294 -0
- package/web-ui/styles/titles-cards.css +14 -0
- package/web-ui/styles/webhook.css +38 -4
package/cli/agents-files.js
CHANGED
|
@@ -92,6 +92,9 @@ function createAgentsFileController(deps = {}) {
|
|
|
92
92
|
function applyClaudeMdFile(params = {}) {
|
|
93
93
|
const filePath = resolveClaudeMdFilePath();
|
|
94
94
|
const content = typeof params.content === 'string' ? params.content : '';
|
|
95
|
+
if (content.length > 2 * 1024 * 1024) {
|
|
96
|
+
return { error: '内容过大(最大 2MB)' };
|
|
97
|
+
}
|
|
95
98
|
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
96
99
|
const normalized = normalizeLineEnding(content, lineEnding);
|
|
97
100
|
const finalContent = ensureUtf8Bom(normalized);
|
|
@@ -150,6 +153,9 @@ function createAgentsFileController(deps = {}) {
|
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
const content = typeof params.content === 'string' ? params.content : '';
|
|
156
|
+
if (content.length > 2 * 1024 * 1024) {
|
|
157
|
+
return { error: '内容过大(最大 2MB)' };
|
|
158
|
+
}
|
|
153
159
|
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
154
160
|
const normalized = normalizeLineEnding(content, lineEnding);
|
|
155
161
|
const finalContent = ensureUtf8Bom(normalized);
|
package/cli/archive-helpers.js
CHANGED
|
@@ -4,6 +4,7 @@ function createArchiveHelperController(deps = {}) {
|
|
|
4
4
|
path,
|
|
5
5
|
os,
|
|
6
6
|
execSync,
|
|
7
|
+
execFileSync,
|
|
7
8
|
zipLib,
|
|
8
9
|
yauzl,
|
|
9
10
|
ensureDir,
|
|
@@ -341,8 +342,11 @@ function createArchiveHelperController(deps = {}) {
|
|
|
341
342
|
|
|
342
343
|
const zipTool = resolveZipTool();
|
|
343
344
|
if (zipTool.type === 'zip') {
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
if (typeof execFileSync === 'function') {
|
|
346
|
+
execFileSync(zipTool.cmd, ['-0', '-q', '-r', zipFilePath, dirPath], { stdio: 'ignore' });
|
|
347
|
+
} else {
|
|
348
|
+
execSync(`"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${dirPath}"`, { stdio: 'ignore' });
|
|
349
|
+
}
|
|
346
350
|
} else {
|
|
347
351
|
await zipWithLibrary(dirPath, zipFilePath);
|
|
348
352
|
}
|
|
@@ -371,8 +375,11 @@ function createArchiveHelperController(deps = {}) {
|
|
|
371
375
|
|
|
372
376
|
try {
|
|
373
377
|
if (zipTool.type === 'zip') {
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
if (typeof execFileSync === 'function') {
|
|
379
|
+
execFileSync(zipTool.cmd, ['-0', '-q', '-r', zipFilePath, dirPath], { stdio: 'ignore' });
|
|
380
|
+
} else {
|
|
381
|
+
execSync(`"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${dirPath}"`, { stdio: 'ignore' });
|
|
382
|
+
}
|
|
376
383
|
} else {
|
|
377
384
|
await zipWithLibrary(dirPath, zipFilePath);
|
|
378
385
|
}
|
package/cli/local-bridge.js
CHANGED
|
@@ -74,7 +74,7 @@ function buildClaudeUpstreamPool(claudeProvidersFile, excludedProviders) {
|
|
|
74
74
|
if (excludedSet.has(name.toLowerCase())) continue;
|
|
75
75
|
const baseUrl = typeof p.baseUrl === 'string' ? p.baseUrl.trim() : '';
|
|
76
76
|
if (!baseUrl || !isValidHttpUrl(normalizeBaseUrl(baseUrl))) continue;
|
|
77
|
-
pool.push({ name, baseUrl: normalizeBaseUrl(baseUrl), apiKey: typeof p.apiKey === 'string' ? p.apiKey : '' });
|
|
77
|
+
pool.push({ name, baseUrl: normalizeBaseUrl(baseUrl), apiKey: typeof p.apiKey === 'string' ? p.apiKey : '', model: typeof p.model === 'string' ? p.model.trim() : '' });
|
|
78
78
|
}
|
|
79
79
|
if (pool.length === 0) return { error: '请先添加可用的 Claude 上游提供商' };
|
|
80
80
|
return { pool };
|
|
@@ -271,7 +271,7 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
273
273
|
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
274
|
-
res.end(JSON.stringify({ object: 'codexmate.claude_local_bridge', provider: entry.name, status: 'ok', pool: pool.map(p => p.name) }));
|
|
274
|
+
res.end(JSON.stringify({ object: 'codexmate.claude_local_bridge', provider: entry.name, model: entry.model || '', status: 'ok', pool: pool.map(p => p.name) }));
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -285,7 +285,12 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
285
285
|
|
|
286
286
|
let parsedBody;
|
|
287
287
|
try { parsedBody = bodyResult.body ? JSON.parse(bodyResult.body) : {}; } catch (_) { parsedBody = {}; }
|
|
288
|
+
// Override model to match the selected upstream provider
|
|
289
|
+
if (entry.model && parsedBody && typeof parsedBody === 'object') {
|
|
290
|
+
parsedBody.model = entry.model;
|
|
291
|
+
}
|
|
288
292
|
const wantsStream = !!(parsedBody && parsedBody.stream);
|
|
293
|
+
const bodyToForward = JSON.stringify(parsedBody);
|
|
289
294
|
const upstreamUrl = joinApiUrl(entry.baseUrl.replace(/\/+$/, ''), suffix);
|
|
290
295
|
const headers = { 'Content-Type': 'application/json' };
|
|
291
296
|
if (entry.apiKey) {
|
|
@@ -300,7 +305,7 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
300
305
|
// Streaming proxy: pipe upstream SSE directly to client
|
|
301
306
|
const upstreamResult = await streamClaudeUpstream(upstreamUrl, {
|
|
302
307
|
method: req.method || 'POST',
|
|
303
|
-
body:
|
|
308
|
+
body: bodyToForward,
|
|
304
309
|
headers,
|
|
305
310
|
maxBytes: maxUpstreamBytes,
|
|
306
311
|
httpAgent,
|
|
@@ -322,7 +327,7 @@ function createLocalBridgeHttpHandler(options = {}) {
|
|
|
322
327
|
// Non-streaming proxy
|
|
323
328
|
const upstreamResult = await retryTransientRequest(() => proxyRequestJson(upstreamUrl, {
|
|
324
329
|
method: req.method || 'POST',
|
|
325
|
-
body:
|
|
330
|
+
body: bodyToForward || null,
|
|
326
331
|
headers,
|
|
327
332
|
maxBytes: maxUpstreamBytes,
|
|
328
333
|
httpAgent,
|
package/cli/openai-bridge.js
CHANGED
|
@@ -5,7 +5,7 @@ const { StringDecoder } = require('string_decoder');
|
|
|
5
5
|
const { readJsonFile, writeJsonAtomic } = require('../lib/cli-file-utils');
|
|
6
6
|
const { isValidHttpUrl, normalizeBaseUrl, joinApiUrl } = require('../lib/cli-utils');
|
|
7
7
|
|
|
8
|
-
const DEFAULT_BRIDGE_TOKEN = '
|
|
8
|
+
const DEFAULT_BRIDGE_TOKEN = crypto.randomBytes(16).toString('hex');
|
|
9
9
|
const SETTINGS_VERSION = 1;
|
|
10
10
|
// 推理模型 reasoning 阶段可能长时间无字节输出,需匹配 codex 的 stream_idle_timeout_ms=300000。
|
|
11
11
|
const STREAM_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
package/cli/update.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
const https = require('https');
|
|
4
5
|
const { execSync, spawn } = require('child_process');
|
|
5
6
|
|
|
@@ -147,8 +148,16 @@ function updateViaStandalone(version) {
|
|
|
147
148
|
if (process.platform !== 'win32') {
|
|
148
149
|
console.log('[Update] 尝试自动执行安装脚本...');
|
|
149
150
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
151
|
+
const crypto = require('crypto');
|
|
152
|
+
const tmpScript = path.join(os.tmpdir(), `codexmate-install-${Date.now()}.sh`);
|
|
153
|
+
const { execFileSync, execSync: _unused } = require('child_process');
|
|
154
|
+
execSync(`curl -fsSL -o "${tmpScript}" https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scripts/install.sh`, { stdio: 'inherit' });
|
|
155
|
+
const scriptContent = fs.readFileSync(tmpScript, 'utf-8');
|
|
156
|
+
const checksum = crypto.createHash('sha256').update(scriptContent).digest('hex');
|
|
157
|
+
console.log(`[Update] Script checksum: ${checksum}`);
|
|
158
|
+
fs.chmodSync(tmpScript, 0o755);
|
|
159
|
+
execFileSync('bash', [tmpScript], { stdio: 'inherit' });
|
|
160
|
+
try { fs.unlinkSync(tmpScript); } catch (_) {}
|
|
152
161
|
} catch (e) {
|
|
153
162
|
console.warn('[Update] 自动脚本执行失败,请手动运行。');
|
|
154
163
|
}
|
package/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const toml = require('@iarna/toml');
|
|
|
7
7
|
const JSON5 = require('json5');
|
|
8
8
|
const zipLib = require('zip-lib');
|
|
9
9
|
const yauzl = require('yauzl');
|
|
10
|
-
const { exec, execSync, spawn, spawnSync } = require('child_process');
|
|
10
|
+
const { exec, execSync, execFileSync, spawn, spawnSync } = require('child_process');
|
|
11
11
|
const http = require('http');
|
|
12
12
|
const https = require('https');
|
|
13
13
|
const net = require('net');
|
|
@@ -175,7 +175,7 @@ const {
|
|
|
175
175
|
} = require('./lib/download-artifacts');
|
|
176
176
|
|
|
177
177
|
const DEFAULT_WEB_PORT = 3737;
|
|
178
|
-
const DEFAULT_WEB_HOST = '
|
|
178
|
+
const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
179
179
|
const DEFAULT_WEB_OPEN_HOST = '127.0.0.1';
|
|
180
180
|
|
|
181
181
|
// ============================================================================
|
|
@@ -758,7 +758,7 @@ function updateAuthJson(apiKey) {
|
|
|
758
758
|
} catch (e) { }
|
|
759
759
|
}
|
|
760
760
|
authData['OPENAI_API_KEY'] = apiKey;
|
|
761
|
-
fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), 'utf-8');
|
|
761
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
762
762
|
}
|
|
763
763
|
|
|
764
764
|
function isPlainObject(value) {
|
|
@@ -1711,6 +1711,7 @@ const {
|
|
|
1711
1711
|
path,
|
|
1712
1712
|
os,
|
|
1713
1713
|
execSync,
|
|
1714
|
+
execFileSync,
|
|
1714
1715
|
zipLib,
|
|
1715
1716
|
yauzl,
|
|
1716
1717
|
ensureDir,
|
|
@@ -4234,6 +4235,9 @@ function parseClaudeSessionSummary(filePath, options = {}) {
|
|
|
4234
4235
|
|
|
4235
4236
|
const tailRecords = parseJsonlTailRecords(filePath, summaryReadBytes);
|
|
4236
4237
|
for (const record of tailRecords) {
|
|
4238
|
+
if (record && record.timestamp) {
|
|
4239
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
4240
|
+
}
|
|
4237
4241
|
applySessionUsageSummaryFromRecord(usageState, record, 'claude');
|
|
4238
4242
|
totalTokens = usageState.totalTokens || 0;
|
|
4239
4243
|
contextWindow = usageState.contextWindow || 0;
|
|
@@ -4741,8 +4745,8 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4741
4745
|
continue;
|
|
4742
4746
|
}
|
|
4743
4747
|
|
|
4744
|
-
|
|
4745
|
-
|
|
4748
|
+
let updatedAt = toIsoTime(entry.modified || entry.fileMtime, fileStat.mtime.toISOString());
|
|
4749
|
+
let createdAt = toIsoTime(entry.created, '');
|
|
4746
4750
|
let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
|
|
4747
4751
|
let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
|
|
4748
4752
|
let totalTokens = 0;
|
|
@@ -4774,6 +4778,12 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4774
4778
|
|
|
4775
4779
|
const quickMessages = [];
|
|
4776
4780
|
for (const record of quickRecords) {
|
|
4781
|
+
if (record && record.timestamp) {
|
|
4782
|
+
if (!createdAt) {
|
|
4783
|
+
createdAt = toIsoTime(record.timestamp, createdAt);
|
|
4784
|
+
}
|
|
4785
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
4786
|
+
}
|
|
4777
4787
|
applySessionUsageSummaryFromRecord(usageState, record, 'claude');
|
|
4778
4788
|
const recordModels = readSessionModelsFromRecord(record);
|
|
4779
4789
|
for (const recordModel of recordModels) {
|
|
@@ -4804,6 +4814,12 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4804
4814
|
|
|
4805
4815
|
const tailRecords = parseJsonlTailRecords(filePath, summaryReadBytes);
|
|
4806
4816
|
for (const record of tailRecords) {
|
|
4817
|
+
if (record && record.timestamp) {
|
|
4818
|
+
if (!createdAt) {
|
|
4819
|
+
createdAt = toIsoTime(record.timestamp, createdAt);
|
|
4820
|
+
}
|
|
4821
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
4822
|
+
}
|
|
4807
4823
|
applySessionUsageSummaryFromRecord(usageState, record, 'claude');
|
|
4808
4824
|
const recordModels = readSessionModelsFromRecord(record);
|
|
4809
4825
|
for (const recordModel of recordModels) {
|
|
@@ -4863,27 +4879,29 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
4863
4879
|
}
|
|
4864
4880
|
}
|
|
4865
4881
|
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
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
|
|
4871
4894
|
});
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
});
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
...summary,
|
|
4880
|
-
derived: isDerivedSessionFile(filePath)
|
|
4881
|
-
}));
|
|
4882
|
-
}
|
|
4895
|
+
if (summary) {
|
|
4896
|
+
sessions.push(attachSessionNativeStatus({
|
|
4897
|
+
...summary,
|
|
4898
|
+
derived: isDerivedSessionFile(filePath)
|
|
4899
|
+
}));
|
|
4900
|
+
seenFilePaths.add(filePath);
|
|
4901
|
+
}
|
|
4883
4902
|
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
}
|
|
4903
|
+
if (sessions.length >= targetCount) {
|
|
4904
|
+
break;
|
|
4887
4905
|
}
|
|
4888
4906
|
}
|
|
4889
4907
|
|
|
@@ -7892,7 +7910,7 @@ function normalizeImportPayload(payload) {
|
|
|
7892
7910
|
const name = item.name || item.provider || '';
|
|
7893
7911
|
const baseUrl = item.baseUrl || item.base_url || item.url || '';
|
|
7894
7912
|
const apiKey = item.apiKey ?? item.key ?? item.preferred_auth_method ?? null;
|
|
7895
|
-
if (name && baseUrl) {
|
|
7913
|
+
if (name && baseUrl && /^[a-zA-Z0-9_\-.\s]+$/.test(name)) {
|
|
7896
7914
|
providers[name] = { baseUrl, apiKey };
|
|
7897
7915
|
}
|
|
7898
7916
|
}
|
|
@@ -7901,7 +7919,7 @@ function normalizeImportPayload(payload) {
|
|
|
7901
7919
|
if (!item || typeof item !== 'object') continue;
|
|
7902
7920
|
const baseUrl = item.baseUrl || item.base_url || item.url || '';
|
|
7903
7921
|
const apiKey = item.apiKey ?? item.key ?? item.preferred_auth_method ?? null;
|
|
7904
|
-
if (name && baseUrl) {
|
|
7922
|
+
if (name && baseUrl && /^[a-zA-Z0-9_\-.\s]+$/.test(name)) {
|
|
7905
7923
|
providers[name] = { baseUrl, apiKey };
|
|
7906
7924
|
}
|
|
7907
7925
|
}
|
|
@@ -9237,6 +9255,9 @@ function applyClaudeSettingsRaw(params = {}) {
|
|
|
9237
9255
|
if (!content.trim()) {
|
|
9238
9256
|
return { error: '内容不能为空' };
|
|
9239
9257
|
}
|
|
9258
|
+
if (content.length > 1024 * 1024) {
|
|
9259
|
+
return { error: '内容过大(最大 1MB)' };
|
|
9260
|
+
}
|
|
9240
9261
|
let parsed;
|
|
9241
9262
|
try {
|
|
9242
9263
|
parsed = JSON.parse(content);
|
|
@@ -10039,9 +10060,10 @@ function watchPathsForRestart(targets, onChange) {
|
|
|
10039
10060
|
}
|
|
10040
10061
|
// #endregion watchPathsForRestart
|
|
10041
10062
|
|
|
10042
|
-
function writeJsonResponse(res, statusCode, payload) {
|
|
10063
|
+
function writeJsonResponse(res, statusCode, payload, headers = {}) {
|
|
10043
10064
|
const body = JSON.stringify(payload, null, 2);
|
|
10044
10065
|
res.writeHead(statusCode, {
|
|
10066
|
+
...headers,
|
|
10045
10067
|
'Content-Type': 'application/json; charset=utf-8',
|
|
10046
10068
|
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
10047
10069
|
});
|
|
@@ -10119,7 +10141,7 @@ function assertRequestAuthorized(req, res) {
|
|
|
10119
10141
|
return { ok: false, mode: 'missing-token' };
|
|
10120
10142
|
}
|
|
10121
10143
|
const actual = extractRequestToken(req);
|
|
10122
|
-
if (!actual || actual
|
|
10144
|
+
if (!actual || !safeTimingEqual(actual, expected)) {
|
|
10123
10145
|
writeJsonResponse(res, 401, { error: 'Unauthorized' });
|
|
10124
10146
|
return { ok: false, mode: 'unauthorized' };
|
|
10125
10147
|
}
|
|
@@ -10576,7 +10598,38 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
10576
10598
|
res.end('Internal Server Error');
|
|
10577
10599
|
};
|
|
10578
10600
|
|
|
10601
|
+
const rateLimitMap = new Map();
|
|
10602
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
10603
|
+
const RATE_LIMIT_MAX = 120;
|
|
10604
|
+
function checkRateLimit(key) {
|
|
10605
|
+
const now = Date.now();
|
|
10606
|
+
const entry = rateLimitMap.get(key);
|
|
10607
|
+
if (!entry || now - entry.start > RATE_LIMIT_WINDOW_MS) {
|
|
10608
|
+
rateLimitMap.set(key, { start: now, count: 1 });
|
|
10609
|
+
return true;
|
|
10610
|
+
}
|
|
10611
|
+
entry.count++;
|
|
10612
|
+
if (entry.count > RATE_LIMIT_MAX) return false;
|
|
10613
|
+
return true;
|
|
10614
|
+
}
|
|
10615
|
+
setInterval(function () {
|
|
10616
|
+
const now = Date.now();
|
|
10617
|
+
for (const [key, entry] of rateLimitMap.entries()) {
|
|
10618
|
+
if (now - entry.start > RATE_LIMIT_WINDOW_MS * 2) rateLimitMap.delete(key);
|
|
10619
|
+
}
|
|
10620
|
+
}, RATE_LIMIT_WINDOW_MS).unref();
|
|
10621
|
+
|
|
10579
10622
|
const server = http.createServer((req, res) => {
|
|
10623
|
+
const securityHeaders = {
|
|
10624
|
+
'X-Content-Type-Options': 'nosniff',
|
|
10625
|
+
'X-Frame-Options': 'DENY',
|
|
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:"
|
|
10627
|
+
};
|
|
10628
|
+
const origWriteHead = res.writeHead.bind(res);
|
|
10629
|
+
res.writeHead = function (statusCode, headers) {
|
|
10630
|
+
const merged = Object.assign({}, securityHeaders, headers || {});
|
|
10631
|
+
return origWriteHead(statusCode, merged);
|
|
10632
|
+
};
|
|
10580
10633
|
const requestPath = (req.url || '/').split('?')[0];
|
|
10581
10634
|
const sendJson = (statusCode, payload) => {
|
|
10582
10635
|
const body = JSON.stringify(payload || {}, null, 2);
|
|
@@ -10599,28 +10652,15 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
10599
10652
|
|| requestPath.startsWith('/download/')
|
|
10600
10653
|
) {
|
|
10601
10654
|
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
|
|
10602
|
-
const isLoopback = !remoteAddr
|
|
10603
|
-
|| remoteAddr === '127.0.0.1'
|
|
10604
|
-
|| remoteAddr === '::1'
|
|
10605
|
-
|| remoteAddr === '::ffff:127.0.0.1';
|
|
10655
|
+
const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
|
|
10606
10656
|
if (!isLoopback) {
|
|
10607
|
-
const
|
|
10608
|
-
|
|
10609
|
-
: '';
|
|
10610
|
-
if (!expected) {
|
|
10611
|
-
sendJson(403, {
|
|
10612
|
-
error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN or use --host 127.0.0.1)'
|
|
10613
|
-
});
|
|
10657
|
+
const rateLimitKey = (remoteAddr || 'unknown') + ':' + requestPath;
|
|
10658
|
+
if (!checkRateLimit(rateLimitKey)) {
|
|
10659
|
+
writeJsonResponse(res, 429, { error: 'Rate limit exceeded' }, { 'Retry-After': '60' });
|
|
10614
10660
|
return;
|
|
10615
10661
|
}
|
|
10616
|
-
const
|
|
10617
|
-
|
|
10618
|
-
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
|
|
10619
|
-
const actual = match && match[1]
|
|
10620
|
-
? match[1].trim()
|
|
10621
|
-
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
|
|
10622
|
-
if (!actual || actual !== expected) {
|
|
10623
|
-
sendJson(401, { error: 'Unauthorized' });
|
|
10662
|
+
const auth = assertRequestAuthorized(req, res);
|
|
10663
|
+
if (!auth.ok) {
|
|
10624
10664
|
return;
|
|
10625
10665
|
}
|
|
10626
10666
|
}
|
|
@@ -11352,15 +11392,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11352
11392
|
res.end(errorBody, 'utf-8');
|
|
11353
11393
|
}
|
|
11354
11394
|
});
|
|
11355
|
-
} else if (requestPath === '/web-ui') {
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
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');
|
|
11363
11405
|
} else if (requestPath.startsWith('/web-ui/')) {
|
|
11406
|
+
// Skip the /web-ui/ directory itself, which is handled above
|
|
11364
11407
|
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
11365
11408
|
const filePath = path.join(__dirname, normalized);
|
|
11366
11409
|
if (!isPathInside(filePath, webDir)) {
|
|
@@ -11369,11 +11412,22 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11369
11412
|
return;
|
|
11370
11413
|
}
|
|
11371
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
|
+
|
|
11372
11423
|
const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
|
|
11373
11424
|
if (dynamicAsset) {
|
|
11374
11425
|
try {
|
|
11375
11426
|
const assetBody = dynamicAsset.reader(filePath);
|
|
11376
|
-
res.writeHead(200, {
|
|
11427
|
+
res.writeHead(200, {
|
|
11428
|
+
'Content-Type': dynamicAsset.mime,
|
|
11429
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11430
|
+
});
|
|
11377
11431
|
res.end(assetBody, 'utf-8');
|
|
11378
11432
|
} catch (error) {
|
|
11379
11433
|
writeWebUiAssetError(res, requestPath, error);
|
|
@@ -11400,7 +11454,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11400
11454
|
: ext === '.json'
|
|
11401
11455
|
? 'application/json; charset=utf-8'
|
|
11402
11456
|
: 'application/octet-stream';
|
|
11403
|
-
res.writeHead(200, {
|
|
11457
|
+
res.writeHead(200, {
|
|
11458
|
+
'Content-Type': mime,
|
|
11459
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11460
|
+
});
|
|
11404
11461
|
fs.createReadStream(filePath).pipe(res);
|
|
11405
11462
|
} else if (requestPath.startsWith('/download/')) {
|
|
11406
11463
|
const fileName = requestPath.slice('/download/'.length);
|
|
@@ -11461,15 +11518,27 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
11461
11518
|
: ext === '.json'
|
|
11462
11519
|
? 'application/json; charset=utf-8'
|
|
11463
11520
|
: 'application/octet-stream';
|
|
11464
|
-
res.writeHead(200, {
|
|
11521
|
+
res.writeHead(200, {
|
|
11522
|
+
'Content-Type': mime,
|
|
11523
|
+
'Cache-Control': 'no-store, max-age=0'
|
|
11524
|
+
});
|
|
11465
11525
|
fs.createReadStream(filePath).pipe(res);
|
|
11466
11526
|
} else {
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
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');
|
|
11473
11542
|
}
|
|
11474
11543
|
}
|
|
11475
11544
|
});
|
package/lib/cli-webhook.js
CHANGED
|
@@ -3,10 +3,33 @@ const fs = require('fs');
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const net = require('net');
|
|
6
7
|
|
|
7
8
|
const ALLOWED_EVENTS = ['provider-switch', 'claude-md-edit'];
|
|
8
9
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
9
10
|
|
|
11
|
+
function isPrivateNetworkHost(hostname) {
|
|
12
|
+
const host = typeof hostname === 'string' ? hostname.trim().toLowerCase() : '';
|
|
13
|
+
if (!host) return true;
|
|
14
|
+
if (host === 'localhost') return true;
|
|
15
|
+
const ipVer = net.isIP(host);
|
|
16
|
+
if (!ipVer) return false;
|
|
17
|
+
if (ipVer === 4) {
|
|
18
|
+
const parts = host.split('.').map(function (x) { return parseInt(x, 10); });
|
|
19
|
+
if (parts[0] === 10) return true;
|
|
20
|
+
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
21
|
+
if (parts[0] === 192 && parts[1] === 168) return true;
|
|
22
|
+
if (parts[0] === 127) return true;
|
|
23
|
+
if (parts[0] === 0) return true;
|
|
24
|
+
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (host === '::1' || host === '::') return true;
|
|
28
|
+
if (host.startsWith('fc') || host.startsWith('fd')) return true;
|
|
29
|
+
if (host.startsWith('fe80')) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
function defaultConfigPath() {
|
|
11
34
|
return path.join(os.homedir(), '.codex', 'codexmate-webhook.json');
|
|
12
35
|
}
|
|
@@ -42,7 +65,7 @@ function saveWebhookConfig(cfg, filePath) {
|
|
|
42
65
|
try {
|
|
43
66
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
44
67
|
} catch (_) {}
|
|
45
|
-
fs.writeFileSync(target, JSON.stringify(normalized, null, 2), 'utf-8');
|
|
68
|
+
fs.writeFileSync(target, JSON.stringify(normalized, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
46
69
|
return normalized;
|
|
47
70
|
}
|
|
48
71
|
|
|
@@ -55,6 +78,11 @@ function postJson(targetUrl, payload, timeoutMs) {
|
|
|
55
78
|
resolve({ ok: false, error: 'invalid-url' });
|
|
56
79
|
return;
|
|
57
80
|
}
|
|
81
|
+
const allowPrivateWebhook = process.env.CODEXMATE_ALLOW_PRIVATE_WEBHOOK === '1';
|
|
82
|
+
if (!allowPrivateWebhook && isPrivateNetworkHost(parsed.hostname || '')) {
|
|
83
|
+
resolve({ ok: false, error: 'Refusing to send webhook to private network host' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
58
86
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
59
87
|
const body = JSON.stringify(payload || {});
|
|
60
88
|
let req;
|
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',
|
|
@@ -71,6 +74,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
71
74
|
showAgentsModal: false,
|
|
72
75
|
showSkillsModal: false,
|
|
73
76
|
showHealthCheckModal: false,
|
|
77
|
+
showCodexBridgePoolModal: false,
|
|
78
|
+
showClaudeBridgePoolModal: false,
|
|
79
|
+
showWebhookModal: false,
|
|
74
80
|
// Plugins
|
|
75
81
|
pluginsActiveId: 'prompt-templates',
|
|
76
82
|
pluginsLoading: false,
|
|
@@ -98,6 +104,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
98
104
|
confirmDialogResolver: null,
|
|
99
105
|
configTemplateContent: '',
|
|
100
106
|
configTemplateApplying: false,
|
|
107
|
+
configTemplateContext: 'codex',
|
|
101
108
|
configTemplateDiffVisible: false,
|
|
102
109
|
configTemplateDiffLoading: false,
|
|
103
110
|
configTemplateDiffError: '',
|
|
@@ -226,6 +233,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
226
233
|
sessionPreviewHeaderEl: null,
|
|
227
234
|
sessionPreviewHeaderResizeObserver: null,
|
|
228
235
|
sessionListRenderEnabled: false,
|
|
236
|
+
preserveSessionRenderOnTabLeave: true,
|
|
229
237
|
sessionListVisibleCount: 0,
|
|
230
238
|
sessionListInitialBatchSize: 40,
|
|
231
239
|
sessionListLoadStep: 80,
|
|
@@ -416,6 +424,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
416
424
|
},
|
|
417
425
|
|
|
418
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
|
+
|
|
419
448
|
if (typeof this.initI18n === 'function') {
|
|
420
449
|
this.initI18n();
|
|
421
450
|
}
|
|
@@ -644,7 +673,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
644
673
|
|
|
645
674
|
computed: createAppComputed(),
|
|
646
675
|
methods: createAppMethods()
|
|
647
|
-
}
|
|
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);
|
|
648
683
|
|
|
649
684
|
app.mount('#app');
|
|
650
685
|
});
|