coding-tool-x 3.2.0
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/CHANGELOG.md +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求日志服务
|
|
3
|
+
*
|
|
4
|
+
* 通过环境变量控制是否记录各类请求日志:
|
|
5
|
+
*
|
|
6
|
+
* CC_TOOL_LOG_REQUESTS=true - 总开关:是否将代理请求快照写入 JSONL 文件(默认 false,需显式开启)
|
|
7
|
+
* CC_TOOL_LOG_API_REQUESTS=true - 是否将 Web UI REST API 请求写入日志文件(默认 false)
|
|
8
|
+
*
|
|
9
|
+
* 日志文件位置:
|
|
10
|
+
* ~/.cc-tool/claude-requests.jsonl
|
|
11
|
+
* ~/.cc-tool/codex-requests.jsonl
|
|
12
|
+
* ~/.cc-tool/gemini-requests.jsonl
|
|
13
|
+
* ~/.cc-tool/opencode-requests.jsonl
|
|
14
|
+
* ~/.cc-tool/logs/api-requests.jsonl
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
const CC_TOOL_DIR = path.join(os.homedir(), '.cc-tool');
|
|
22
|
+
|
|
23
|
+
function ensureDir(dir) {
|
|
24
|
+
if (!fs.existsSync(dir)) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 总开关:代理请求快照写文件
|
|
31
|
+
* 环境变量 CC_TOOL_LOG_REQUESTS=true 时开启,默认关闭(避免将含完整 messages 的请求体无限写入磁盘)
|
|
32
|
+
*/
|
|
33
|
+
function isProxyRequestLoggingEnabled() {
|
|
34
|
+
const val = process.env.CC_TOOL_LOG_REQUESTS;
|
|
35
|
+
if (!val) return false;
|
|
36
|
+
return val.toLowerCase() === 'true' || val === '1';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* REST API 请求日志开关
|
|
41
|
+
* 环境变量 CC_TOOL_LOG_API_REQUESTS=true 时开启,默认关闭
|
|
42
|
+
*/
|
|
43
|
+
function isApiRequestLoggingEnabled() {
|
|
44
|
+
const val = process.env.CC_TOOL_LOG_API_REQUESTS;
|
|
45
|
+
if (!val) return false;
|
|
46
|
+
return val.toLowerCase() === 'true' || val === '1';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 将代理请求快照异步追加写入 JSONL 文件
|
|
51
|
+
* @param {string} source - 来源类型: 'claude' | 'codex' | 'gemini' | 'opencode'
|
|
52
|
+
* @param {object} payload - 要写入的数据对象
|
|
53
|
+
*/
|
|
54
|
+
function persistProxyRequestSnapshot(source, payload) {
|
|
55
|
+
if (!isProxyRequestLoggingEnabled()) return;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
ensureDir(CC_TOOL_DIR);
|
|
59
|
+
const logPath = path.join(CC_TOOL_DIR, `${source}-requests.jsonl`);
|
|
60
|
+
fs.appendFile(logPath, `${JSON.stringify(payload)}\n`, (error) => {
|
|
61
|
+
if (error) {
|
|
62
|
+
console.error(`[request-logger] Failed to persist ${source} request snapshot:`, error);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`[request-logger] Failed to persist ${source} request snapshot:`, error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 创建 Express 中间件,用于记录 REST API 请求
|
|
72
|
+
* 仅当 CC_TOOL_LOG_API_REQUESTS=true 时生效
|
|
73
|
+
*/
|
|
74
|
+
function createApiRequestLogger() {
|
|
75
|
+
return function apiRequestLogger(req, res, next) {
|
|
76
|
+
if (!isApiRequestLoggingEnabled()) return next();
|
|
77
|
+
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
const originalJson = res.json.bind(res);
|
|
80
|
+
const originalSend = res.send.bind(res);
|
|
81
|
+
|
|
82
|
+
let statusCode = 200;
|
|
83
|
+
|
|
84
|
+
// 拦截响应以获取状态码和耗时
|
|
85
|
+
function logAndCallThrough(fn, body) {
|
|
86
|
+
statusCode = res.statusCode || 200;
|
|
87
|
+
const duration = Date.now() - startTime;
|
|
88
|
+
const entry = {
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
method: req.method,
|
|
91
|
+
path: req.path,
|
|
92
|
+
url: req.originalUrl || req.url,
|
|
93
|
+
statusCode,
|
|
94
|
+
duration,
|
|
95
|
+
ip: req.ip || req.connection?.remoteAddress || null,
|
|
96
|
+
userAgent: req.headers?.['user-agent'] || null
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
ensureDir(path.join(CC_TOOL_DIR, 'logs'));
|
|
101
|
+
const logPath = path.join(CC_TOOL_DIR, 'logs', 'api-requests.jsonl');
|
|
102
|
+
fs.appendFile(logPath, `${JSON.stringify(entry)}\n`, (err) => {
|
|
103
|
+
if (err) {
|
|
104
|
+
console.error('[request-logger] Failed to write API request log:', err);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error('[request-logger] Failed to write API request log:', err);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return fn(body);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.json = (body) => logAndCallThrough(originalJson, body);
|
|
115
|
+
res.send = (body) => {
|
|
116
|
+
// 对于非 JSON 响应,只在实际调用 send 时记录一次
|
|
117
|
+
if (res.headersSent) return originalSend(body);
|
|
118
|
+
return logAndCallThrough(originalSend, body);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
next();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
isProxyRequestLoggingEnabled,
|
|
127
|
+
isApiRequestLoggingEnabled,
|
|
128
|
+
persistProxyRequestSnapshot,
|
|
129
|
+
createApiRequestLogger
|
|
130
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const zlib = require('zlib');
|
|
2
|
+
|
|
3
|
+
function createDecodedStream(res) {
|
|
4
|
+
const encoding = String(res.headers['content-encoding'] || '').toLowerCase();
|
|
5
|
+
|
|
6
|
+
if (encoding.includes('gzip')) {
|
|
7
|
+
return res.pipe(zlib.createGunzip());
|
|
8
|
+
}
|
|
9
|
+
if (encoding.includes('deflate')) {
|
|
10
|
+
return res.pipe(zlib.createInflate());
|
|
11
|
+
}
|
|
12
|
+
if (encoding.includes('br') && typeof zlib.createBrotliDecompress === 'function') {
|
|
13
|
+
return res.pipe(zlib.createBrotliDecompress());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return res;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
createDecodedStream
|
|
21
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
const SECURITY_DIR = path.join(os.homedir(), '.cc-tool');
|
|
7
|
+
const SECURITY_FILE = path.join(SECURITY_DIR, 'security.json');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SECURITY_CONFIG = {
|
|
10
|
+
passwordHash: '',
|
|
11
|
+
salt: '',
|
|
12
|
+
updatedAt: null
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PBKDF2_ITERATIONS = 120000;
|
|
16
|
+
const PBKDF2_KEYLEN = 64;
|
|
17
|
+
const PBKDF2_DIGEST = 'sha512';
|
|
18
|
+
|
|
19
|
+
function ensureSecurityDir() {
|
|
20
|
+
if (!fs.existsSync(SECURITY_DIR)) {
|
|
21
|
+
fs.mkdirSync(SECURITY_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readSecurityConfig() {
|
|
26
|
+
ensureSecurityDir();
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(SECURITY_FILE)) {
|
|
29
|
+
return { ...DEFAULT_SECURITY_CONFIG };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = fs.readFileSync(SECURITY_FILE, 'utf8');
|
|
34
|
+
const data = JSON.parse(content);
|
|
35
|
+
return {
|
|
36
|
+
...DEFAULT_SECURITY_CONFIG,
|
|
37
|
+
...data
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error loading security config:', error);
|
|
41
|
+
return { ...DEFAULT_SECURITY_CONFIG };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function writeSecurityConfig(config) {
|
|
46
|
+
ensureSecurityDir();
|
|
47
|
+
fs.writeFileSync(SECURITY_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
48
|
+
try {
|
|
49
|
+
fs.chmodSync(SECURITY_FILE, 0o600);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn('Failed to set security config permissions:', error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasPassword(config) {
|
|
56
|
+
return Boolean(config.passwordHash && config.salt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hashPassword(password, salt) {
|
|
60
|
+
return crypto
|
|
61
|
+
.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, PBKDF2_KEYLEN, PBKDF2_DIGEST)
|
|
62
|
+
.toString('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function safeEqualHash(a, b) {
|
|
66
|
+
try {
|
|
67
|
+
const bufferA = Buffer.from(a, 'hex');
|
|
68
|
+
const bufferB = Buffer.from(b, 'hex');
|
|
69
|
+
if (bufferA.length !== bufferB.length) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return crypto.timingSafeEqual(bufferA, bufferB);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getSecurityStatus() {
|
|
79
|
+
const config = readSecurityConfig();
|
|
80
|
+
return { hasPassword: hasPassword(config) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function verifySecurityPassword(password) {
|
|
84
|
+
const config = readSecurityConfig();
|
|
85
|
+
if (!hasPassword(config)) {
|
|
86
|
+
return { ok: false, reason: 'not_set' };
|
|
87
|
+
}
|
|
88
|
+
const hash = hashPassword(password, config.salt);
|
|
89
|
+
return { ok: safeEqualHash(hash, config.passwordHash) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function setSecurityPassword({ currentPassword, newPassword }) {
|
|
93
|
+
if (typeof newPassword !== 'string' || newPassword.length < 4) {
|
|
94
|
+
const error = new Error('新密码至少 4 位');
|
|
95
|
+
error.code = 'WEAK_PASSWORD';
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const config = readSecurityConfig();
|
|
100
|
+
if (hasPassword(config)) {
|
|
101
|
+
if (typeof currentPassword !== 'string' || !currentPassword) {
|
|
102
|
+
const error = new Error('请输入当前密码');
|
|
103
|
+
error.code = 'CURRENT_REQUIRED';
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const verifyResult = verifySecurityPassword(currentPassword);
|
|
107
|
+
if (!verifyResult.ok) {
|
|
108
|
+
const error = new Error('当前密码错误');
|
|
109
|
+
error.code = 'INVALID_PASSWORD';
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
115
|
+
const passwordHash = hashPassword(newPassword, salt);
|
|
116
|
+
const nextConfig = {
|
|
117
|
+
...config,
|
|
118
|
+
passwordHash,
|
|
119
|
+
salt,
|
|
120
|
+
updatedAt: new Date().toISOString()
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
writeSecurityConfig(nextConfig);
|
|
124
|
+
return { hasPassword: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
getSecurityStatus,
|
|
129
|
+
verifySecurityPassword,
|
|
130
|
+
setSecurityPassword
|
|
131
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const PROJECTS_CACHE_TTL = 30 * 1000; // 30s
|
|
6
|
+
const projectsCache = new Map();
|
|
7
|
+
|
|
8
|
+
const HAS_MESSAGES_CACHE_LIMIT = 50000;
|
|
9
|
+
const hasMessagesCache = new Map();
|
|
10
|
+
let hasMessagesPersisted = {};
|
|
11
|
+
let hasMessagesPersistTimer = null;
|
|
12
|
+
|
|
13
|
+
function getCcToolDir() {
|
|
14
|
+
return path.join(os.homedir(), '.cc-tool');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDirExists(dir) {
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getProjectsCacheKey(config) {
|
|
24
|
+
return config?.projectsDir || '__default__';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getCachedProjects(config) {
|
|
28
|
+
const cacheEntry = projectsCache.get(getProjectsCacheKey(config));
|
|
29
|
+
if (!cacheEntry) return null;
|
|
30
|
+
if ((Date.now() - cacheEntry.timestamp) > PROJECTS_CACHE_TTL) {
|
|
31
|
+
projectsCache.delete(getProjectsCacheKey(config));
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return cacheEntry.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setCachedProjects(config, data) {
|
|
38
|
+
projectsCache.set(getProjectsCacheKey(config), {
|
|
39
|
+
data,
|
|
40
|
+
timestamp: Date.now()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function invalidateProjectsCache(configOrPath) {
|
|
45
|
+
if (!configOrPath) {
|
|
46
|
+
projectsCache.clear();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const key = typeof configOrPath === 'string'
|
|
50
|
+
? configOrPath
|
|
51
|
+
: getProjectsCacheKey(configOrPath);
|
|
52
|
+
projectsCache.delete(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hasMessagesCacheFile = path.join(getCcToolDir(), 'session-has-cache.json');
|
|
56
|
+
loadHasMessagesCacheFromDisk();
|
|
57
|
+
|
|
58
|
+
function loadHasMessagesCacheFromDisk() {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(hasMessagesCacheFile)) {
|
|
61
|
+
hasMessagesPersisted = {};
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const raw = fs.readFileSync(hasMessagesCacheFile, 'utf8');
|
|
65
|
+
const parsed = JSON.parse(raw) || {};
|
|
66
|
+
hasMessagesPersisted = parsed;
|
|
67
|
+
Object.entries(parsed).forEach(([filePath, entry]) => {
|
|
68
|
+
if (!entry || typeof entry !== 'object') return;
|
|
69
|
+
const { size, mtimeMs, value } = entry;
|
|
70
|
+
if (typeof size === 'number' && typeof mtimeMs === 'number' && typeof value === 'boolean') {
|
|
71
|
+
hasMessagesCache.set(`${filePath}:${size}:${mtimeMs}`, value);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
hasMessagesPersisted = {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkHasMessagesCache(filePath, stats) {
|
|
80
|
+
if (!filePath || !stats) return undefined;
|
|
81
|
+
const cacheKey = `${filePath}:${stats.size}:${stats.mtimeMs}`;
|
|
82
|
+
if (!hasMessagesCache.has(cacheKey)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return hasMessagesCache.get(cacheKey);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rememberHasMessages(filePath, stats, value) {
|
|
89
|
+
if (!filePath || !stats) return;
|
|
90
|
+
const cacheKey = `${filePath}:${stats.size}:${stats.mtimeMs}`;
|
|
91
|
+
if (hasMessagesCache.size >= HAS_MESSAGES_CACHE_LIMIT) {
|
|
92
|
+
const firstKey = hasMessagesCache.keys().next().value;
|
|
93
|
+
if (firstKey) {
|
|
94
|
+
hasMessagesCache.delete(firstKey);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
hasMessagesCache.set(cacheKey, value);
|
|
98
|
+
|
|
99
|
+
hasMessagesPersisted[filePath] = {
|
|
100
|
+
size: stats.size,
|
|
101
|
+
mtimeMs: stats.mtimeMs,
|
|
102
|
+
value
|
|
103
|
+
};
|
|
104
|
+
schedulePersistHasMessagesCache();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function schedulePersistHasMessagesCache() {
|
|
108
|
+
if (hasMessagesPersistTimer) return;
|
|
109
|
+
hasMessagesPersistTimer = setTimeout(() => {
|
|
110
|
+
try {
|
|
111
|
+
ensureDirExists(path.dirname(hasMessagesCacheFile));
|
|
112
|
+
fs.writeFileSync(hasMessagesCacheFile, JSON.stringify(hasMessagesPersisted, null, 2), 'utf8');
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// ignore persistence errors
|
|
115
|
+
} finally {
|
|
116
|
+
hasMessagesPersistTimer = null;
|
|
117
|
+
}
|
|
118
|
+
}, 1000);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
getCachedProjects,
|
|
123
|
+
setCachedProjects,
|
|
124
|
+
invalidateProjectsCache,
|
|
125
|
+
checkHasMessagesCache,
|
|
126
|
+
rememberHasMessages
|
|
127
|
+
};
|