coding-tool-x 3.4.3 → 3.4.5
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/dist/web/assets/{Analytics-CbGxotgz.js → Analytics-DFWyPf5C.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-oP6nrFEb.js → ConfigTemplates-BFE7hmKd.js} +1 -1
- package/dist/web/assets/{Home-DMntmEvh.js → Home-DZUuCrxk.js} +1 -1
- package/dist/web/assets/{PluginManager-BUC_c7nH.js → PluginManager-WyGY2BQN.js} +1 -1
- package/dist/web/assets/{ProjectList-CW8J49n7.js → ProjectList-CBc0QawN.js} +1 -1
- package/dist/web/assets/{ProjectList-oJIyIRkP.css → ProjectList-DL4JK6ci.css} +1 -1
- package/dist/web/assets/{SessionList-7lYnF92v.js → SessionList-CdPR7QLq.js} +1 -1
- package/dist/web/assets/{SkillManager-Cs08216i.js → SkillManager-B5-DxQOS.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CY-oGtyB.js → WorkspaceManager-C7yqFjpi.js} +1 -1
- package/dist/web/assets/index-BDsmoSfO.js +2 -0
- package/dist/web/assets/{index-5qy5NMIP.css → index-C1pzEgmj.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/src/commands/channels.js +13 -13
- package/src/commands/cli-type.js +5 -5
- package/src/commands/daemon.js +31 -31
- package/src/commands/doctor.js +14 -14
- package/src/commands/export-config.js +23 -23
- package/src/commands/list.js +4 -4
- package/src/commands/logs.js +19 -19
- package/src/commands/plugin.js +62 -62
- package/src/commands/port-config.js +4 -4
- package/src/commands/proxy-control.js +35 -35
- package/src/commands/proxy.js +28 -28
- package/src/commands/resume.js +4 -4
- package/src/commands/search.js +9 -9
- package/src/commands/security.js +5 -5
- package/src/commands/stats.js +18 -18
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +18 -18
- package/src/commands/ui.js +11 -11
- package/src/commands/update.js +9 -9
- package/src/commands/workspace.js +11 -11
- package/src/index.js +24 -24
- package/src/plugins/plugin-installer.js +1 -1
- package/src/reset-config.js +9 -9
- package/src/server/api/channels.js +1 -1
- package/src/server/api/claude-hooks.js +3 -2
- package/src/server/api/plugins.js +165 -14
- package/src/server/api/pm2-autostart.js +2 -2
- package/src/server/api/proxy.js +6 -6
- package/src/server/api/skills.js +66 -7
- package/src/server/codex-proxy-server.js +10 -2
- package/src/server/dev-server.js +2 -2
- package/src/server/gemini-proxy-server.js +10 -2
- package/src/server/index.js +37 -37
- package/src/server/opencode-proxy-server.js +10 -2
- package/src/server/proxy-server.js +14 -6
- package/src/server/services/codex-channels.js +64 -21
- package/src/server/services/codex-env-manager.js +44 -28
- package/src/server/services/config-export-service.js +1 -1
- package/src/server/services/mcp-service.js +2 -1
- package/src/server/services/model-detector.js +2 -2
- package/src/server/services/native-keychain.js +1 -0
- package/src/server/services/plugins-service.js +1066 -261
- package/src/server/services/proxy-runtime.js +129 -5
- package/src/server/services/server-shutdown.js +79 -0
- package/src/server/services/settings-manager.js +3 -3
- package/src/server/services/skill-service.js +146 -29
- package/src/server/websocket-server.js +8 -8
- package/src/ui/menu.js +2 -2
- package/src/ui/prompts.js +5 -5
- package/dist/web/assets/index-ClCqKpvX.js +0 -2
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { PATHS } = require('../../config/paths');
|
|
4
|
+
|
|
5
|
+
const LOG_RECOVERY_BYTES = 1024 * 1024;
|
|
6
|
+
const LOG_FILE_PATH = path.join(PATHS.logs, 'cc-tool-out.log');
|
|
7
|
+
const PROXY_START_LOG_PATTERNS = {
|
|
8
|
+
claude: [
|
|
9
|
+
/Proxy server started on http:\/\/127\.0\.0\.1:\d+/,
|
|
10
|
+
/Claude 代理已自动启动,端口: \d+/
|
|
11
|
+
],
|
|
12
|
+
codex: [/Codex proxy server started on http:\/\/127\.0\.0\.1:\d+/],
|
|
13
|
+
gemini: [/Gemini proxy server started on http:\/\/127\.0\.0\.1:\d+/],
|
|
14
|
+
opencode: [/OpenCode proxy server started on http:\/\/127\.0\.0\.1:\d+/]
|
|
15
|
+
};
|
|
4
16
|
|
|
5
17
|
function getRuntimeFilePath(proxyType) {
|
|
6
18
|
const filePath = PATHS.proxyRuntime?.[proxyType]
|
|
@@ -27,17 +39,129 @@ function saveProxyStartTime(proxyType, preserveExisting = false) {
|
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
function
|
|
42
|
+
function toValidStartTime(value) {
|
|
43
|
+
const parsed = Number(value);
|
|
44
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return Math.floor(parsed);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function persistRecoveredStartTime(proxyType, startTime, recoveredFrom) {
|
|
51
|
+
const validStartTime = toValidStartTime(startTime);
|
|
52
|
+
if (!validStartTime) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const runtimeFilePath = getRuntimeFilePath(proxyType);
|
|
57
|
+
const data = {
|
|
58
|
+
startTime: validStartTime,
|
|
59
|
+
type: proxyType,
|
|
60
|
+
recoveredFrom
|
|
61
|
+
};
|
|
62
|
+
fs.writeFileSync(runtimeFilePath, JSON.stringify(data, null, 2), 'utf8');
|
|
63
|
+
return validStartTime;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readStoredProxyStartTime(proxyType) {
|
|
31
67
|
try {
|
|
32
68
|
const filePath = getRuntimeFilePath(proxyType);
|
|
33
69
|
if (!fs.existsSync(filePath)) return null;
|
|
34
70
|
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
35
|
-
return data.startTime
|
|
71
|
+
return toValidStartTime(data.startTime);
|
|
36
72
|
} catch (err) {
|
|
37
73
|
return null;
|
|
38
74
|
}
|
|
39
75
|
}
|
|
40
76
|
|
|
77
|
+
function parseTimestampPrefix(line) {
|
|
78
|
+
const match = String(line || '').match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}):/);
|
|
79
|
+
if (!match) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const parsed = Date.parse(match[1].replace(' ', 'T'));
|
|
84
|
+
return toValidStartTime(parsed);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function recoverProxyStartTimeFromLogs(proxyType) {
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(LOG_FILE_PATH)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const patterns = PROXY_START_LOG_PATTERNS[proxyType];
|
|
94
|
+
if (!Array.isArray(patterns) || patterns.length === 0) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const stat = fs.statSync(LOG_FILE_PATH);
|
|
99
|
+
const start = Math.max(0, stat.size - LOG_RECOVERY_BYTES);
|
|
100
|
+
const fd = fs.openSync(LOG_FILE_PATH, 'r');
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const length = stat.size - start;
|
|
104
|
+
const buffer = Buffer.alloc(length);
|
|
105
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
106
|
+
|
|
107
|
+
const lines = buffer.toString('utf8').split(/\r?\n/).reverse();
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
if (!patterns.some(pattern => pattern.test(line))) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const timestamp = parseTimestampPrefix(line);
|
|
114
|
+
if (timestamp) {
|
|
115
|
+
return persistRecoveredStartTime(proxyType, timestamp, 'log');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
fs.closeSync(fd);
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
console.warn(`Failed to recover ${proxyType} proxy start time from logs:`, err.message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function recoverProxyStartTime(proxyType) {
|
|
129
|
+
try {
|
|
130
|
+
const logRecoveredStartTime = recoverProxyStartTimeFromLogs(proxyType);
|
|
131
|
+
if (logRecoveredStartTime) {
|
|
132
|
+
return logRecoveredStartTime;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const activeChannelPath = PATHS.activeChannel?.[proxyType];
|
|
136
|
+
if (!activeChannelPath || !fs.existsSync(activeChannelPath)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const recoveredStartTime = toValidStartTime(fs.statSync(activeChannelPath).mtimeMs);
|
|
141
|
+
if (!recoveredStartTime) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return persistRecoveredStartTime(proxyType, recoveredStartTime, 'active-channel');
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.warn(`Failed to recover ${proxyType} proxy start time from active channel:`, err.message);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getProxyStartTime(proxyType, options = {}) {
|
|
153
|
+
const storedStartTime = readStoredProxyStartTime(proxyType);
|
|
154
|
+
if (storedStartTime) {
|
|
155
|
+
return storedStartTime;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (options.allowRecovery) {
|
|
159
|
+
return recoverProxyStartTime(proxyType);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
41
165
|
function clearProxyStartTime(proxyType) {
|
|
42
166
|
try {
|
|
43
167
|
const filePath = getRuntimeFilePath(proxyType);
|
|
@@ -49,9 +173,9 @@ function clearProxyStartTime(proxyType) {
|
|
|
49
173
|
}
|
|
50
174
|
}
|
|
51
175
|
|
|
52
|
-
function getProxyRuntime(proxyType) {
|
|
53
|
-
const startTime = getProxyStartTime(proxyType);
|
|
54
|
-
return startTime ? Date.now() - startTime : null;
|
|
176
|
+
function getProxyRuntime(proxyType, options = {}) {
|
|
177
|
+
const startTime = getProxyStartTime(proxyType, options);
|
|
178
|
+
return startTime ? Math.max(0, Date.now() - startTime) : null;
|
|
55
179
|
}
|
|
56
180
|
|
|
57
181
|
function formatRuntime(ms) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const SOCKET_TRACKER = Symbol('ccTool.socketTracker');
|
|
2
|
+
|
|
3
|
+
function attachServerShutdownHandling(server, options = {}) {
|
|
4
|
+
if (!server || server[SOCKET_TRACKER]) {
|
|
5
|
+
return server;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const sockets = new Set();
|
|
9
|
+
server.on('connection', (socket) => {
|
|
10
|
+
sockets.add(socket);
|
|
11
|
+
socket.on('close', () => {
|
|
12
|
+
sockets.delete(socket);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const keepAliveTimeout = Number.isFinite(options.keepAliveTimeout)
|
|
17
|
+
? Math.max(0, options.keepAliveTimeout)
|
|
18
|
+
: 1000;
|
|
19
|
+
const headersTimeout = Number.isFinite(options.headersTimeout)
|
|
20
|
+
? Math.max(keepAliveTimeout + 1000, options.headersTimeout)
|
|
21
|
+
: keepAliveTimeout + 1000;
|
|
22
|
+
|
|
23
|
+
server.keepAliveTimeout = keepAliveTimeout;
|
|
24
|
+
server.headersTimeout = headersTimeout;
|
|
25
|
+
server[SOCKET_TRACKER] = { sockets };
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function expediteServerShutdown(server, options = {}) {
|
|
30
|
+
if (!server) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (typeof server.closeIdleConnections === 'function') {
|
|
36
|
+
server.closeIdleConnections();
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// ignore idle-connection close failures
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const delay = Number.isFinite(options.forceAfterMs)
|
|
43
|
+
? Math.max(0, options.forceAfterMs)
|
|
44
|
+
: 300;
|
|
45
|
+
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
try {
|
|
48
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
49
|
+
server.closeAllConnections();
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// ignore force-close failures and fallback to socket destroy
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tracker = server[SOCKET_TRACKER];
|
|
56
|
+
if (!tracker?.sockets) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const socket of tracker.sockets) {
|
|
61
|
+
try {
|
|
62
|
+
socket.destroy();
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore socket destroy failures
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, delay);
|
|
68
|
+
|
|
69
|
+
if (typeof timer.unref === 'function') {
|
|
70
|
+
timer.unref();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return timer;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
attachServerShutdownHandling,
|
|
78
|
+
expediteServerShutdown
|
|
79
|
+
};
|
|
@@ -57,7 +57,7 @@ function backupSettings() {
|
|
|
57
57
|
const content = fs.readFileSync(getSettingsPath(), 'utf8');
|
|
58
58
|
fs.writeFileSync(getBackupPath(), content, 'utf8');
|
|
59
59
|
|
|
60
|
-
console.log('
|
|
60
|
+
console.log('[OK] Settings backed up to:', getBackupPath());
|
|
61
61
|
return { success: true, alreadyExists: false };
|
|
62
62
|
} catch (err) {
|
|
63
63
|
throw new Error('Failed to backup settings: ' + err.message);
|
|
@@ -77,7 +77,7 @@ function restoreSettings() {
|
|
|
77
77
|
// 删除备份文件
|
|
78
78
|
fs.unlinkSync(getBackupPath());
|
|
79
79
|
|
|
80
|
-
console.log('
|
|
80
|
+
console.log('[OK] Settings restored from backup');
|
|
81
81
|
return { success: true };
|
|
82
82
|
} catch (err) {
|
|
83
83
|
throw new Error('Failed to restore settings: ' + err.message);
|
|
@@ -118,7 +118,7 @@ function setProxyConfig(proxyPort) {
|
|
|
118
118
|
// 写入
|
|
119
119
|
writeSettings(settings);
|
|
120
120
|
|
|
121
|
-
console.log(
|
|
121
|
+
console.log(`[OK] Settings updated to use proxy on port ${proxyPort}`);
|
|
122
122
|
return { success: true, port: proxyPort };
|
|
123
123
|
} catch (err) {
|
|
124
124
|
throw new Error('Failed to set proxy config: ' + err.message);
|
|
@@ -17,6 +17,7 @@ const AdmZip = require('adm-zip');
|
|
|
17
17
|
const {
|
|
18
18
|
parseSkillContent,
|
|
19
19
|
} = require('./format-converter');
|
|
20
|
+
const { maskToken } = require('./oauth-utils');
|
|
20
21
|
const { NATIVE_PATHS, HOME_DIR, PATHS } = require('../../config/paths');
|
|
21
22
|
|
|
22
23
|
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
@@ -47,6 +48,10 @@ function stripGitSuffix(value = '') {
|
|
|
47
48
|
return String(value || '').replace(/\.git$/i, '');
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function normalizeRepoToken(token = '') {
|
|
52
|
+
return String(token || '').trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
function isWindowsAbsolutePath(input = '') {
|
|
51
56
|
return /^[a-zA-Z]:[\\/]/.test(String(input || ''));
|
|
52
57
|
}
|
|
@@ -153,18 +158,10 @@ function isRootSkillFile(filePath = '') {
|
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
const DEFAULT_REPOS_BY_PLATFORM = {
|
|
156
|
-
claude: [
|
|
157
|
-
|
|
158
|
-
],
|
|
159
|
-
|
|
160
|
-
{ owner: 'openai', name: 'skills', branch: 'main', directory: 'skills/.curated', enabled: true }
|
|
161
|
-
],
|
|
162
|
-
gemini: [
|
|
163
|
-
{ owner: 'google-gemini', name: 'gemini-cli', branch: 'main', directory: '.gemini/skills', enabled: true }
|
|
164
|
-
],
|
|
165
|
-
opencode: [
|
|
166
|
-
{ owner: 'Shakudo-io', name: 'opencode-skills', branch: 'main', directory: '', enabled: true }
|
|
167
|
-
]
|
|
161
|
+
claude: [],
|
|
162
|
+
codex: [],
|
|
163
|
+
gemini: [],
|
|
164
|
+
opencode: []
|
|
168
165
|
};
|
|
169
166
|
|
|
170
167
|
const PLATFORM_CONFIG = {
|
|
@@ -334,6 +331,13 @@ class SkillService {
|
|
|
334
331
|
normalized.label = buildRepoLabel(normalized);
|
|
335
332
|
normalized.id = buildRepoId(normalized);
|
|
336
333
|
|
|
334
|
+
if (provider !== 'local') {
|
|
335
|
+
const token = normalizeRepoToken(repo.token);
|
|
336
|
+
if (token) {
|
|
337
|
+
normalized.token = token;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
337
341
|
return normalized;
|
|
338
342
|
}
|
|
339
343
|
|
|
@@ -366,6 +370,55 @@ class SkillService {
|
|
|
366
370
|
fs.writeFileSync(this.reposConfigPath, JSON.stringify({ repos: normalizedRepos }, null, 2));
|
|
367
371
|
}
|
|
368
372
|
|
|
373
|
+
toClientRepo(repo = {}) {
|
|
374
|
+
const normalizedRepo = this.normalizeRepoConfig(repo);
|
|
375
|
+
const token = normalizeRepoToken(normalizedRepo.token);
|
|
376
|
+
const clientRepo = {
|
|
377
|
+
...normalizedRepo,
|
|
378
|
+
hasToken: Boolean(token),
|
|
379
|
+
tokenPreview: token ? maskToken(token) : ''
|
|
380
|
+
};
|
|
381
|
+
delete clientRepo.token;
|
|
382
|
+
return clientRepo;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getReposForClient(repos = null) {
|
|
386
|
+
const sourceRepos = Array.isArray(repos) ? repos : this.loadRepos();
|
|
387
|
+
return sourceRepos.map(repo => this.toClientRepo(repo));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
findStoredRepo(repo = {}) {
|
|
391
|
+
const repoId = String(repo.id || repo.repoId || '').trim();
|
|
392
|
+
const repos = this.loadRepos();
|
|
393
|
+
|
|
394
|
+
if (repoId) {
|
|
395
|
+
return repos.find(candidate => candidate.id === repoId) || null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const normalizedRepo = this.normalizeRepoConfig(repo);
|
|
400
|
+
return repos.find(candidate => candidate.id === normalizedRepo.id) || null;
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
resolveRepoToken(repo = null) {
|
|
407
|
+
if (!repo || typeof repo !== 'object') return null;
|
|
408
|
+
|
|
409
|
+
const directToken = normalizeRepoToken(repo.token);
|
|
410
|
+
if (directToken) {
|
|
411
|
+
return directToken;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const storedRepo = this.findStoredRepo(repo);
|
|
415
|
+
if (!storedRepo) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return normalizeRepoToken(storedRepo.token) || null;
|
|
420
|
+
}
|
|
421
|
+
|
|
369
422
|
/**
|
|
370
423
|
* 添加仓库
|
|
371
424
|
* @param {Object} repo - 仓库配置
|
|
@@ -443,6 +496,43 @@ class SkillService {
|
|
|
443
496
|
return this.loadRepos();
|
|
444
497
|
}
|
|
445
498
|
|
|
499
|
+
updateRepoAuth(owner, name, directory = '', token = '', clearToken = false, repoId = '') {
|
|
500
|
+
const repos = this.loadRepos();
|
|
501
|
+
const normalizedDirectory = normalizeRepoDirectory(directory);
|
|
502
|
+
const repo = repos.find(r => {
|
|
503
|
+
if (repoId) {
|
|
504
|
+
return r.id === repoId;
|
|
505
|
+
}
|
|
506
|
+
return (
|
|
507
|
+
(r.owner || '') === owner &&
|
|
508
|
+
(r.name || '') === name &&
|
|
509
|
+
normalizeRepoDirectory(r.directory) === normalizedDirectory
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
if (!repo) {
|
|
514
|
+
throw new Error('Repository not found');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (repo.provider === 'local') {
|
|
518
|
+
throw new Error('Local repository does not support token auth');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (clearToken) {
|
|
522
|
+
delete repo.token;
|
|
523
|
+
} else {
|
|
524
|
+
const normalizedToken = normalizeRepoToken(token);
|
|
525
|
+
if (!normalizedToken) {
|
|
526
|
+
throw new Error('Missing token');
|
|
527
|
+
}
|
|
528
|
+
repo.token = normalizedToken;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.saveRepos(repos);
|
|
532
|
+
this.clearCache({ removeFile: true });
|
|
533
|
+
return this.loadRepos();
|
|
534
|
+
}
|
|
535
|
+
|
|
446
536
|
/**
|
|
447
537
|
* 获取所有技能列表(带缓存)
|
|
448
538
|
*/
|
|
@@ -880,7 +970,18 @@ class SkillService {
|
|
|
880
970
|
}
|
|
881
971
|
}
|
|
882
972
|
|
|
883
|
-
getGitHubToken(
|
|
973
|
+
getGitHubToken(repoOrHost = DEFAULT_GITHUB_HOST) {
|
|
974
|
+
if (repoOrHost && typeof repoOrHost === 'object') {
|
|
975
|
+
const repoToken = this.resolveRepoToken(repoOrHost);
|
|
976
|
+
if (repoToken) {
|
|
977
|
+
return repoToken;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const host = typeof repoOrHost === 'string'
|
|
982
|
+
? repoOrHost
|
|
983
|
+
: (repoOrHost?.host || DEFAULT_GITHUB_HOST);
|
|
984
|
+
|
|
884
985
|
// 优先从环境变量获取
|
|
885
986
|
if (process.env.GITHUB_TOKEN) {
|
|
886
987
|
return process.env.GITHUB_TOKEN;
|
|
@@ -907,7 +1008,18 @@ class SkillService {
|
|
|
907
1008
|
return this.getTokenFromGitCredential(host);
|
|
908
1009
|
}
|
|
909
1010
|
|
|
910
|
-
getGitLabToken(
|
|
1011
|
+
getGitLabToken(repoOrHost = DEFAULT_GITLAB_HOST) {
|
|
1012
|
+
if (repoOrHost && typeof repoOrHost === 'object') {
|
|
1013
|
+
const repoToken = this.resolveRepoToken(repoOrHost);
|
|
1014
|
+
if (repoToken) {
|
|
1015
|
+
return repoToken;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const host = typeof repoOrHost === 'string'
|
|
1020
|
+
? repoOrHost
|
|
1021
|
+
: (repoOrHost?.host || DEFAULT_GITLAB_HOST);
|
|
1022
|
+
|
|
911
1023
|
if (process.env.GITLAB_TOKEN) {
|
|
912
1024
|
return process.env.GITLAB_TOKEN;
|
|
913
1025
|
}
|
|
@@ -939,8 +1051,8 @@ class SkillService {
|
|
|
939
1051
|
/**
|
|
940
1052
|
* 通用 GitHub API 请求
|
|
941
1053
|
*/
|
|
942
|
-
async fetchGitHubApi(url) {
|
|
943
|
-
const token = this.getGitHubToken(url);
|
|
1054
|
+
async fetchGitHubApi(url, repo = null) {
|
|
1055
|
+
const token = this.getGitHubToken(repo || url);
|
|
944
1056
|
const headers = {
|
|
945
1057
|
'User-Agent': 'cc-cli-skill-service',
|
|
946
1058
|
'Accept': 'application/vnd.github.v3+json'
|
|
@@ -979,15 +1091,15 @@ class SkillService {
|
|
|
979
1091
|
|
|
980
1092
|
async fetchGitHubRepoTree(repo) {
|
|
981
1093
|
const treeUrl = `https://api.github.com/repos/${repo.owner}/${repo.name}/git/trees/${repo.branch}?recursive=1`;
|
|
982
|
-
const tree = await this.fetchGitHubApi(treeUrl);
|
|
1094
|
+
const tree = await this.fetchGitHubApi(treeUrl, repo);
|
|
983
1095
|
if (tree?.truncated) {
|
|
984
1096
|
console.warn(`[SkillService] GitHub tree truncated for ${repo.owner}/${repo.name}`);
|
|
985
1097
|
}
|
|
986
1098
|
return tree?.tree || [];
|
|
987
1099
|
}
|
|
988
1100
|
|
|
989
|
-
async fetchGitLabApi(url, { raw = false } = {}) {
|
|
990
|
-
const token = this.getGitLabToken(url);
|
|
1101
|
+
async fetchGitLabApi(url, { raw = false, repo = null } = {}) {
|
|
1102
|
+
const token = this.getGitLabToken(repo || url);
|
|
991
1103
|
const headers = {
|
|
992
1104
|
'User-Agent': 'cc-cli-skill-service'
|
|
993
1105
|
};
|
|
@@ -1041,7 +1153,7 @@ class SkillService {
|
|
|
1041
1153
|
|
|
1042
1154
|
while (page) {
|
|
1043
1155
|
const url = `${repo.host}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(repo.branch)}&recursive=true&per_page=100&page=${page}`;
|
|
1044
|
-
const response = await this.fetchGitLabApi(url);
|
|
1156
|
+
const response = await this.fetchGitLabApi(url, { repo });
|
|
1045
1157
|
tree.push(...(response.data || []).map(item => ({
|
|
1046
1158
|
...item,
|
|
1047
1159
|
type: item.type === 'tree' ? 'tree' : 'blob'
|
|
@@ -1058,21 +1170,26 @@ class SkillService {
|
|
|
1058
1170
|
const projectId = encodeURIComponent(repo.projectPath);
|
|
1059
1171
|
const normalizedFilePath = encodeURIComponent(normalizeRepoPath(filePath));
|
|
1060
1172
|
const url = `${repo.host}/api/v4/projects/${projectId}/repository/files/${normalizedFilePath}/raw?ref=${encodeURIComponent(repo.branch)}`;
|
|
1061
|
-
return this.fetchGitLabApi(url, { raw: true });
|
|
1173
|
+
return this.fetchGitLabApi(url, { raw: true, repo });
|
|
1062
1174
|
}
|
|
1063
1175
|
|
|
1064
1176
|
/**
|
|
1065
1177
|
* 使用 GitHub API 获取目录内容
|
|
1066
1178
|
*/
|
|
1067
|
-
async fetchGitHubContents(owner, name, path, branch) {
|
|
1179
|
+
async fetchGitHubContents(owner, name, path, branch, repo = null) {
|
|
1068
1180
|
const url = `https://api.github.com/repos/${owner}/${name}/contents/${path}?ref=${branch}`;
|
|
1181
|
+
const token = this.getGitHubToken(repo || url);
|
|
1182
|
+
const headers = {
|
|
1183
|
+
'User-Agent': 'cc-cli-skill-service',
|
|
1184
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
1185
|
+
};
|
|
1186
|
+
if (token) {
|
|
1187
|
+
headers.Authorization = `token ${token}`;
|
|
1188
|
+
}
|
|
1069
1189
|
|
|
1070
1190
|
return new Promise((resolve, reject) => {
|
|
1071
1191
|
const req = https.get(url, {
|
|
1072
|
-
headers
|
|
1073
|
-
'User-Agent': 'cc-cli-skill-service',
|
|
1074
|
-
'Accept': 'application/vnd.github.v3+json'
|
|
1075
|
-
},
|
|
1192
|
+
headers,
|
|
1076
1193
|
timeout: 15000
|
|
1077
1194
|
}, (res) => {
|
|
1078
1195
|
let data = '';
|
|
@@ -1144,7 +1261,7 @@ class SkillService {
|
|
|
1144
1261
|
if (dir.name.startsWith('.') || dir.name === 'node_modules') continue;
|
|
1145
1262
|
|
|
1146
1263
|
try {
|
|
1147
|
-
const subContents = await this.fetchGitHubContents(repo.owner, repo.name, dir.path, repo.branch);
|
|
1264
|
+
const subContents = await this.fetchGitHubContents(repo.owner, repo.name, dir.path, repo.branch, repo);
|
|
1148
1265
|
await this.scanRepoContents(subContents, repo, dir.path, skills);
|
|
1149
1266
|
} catch (err) {
|
|
1150
1267
|
// 忽略子目录错误,继续扫描
|
|
@@ -1367,13 +1484,13 @@ class SkillService {
|
|
|
1367
1484
|
if (normalizedRepo.provider === 'gitlab') {
|
|
1368
1485
|
const projectId = encodeURIComponent(normalizedRepo.projectPath);
|
|
1369
1486
|
zipUrl = `${normalizedRepo.host}/api/v4/projects/${projectId}/repository/archive.zip?sha=${encodeURIComponent(normalizedRepo.branch)}`;
|
|
1370
|
-
const token = this.getGitLabToken(normalizedRepo
|
|
1487
|
+
const token = this.getGitLabToken(normalizedRepo);
|
|
1371
1488
|
if (token) {
|
|
1372
1489
|
zipHeaders['PRIVATE-TOKEN'] = token;
|
|
1373
1490
|
}
|
|
1374
1491
|
} else {
|
|
1375
1492
|
zipUrl = `https://api.github.com/repos/${normalizedRepo.owner}/${normalizedRepo.name}/zipball/${encodeURIComponent(normalizedRepo.branch)}`;
|
|
1376
|
-
const token = this.getGitHubToken(normalizedRepo
|
|
1493
|
+
const token = this.getGitHubToken(normalizedRepo);
|
|
1377
1494
|
zipHeaders.Accept = 'application/vnd.github+json';
|
|
1378
1495
|
if (token) {
|
|
1379
1496
|
zipHeaders.Authorization = `token ${token}`;
|
|
@@ -291,7 +291,7 @@ function startWebSocketServer(httpServer, options = {}) {
|
|
|
291
291
|
acc[source] = (acc[source] || 0) + 1;
|
|
292
292
|
return acc;
|
|
293
293
|
}, {});
|
|
294
|
-
console.log(
|
|
294
|
+
console.log(`[NOTE] Loaded ${logsCache.length} persisted logs today ->`, counts);
|
|
295
295
|
|
|
296
296
|
try {
|
|
297
297
|
// 如果传入的是 HTTP server,则附加到该服务器;否则创建独立的 WebSocket 服务器
|
|
@@ -301,7 +301,7 @@ function startWebSocketServer(httpServer, options = {}) {
|
|
|
301
301
|
path: '/ws' // 指定 WebSocket 路径
|
|
302
302
|
});
|
|
303
303
|
installOriginGuard(wss);
|
|
304
|
-
console.log(
|
|
304
|
+
console.log(`[OK] WebSocket server attached to HTTP server at /ws`);
|
|
305
305
|
} else {
|
|
306
306
|
// 创建独立的 WebSocket 服务器,使用配置的 webUI 端口
|
|
307
307
|
const config = loadConfig();
|
|
@@ -311,7 +311,7 @@ function startWebSocketServer(httpServer, options = {}) {
|
|
|
311
311
|
path: '/ws'
|
|
312
312
|
});
|
|
313
313
|
installOriginGuard(wss);
|
|
314
|
-
console.log(
|
|
314
|
+
console.log(`[OK] WebSocket server started on ws://127.0.0.1:${port}/ws`);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
wss.on('connection', (ws, req) => {
|
|
@@ -352,7 +352,7 @@ function startWebSocketServer(httpServer, options = {}) {
|
|
|
352
352
|
wsClients.forEach(ws => {
|
|
353
353
|
if (ws.isAlive === false) {
|
|
354
354
|
// 客户端没有响应 pong,断开连接
|
|
355
|
-
console.log('
|
|
355
|
+
console.log('[ERROR] WebSocket client timeout, terminating');
|
|
356
356
|
wsClients.delete(ws);
|
|
357
357
|
return ws.terminate();
|
|
358
358
|
}
|
|
@@ -369,8 +369,8 @@ function startWebSocketServer(httpServer, options = {}) {
|
|
|
369
369
|
wss.on('error', (error) => {
|
|
370
370
|
console.error('WebSocket server error:', error);
|
|
371
371
|
if (error.code === 'EADDRINUSE') {
|
|
372
|
-
console.error(chalk.red('\n
|
|
373
|
-
console.error(chalk.yellow('\n
|
|
372
|
+
console.error(chalk.red('\n[ERROR] WebSocket 端口已被占用'));
|
|
373
|
+
console.error(chalk.yellow('\n[TIP] 请检查端口配置\n'));
|
|
374
374
|
wss = null;
|
|
375
375
|
}
|
|
376
376
|
});
|
|
@@ -400,7 +400,7 @@ function stopWebSocketServer() {
|
|
|
400
400
|
|
|
401
401
|
// 关闭服务器
|
|
402
402
|
wss.close(() => {
|
|
403
|
-
console.log('
|
|
403
|
+
console.log('[OK] WebSocket server stopped');
|
|
404
404
|
});
|
|
405
405
|
|
|
406
406
|
wss = null;
|
|
@@ -443,7 +443,7 @@ function broadcastLog(logData) {
|
|
|
443
443
|
function clearAllLogs() {
|
|
444
444
|
logsCache = [];
|
|
445
445
|
saveLogsToFile([]);
|
|
446
|
-
console.log('
|
|
446
|
+
console.log('[OK] All logs cleared');
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
// 去掉敏感字段
|
package/src/ui/menu.js
CHANGED
|
@@ -50,9 +50,9 @@ function getChannelAndProxyStatus(cliType) {
|
|
|
50
50
|
* 显示主菜单
|
|
51
51
|
*/
|
|
52
52
|
async function showMainMenu(config) {
|
|
53
|
-
console.log(chalk.bold.cyan('\n
|
|
53
|
+
console.log(chalk.bold.cyan('\n╔===============================================╗'));
|
|
54
54
|
console.log(chalk.bold.cyan(`║ Claude Code 会话管理工具 v${packageInfo.version} ║`));
|
|
55
|
-
console.log(chalk.bold.cyan('
|
|
55
|
+
console.log(chalk.bold.cyan('╚===============================================╝\n'));
|
|
56
56
|
|
|
57
57
|
// 显示当前CLI类型
|
|
58
58
|
const cliTypes = {
|
package/src/ui/prompts.js
CHANGED
|
@@ -31,15 +31,15 @@ async function promptForkConfirm() {
|
|
|
31
31
|
default: 'continue',
|
|
32
32
|
choices: [
|
|
33
33
|
{
|
|
34
|
-
name: chalk.green('
|
|
34
|
+
name: chalk.green('[NOTE] 继续原会话 (推荐) - 在原会话上继续对话,所有内容会追加到原文件'),
|
|
35
35
|
value: 'continue',
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
|
-
name: chalk.yellow('
|
|
38
|
+
name: chalk.yellow('[FORK] 创建新分支 (Fork) - 基于原会话创建新会话,保留原会话不变'),
|
|
39
39
|
value: 'fork',
|
|
40
40
|
},
|
|
41
41
|
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
42
|
-
{ name: chalk.blue('
|
|
42
|
+
{ name: chalk.blue('[<-] 返回重新选择'), value: 'back' },
|
|
43
43
|
],
|
|
44
44
|
},
|
|
45
45
|
]);
|
|
@@ -55,7 +55,7 @@ async function promptSearchKeyword() {
|
|
|
55
55
|
{
|
|
56
56
|
type: 'input',
|
|
57
57
|
name: 'keyword',
|
|
58
|
-
message: chalk.cyan('
|
|
58
|
+
message: chalk.cyan('[SEARCH] 输入搜索关键词:'),
|
|
59
59
|
validate: (input) => {
|
|
60
60
|
if (!input.trim()) {
|
|
61
61
|
return '请输入搜索关键词';
|
|
@@ -76,7 +76,7 @@ async function promptSelectProject(projects) {
|
|
|
76
76
|
const choices = [
|
|
77
77
|
...projects,
|
|
78
78
|
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
79
|
-
{ name: chalk.gray('
|
|
79
|
+
{ name: chalk.gray('[<-] 取消切换'), value: null }
|
|
80
80
|
];
|
|
81
81
|
|
|
82
82
|
const { project } = await inquirer.prompt([
|