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,190 @@
|
|
|
1
|
+
// 搜索会话命令
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { promptSelectSession, promptSearchKeyword, promptForkConfirm } = require('../ui/prompts');
|
|
6
|
+
const { resumeSession } = require('./resume');
|
|
7
|
+
const { getProjects, searchSessions: searchSessionsInProject, parseRealProjectPath } = require('../server/services/sessions');
|
|
8
|
+
const { loadAliases } = require('../server/services/alias');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 跨所有项目搜索会话内容
|
|
12
|
+
*/
|
|
13
|
+
async function searchSessionsAcrossProjects(config, keyword) {
|
|
14
|
+
const spinner = ora(`🔍 正在搜索 "${keyword}"...`).start();
|
|
15
|
+
|
|
16
|
+
const projects = await getProjects(config);
|
|
17
|
+
const aliases = loadAliases();
|
|
18
|
+
const allResults = [];
|
|
19
|
+
|
|
20
|
+
// 跨所有项目搜索
|
|
21
|
+
for (const projectName of projects) {
|
|
22
|
+
try {
|
|
23
|
+
const { projectName: displayName } = parseRealProjectPath(projectName);
|
|
24
|
+
spinner.text = `🔍 正在搜索项目: ${displayName}...`;
|
|
25
|
+
const results = searchSessionsInProject(config, projectName, keyword, 15);
|
|
26
|
+
|
|
27
|
+
if (results.length > 0) {
|
|
28
|
+
results.forEach(result => {
|
|
29
|
+
allResults.push({
|
|
30
|
+
...result,
|
|
31
|
+
projectName: projectName,
|
|
32
|
+
projectDisplayName: displayName,
|
|
33
|
+
alias: aliases[result.sessionId] || null
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// 忽略单个项目的错误
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
spinner.stop();
|
|
43
|
+
spinner.clear();
|
|
44
|
+
|
|
45
|
+
if (allResults.length === 0) {
|
|
46
|
+
console.clear();
|
|
47
|
+
console.log(chalk.red(`\n❌ 未找到包含 "${keyword}" 的对话\n`));
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 按匹配数量排序
|
|
52
|
+
allResults.sort((a, b) => b.matchCount - a.matchCount);
|
|
53
|
+
|
|
54
|
+
// 统计总匹配数
|
|
55
|
+
const totalMatches = allResults.reduce((sum, r) => sum + r.matchCount, 0);
|
|
56
|
+
|
|
57
|
+
console.clear();
|
|
58
|
+
console.log(chalk.green(`\n✨ 找到 ${allResults.length} 个对话,共 ${totalMatches} 处匹配\n`));
|
|
59
|
+
|
|
60
|
+
const choices = [];
|
|
61
|
+
|
|
62
|
+
allResults.forEach((result, index) => {
|
|
63
|
+
// 构建显示名称
|
|
64
|
+
let displayName = '';
|
|
65
|
+
|
|
66
|
+
// 序号
|
|
67
|
+
displayName += chalk.bold.white(`${index + 1}. `);
|
|
68
|
+
|
|
69
|
+
// 项目名(洋红色高亮)
|
|
70
|
+
displayName += chalk.magenta.bold(`[${result.projectDisplayName}] `);
|
|
71
|
+
|
|
72
|
+
// 会话别名或 ID
|
|
73
|
+
if (result.alias) {
|
|
74
|
+
displayName += chalk.yellow.bold(`[${result.alias}] `);
|
|
75
|
+
} else {
|
|
76
|
+
displayName += chalk.gray(`[${result.sessionId.substring(0, 8)}] `);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 匹配数量
|
|
80
|
+
displayName += chalk.cyan(`(${result.matchCount} 处匹配)`);
|
|
81
|
+
|
|
82
|
+
choices.push({
|
|
83
|
+
name: displayName,
|
|
84
|
+
value: { sessionId: result.sessionId, projectName: result.projectName },
|
|
85
|
+
short: result.alias || result.sessionId.substring(0, 8)
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 显示前 3 个匹配的上下文
|
|
89
|
+
const matchesToShow = result.matches.slice(0, 3);
|
|
90
|
+
matchesToShow.forEach((match, idx) => {
|
|
91
|
+
const roleColor = match.role === 'user' ? chalk.blue : chalk.green;
|
|
92
|
+
const roleLabel = match.role === 'user' ? '用户' : '助手';
|
|
93
|
+
|
|
94
|
+
choices.push({
|
|
95
|
+
name: ` ${roleColor(`[${roleLabel}]`)} ${chalk.gray(match.context)}`,
|
|
96
|
+
value: null,
|
|
97
|
+
disabled: true
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 如果还有更多匹配,显示提示
|
|
102
|
+
if (result.matches.length > 3) {
|
|
103
|
+
choices.push({
|
|
104
|
+
name: chalk.gray(` ... 还有 ${result.matches.length - 3} 处匹配`),
|
|
105
|
+
value: null,
|
|
106
|
+
disabled: true
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 添加分隔线(不是最后一个)
|
|
111
|
+
if (index < allResults.length - 1) {
|
|
112
|
+
choices.push(new inquirer.Separator(chalk.gray('─'.repeat(10))));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return choices;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 处理搜索会话
|
|
121
|
+
*/
|
|
122
|
+
async function handleSearch(config, switchProjectCallback) {
|
|
123
|
+
while (true) {
|
|
124
|
+
const keyword = await promptSearchKeyword();
|
|
125
|
+
const choices = await searchSessionsAcrossProjects(config, keyword);
|
|
126
|
+
|
|
127
|
+
if (choices.length === 0) {
|
|
128
|
+
const { action } = await inquirer.prompt([
|
|
129
|
+
{
|
|
130
|
+
type: 'list',
|
|
131
|
+
name: 'action',
|
|
132
|
+
message: '未找到匹配的对话',
|
|
133
|
+
choices: [
|
|
134
|
+
{ name: chalk.blue('↩️ 返回主菜单'), value: 'back' },
|
|
135
|
+
{ name: chalk.cyan('🔎 重新搜索'), value: 'retry' },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
if (action === 'back') return;
|
|
141
|
+
if (action === 'retry') continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 添加操作选项
|
|
145
|
+
choices.push(new inquirer.Separator(chalk.gray('═'.repeat(80))));
|
|
146
|
+
choices.push({ name: chalk.blue('↩️ 返回主菜单'), value: 'back' });
|
|
147
|
+
choices.push({ name: chalk.cyan('🔎 重新搜索'), value: 'retry' });
|
|
148
|
+
|
|
149
|
+
// 使用自定义 pageSize 以便显示更多结果
|
|
150
|
+
const { selected } = await inquirer.prompt([
|
|
151
|
+
{
|
|
152
|
+
type: 'list',
|
|
153
|
+
name: 'selected',
|
|
154
|
+
message: '选择对话:',
|
|
155
|
+
pageSize: 20,
|
|
156
|
+
choices: choices,
|
|
157
|
+
},
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
if (selected === 'back') {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (selected === 'retry') {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// selected 是 { sessionId, projectName }
|
|
169
|
+
const sessionId = selected.sessionId;
|
|
170
|
+
const projectName = selected.projectName;
|
|
171
|
+
|
|
172
|
+
// 切换到该项目
|
|
173
|
+
config.currentProject = projectName;
|
|
174
|
+
|
|
175
|
+
// 询问是否 fork
|
|
176
|
+
const action = await promptForkConfirm();
|
|
177
|
+
|
|
178
|
+
if (action === 'back') {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const fork = action === 'fork';
|
|
183
|
+
await resumeSession(config, sessionId, fork);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
searchSessionsAcrossProjects,
|
|
189
|
+
handleSearch,
|
|
190
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { PATHS, ensureStorageDirMigrated } = require('../config/paths');
|
|
5
|
+
|
|
6
|
+
const SECURITY_FILE = path.join(PATHS.base, 'security.json');
|
|
7
|
+
|
|
8
|
+
function showSecurityHelp() {
|
|
9
|
+
console.log(chalk.yellow('\n🔐 安全设置命令:'));
|
|
10
|
+
console.log(' ctx security reset 关闭访问密码(删除安全配置文件)');
|
|
11
|
+
console.log('');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function handleSecurityReset() {
|
|
15
|
+
console.log(chalk.cyan('\n🔐 安全设置 - 关闭访问密码\n'));
|
|
16
|
+
ensureStorageDirMigrated();
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(SECURITY_FILE)) {
|
|
19
|
+
console.log(chalk.yellow('⚠️ 未检测到安全配置文件'));
|
|
20
|
+
console.log(chalk.gray(`路径: ${SECURITY_FILE}\n`));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
fs.unlinkSync(SECURITY_FILE);
|
|
26
|
+
console.log(chalk.green('✅ 访问密码已关闭'));
|
|
27
|
+
console.log(chalk.gray(`已删除: ${SECURITY_FILE}\n`));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(chalk.red('❌ 关闭密码失败:'), error.message);
|
|
30
|
+
console.log(chalk.gray(`路径: ${SECURITY_FILE}\n`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
showSecurityHelp,
|
|
36
|
+
handleSecurityReset
|
|
37
|
+
};
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { loadConfig } = require('../config/loader');
|
|
4
|
+
|
|
5
|
+
const TOOL_TYPES = ['claude', 'codex', 'gemini', 'opencode'];
|
|
6
|
+
const TOOL_ENDPOINTS = {
|
|
7
|
+
claude: '/api/statistics',
|
|
8
|
+
codex: '/api/codex/statistics',
|
|
9
|
+
gemini: '/api/gemini/statistics',
|
|
10
|
+
opencode: '/api/opencode/statistics'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* HTTP 请求辅助函数
|
|
15
|
+
*/
|
|
16
|
+
function httpRequest(method, path, data = null) {
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
const port = config.ports?.webUI || 19999;
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const postData = data ? JSON.stringify(data) : null;
|
|
22
|
+
const options = {
|
|
23
|
+
hostname: 'localhost',
|
|
24
|
+
port: port,
|
|
25
|
+
path: path,
|
|
26
|
+
method: method,
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
...(postData && { 'Content-Length': Buffer.byteLength(postData) })
|
|
30
|
+
},
|
|
31
|
+
timeout: 5000
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const req = http.request(options, (res) => {
|
|
35
|
+
let responseData = '';
|
|
36
|
+
|
|
37
|
+
res.on('data', (chunk) => {
|
|
38
|
+
responseData += chunk;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
try {
|
|
43
|
+
const json = JSON.parse(responseData);
|
|
44
|
+
resolve({ data: json, status: res.statusCode });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
reject(new Error('Invalid JSON response'));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
req.on('error', (err) => {
|
|
52
|
+
reject(err);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
req.on('timeout', () => {
|
|
56
|
+
req.destroy();
|
|
57
|
+
reject(new Error('Request timeout'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (postData) {
|
|
61
|
+
req.write(postData);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 检查 UI 服务是否运行
|
|
70
|
+
*/
|
|
71
|
+
async function checkUIService() {
|
|
72
|
+
try {
|
|
73
|
+
await httpRequest('GET', '/api/proxy/status');
|
|
74
|
+
return true;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateToolType(type) {
|
|
81
|
+
if (!type) return true;
|
|
82
|
+
return TOOL_TYPES.includes(type);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getDateString(offsetDays = 0) {
|
|
86
|
+
const date = new Date();
|
|
87
|
+
date.setHours(0, 0, 0, 0);
|
|
88
|
+
date.setDate(date.getDate() - offsetDays);
|
|
89
|
+
const y = date.getFullYear();
|
|
90
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
91
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
92
|
+
return `${y}-${m}-${d}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function emptySummary() {
|
|
96
|
+
return {
|
|
97
|
+
requests: 0,
|
|
98
|
+
tokens: 0,
|
|
99
|
+
cost: 0,
|
|
100
|
+
inputTokens: 0,
|
|
101
|
+
outputTokens: 0,
|
|
102
|
+
cacheCreation: 0,
|
|
103
|
+
cacheRead: 0,
|
|
104
|
+
reasoningTokens: 0,
|
|
105
|
+
cachedTokens: 0
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeNumber(value) {
|
|
110
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
111
|
+
if (typeof value === 'string') {
|
|
112
|
+
const parsed = Number(value);
|
|
113
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function extractSummary(stats) {
|
|
119
|
+
const summary = emptySummary();
|
|
120
|
+
const sourceSummary = stats?.summary || {};
|
|
121
|
+
const sourceGlobal = stats?.global || {};
|
|
122
|
+
|
|
123
|
+
summary.requests = normalizeNumber(
|
|
124
|
+
sourceSummary.totalRequests !== undefined ? sourceSummary.totalRequests : sourceSummary.requests
|
|
125
|
+
) || normalizeNumber(sourceGlobal.totalRequests);
|
|
126
|
+
|
|
127
|
+
summary.tokens = normalizeNumber(
|
|
128
|
+
sourceSummary.totalTokens !== undefined ? sourceSummary.totalTokens : sourceSummary.tokens
|
|
129
|
+
) || normalizeNumber(sourceGlobal.totalTokens);
|
|
130
|
+
|
|
131
|
+
summary.cost = normalizeNumber(
|
|
132
|
+
sourceSummary.totalCost !== undefined ? sourceSummary.totalCost : sourceSummary.cost
|
|
133
|
+
) || normalizeNumber(sourceGlobal.totalCost);
|
|
134
|
+
|
|
135
|
+
summary.inputTokens = normalizeNumber(sourceSummary.inputTokens ?? sourceSummary.input);
|
|
136
|
+
summary.outputTokens = normalizeNumber(sourceSummary.outputTokens ?? sourceSummary.output);
|
|
137
|
+
summary.cacheCreation = normalizeNumber(sourceSummary.cacheCreation ?? sourceSummary.cache_creation);
|
|
138
|
+
summary.cacheRead = normalizeNumber(sourceSummary.cacheRead ?? sourceSummary.cache_read);
|
|
139
|
+
summary.reasoningTokens = normalizeNumber(sourceSummary.reasoningTokens ?? sourceSummary.reasoning);
|
|
140
|
+
summary.cachedTokens = normalizeNumber(sourceSummary.cachedTokens ?? sourceSummary.cached);
|
|
141
|
+
|
|
142
|
+
const detailedTotal =
|
|
143
|
+
summary.inputTokens +
|
|
144
|
+
summary.outputTokens +
|
|
145
|
+
summary.cacheCreation +
|
|
146
|
+
summary.cacheRead +
|
|
147
|
+
summary.reasoningTokens +
|
|
148
|
+
summary.cachedTokens;
|
|
149
|
+
|
|
150
|
+
if (!summary.tokens && detailedTotal > 0) {
|
|
151
|
+
summary.tokens = detailedTotal;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return summary;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function mergeSummaries(target, source) {
|
|
158
|
+
target.requests += normalizeNumber(source.requests);
|
|
159
|
+
target.tokens += normalizeNumber(source.tokens);
|
|
160
|
+
target.cost += normalizeNumber(source.cost);
|
|
161
|
+
target.inputTokens += normalizeNumber(source.inputTokens);
|
|
162
|
+
target.outputTokens += normalizeNumber(source.outputTokens);
|
|
163
|
+
target.cacheCreation += normalizeNumber(source.cacheCreation);
|
|
164
|
+
target.cacheRead += normalizeNumber(source.cacheRead);
|
|
165
|
+
target.reasoningTokens += normalizeNumber(source.reasoningTokens);
|
|
166
|
+
target.cachedTokens += normalizeNumber(source.cachedTokens);
|
|
167
|
+
return target;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getRangeDays(timeRange) {
|
|
171
|
+
if (timeRange === 'week') return 7;
|
|
172
|
+
if (timeRange === 'month') return 30;
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function fetchToolStats(toolType, timeRange) {
|
|
177
|
+
const endpointBase = TOOL_ENDPOINTS[toolType];
|
|
178
|
+
if (!endpointBase) {
|
|
179
|
+
throw new Error(`不支持的渠道类型: ${toolType}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (timeRange === 'today') {
|
|
183
|
+
const response = await httpRequest('GET', `${endpointBase}/today`);
|
|
184
|
+
return extractSummary(response.data);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (timeRange === 'all') {
|
|
188
|
+
const response = await httpRequest('GET', `${endpointBase}/summary`);
|
|
189
|
+
return extractSummary(response.data);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const days = getRangeDays(timeRange);
|
|
193
|
+
const merged = emptySummary();
|
|
194
|
+
for (let i = 0; i < days; i++) {
|
|
195
|
+
const date = getDateString(i);
|
|
196
|
+
const response = await httpRequest('GET', `${endpointBase}/daily/${date}`);
|
|
197
|
+
const dailySummary = extractSummary(response.data);
|
|
198
|
+
mergeSummaries(merged, dailySummary);
|
|
199
|
+
}
|
|
200
|
+
return merged;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function fetchOverallStats(timeRange) {
|
|
204
|
+
const byToolType = {};
|
|
205
|
+
const summary = emptySummary();
|
|
206
|
+
|
|
207
|
+
for (const toolType of TOOL_TYPES) {
|
|
208
|
+
const toolSummary = await fetchToolStats(toolType, timeRange);
|
|
209
|
+
byToolType[toolType] = toolSummary;
|
|
210
|
+
mergeSummaries(summary, toolSummary);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { summary, byToolType };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function buildDisplayPayload(type, timeRange, data) {
|
|
217
|
+
if (type) {
|
|
218
|
+
return {
|
|
219
|
+
type,
|
|
220
|
+
timeRange,
|
|
221
|
+
summary: data,
|
|
222
|
+
byToolType: null
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
type: null,
|
|
228
|
+
timeRange,
|
|
229
|
+
summary: data.summary,
|
|
230
|
+
byToolType: data.byToolType
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 查看统计信息
|
|
236
|
+
*/
|
|
237
|
+
async function handleStats(type = null, options = {}) {
|
|
238
|
+
// 检查 UI 服务
|
|
239
|
+
const uiRunning = await checkUIService();
|
|
240
|
+
if (!uiRunning) {
|
|
241
|
+
console.error(chalk.red('\n❌ UI 服务未运行\n'));
|
|
242
|
+
console.log(chalk.yellow('💡 请先启动 UI 服务: ') + chalk.cyan('ctx start\n'));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const timeRange = options.today ? 'today' : options.week ? 'week' : options.month ? 'month' : 'all';
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
if (!validateToolType(type)) {
|
|
250
|
+
console.error(chalk.red(`\n❌ 无效的渠道类型: ${type}\n`));
|
|
251
|
+
console.log(chalk.gray('支持的类型: claude, codex, gemini, opencode\n'));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let payload;
|
|
256
|
+
if (type) {
|
|
257
|
+
const summary = await fetchToolStats(type, timeRange);
|
|
258
|
+
payload = buildDisplayPayload(type, timeRange, summary);
|
|
259
|
+
} else {
|
|
260
|
+
const overall = await fetchOverallStats(timeRange);
|
|
261
|
+
payload = buildDisplayPayload(null, timeRange, overall);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
displayStats(payload);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error(chalk.red(`\n❌ 获取统计失败: ${error.message}\n`));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 显示统计信息
|
|
273
|
+
*/
|
|
274
|
+
function displayStats(stats) {
|
|
275
|
+
const type = stats.type;
|
|
276
|
+
const timeRange = stats.timeRange;
|
|
277
|
+
const title = type ? `${type.toUpperCase()} 统计信息` : '总体统计信息';
|
|
278
|
+
const rangeText = {
|
|
279
|
+
today: '今日',
|
|
280
|
+
week: '本周',
|
|
281
|
+
month: '本月',
|
|
282
|
+
all: '全部'
|
|
283
|
+
}[timeRange];
|
|
284
|
+
|
|
285
|
+
console.log(chalk.bold.cyan(`\n╔══════════════════════════════════════╗`));
|
|
286
|
+
console.log(chalk.bold.cyan(`║ ${title} (${rangeText}) ║`));
|
|
287
|
+
console.log(chalk.bold.cyan(`╚══════════════════════════════════════╝\n`));
|
|
288
|
+
|
|
289
|
+
if (!stats || !stats.summary) {
|
|
290
|
+
console.log(chalk.gray(' 暂无统计数据\n'));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const summary = stats.summary;
|
|
295
|
+
|
|
296
|
+
// 请求统计
|
|
297
|
+
console.log(chalk.bold('📊 请求统计:'));
|
|
298
|
+
console.log(chalk.gray(' 总请求数: ') + chalk.cyan(formatNumber(summary.requests)));
|
|
299
|
+
|
|
300
|
+
// Token 使用
|
|
301
|
+
if (summary.tokens !== undefined) {
|
|
302
|
+
console.log(chalk.bold('\n🎯 Token 使用:'));
|
|
303
|
+
console.log(chalk.gray(' 输入 Tokens: ') + chalk.cyan(formatNumber(summary.inputTokens || 0)));
|
|
304
|
+
console.log(chalk.gray(' 输出 Tokens: ') + chalk.cyan(formatNumber(summary.outputTokens || 0)));
|
|
305
|
+
console.log(chalk.gray(' 缓存创建: ') + chalk.cyan(formatNumber(summary.cacheCreation || 0)));
|
|
306
|
+
console.log(chalk.gray(' 缓存读取: ') + chalk.cyan(formatNumber(summary.cacheRead || 0)));
|
|
307
|
+
console.log(chalk.gray(' 推理 Tokens: ') + chalk.cyan(formatNumber(summary.reasoningTokens || 0)));
|
|
308
|
+
console.log(chalk.gray(' 缓存 Tokens: ') + chalk.cyan(formatNumber(summary.cachedTokens || 0)));
|
|
309
|
+
console.log(chalk.gray(' 总计: ') + chalk.bold.cyan(formatNumber(summary.tokens || 0)));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 成本统计
|
|
313
|
+
if (summary.cost !== undefined) {
|
|
314
|
+
console.log(chalk.bold('\n💰 成本统计:'));
|
|
315
|
+
console.log(chalk.gray(' 总成本: ') + chalk.yellow(`$${normalizeNumber(summary.cost).toFixed(4)}`));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!type && stats.byToolType) {
|
|
319
|
+
const iconMap = { claude: '🟢', codex: '🔵', gemini: '🟣', opencode: '🟠' };
|
|
320
|
+
console.log(chalk.bold('\n📡 分渠道汇总:'));
|
|
321
|
+
TOOL_TYPES.forEach((toolType) => {
|
|
322
|
+
const item = stats.byToolType[toolType] || emptySummary();
|
|
323
|
+
console.log(chalk.gray(` ${iconMap[toolType]} ${toolType.toUpperCase()}:`));
|
|
324
|
+
console.log(
|
|
325
|
+
chalk.gray(
|
|
326
|
+
` 请求: ${formatNumber(item.requests)} | Tokens: ${formatNumber(item.tokens)} | 成本: $${normalizeNumber(item.cost).toFixed(4)}`
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log(chalk.gray('\n💡 提示:'));
|
|
333
|
+
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats --today') + chalk.gray(' 查看今日统计'));
|
|
334
|
+
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats claude') + chalk.gray(' 查看特定渠道'));
|
|
335
|
+
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats opencode') + chalk.gray(' 查看 OpenCode 统计'));
|
|
336
|
+
console.log(chalk.gray(' • 使用 ') + chalk.cyan('ctx stats export') + chalk.gray(' 导出统计数据\n'));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 格式化数字
|
|
341
|
+
*/
|
|
342
|
+
function formatNumber(num) {
|
|
343
|
+
const normalized = normalizeNumber(num);
|
|
344
|
+
return normalized.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 导出统计数据
|
|
349
|
+
*/
|
|
350
|
+
async function handleStatsExport(type = null, format = 'json') {
|
|
351
|
+
console.log(chalk.cyan('\n📤 导出统计数据...\n'));
|
|
352
|
+
|
|
353
|
+
const uiRunning = await checkUIService();
|
|
354
|
+
if (!uiRunning) {
|
|
355
|
+
console.error(chalk.red('❌ UI 服务未运行\n'));
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
if (!validateToolType(type)) {
|
|
361
|
+
console.error(chalk.red(`\n❌ 无效的渠道类型: ${type}\n`));
|
|
362
|
+
console.log(chalk.gray('支持的类型: claude, codex, gemini, opencode\n'));
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const exportFormat = format || 'json';
|
|
367
|
+
if (exportFormat !== 'json') {
|
|
368
|
+
console.log(chalk.yellow(`⚠️ 暂不支持 ${exportFormat} 格式,已回退为 json`));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let payload;
|
|
372
|
+
if (type) {
|
|
373
|
+
const summary = await fetchToolStats(type, 'all');
|
|
374
|
+
payload = buildDisplayPayload(type, 'all', summary);
|
|
375
|
+
} else {
|
|
376
|
+
const overall = await fetchOverallStats('all');
|
|
377
|
+
payload = buildDisplayPayload(null, 'all', overall);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const fs = require('fs');
|
|
381
|
+
const path = require('path');
|
|
382
|
+
const filename = `cc-tool-stats-${type || 'all'}-${Date.now()}.json`;
|
|
383
|
+
const filepath = path.join(process.cwd(), filename);
|
|
384
|
+
|
|
385
|
+
fs.writeFileSync(filepath, JSON.stringify(payload, null, 2));
|
|
386
|
+
|
|
387
|
+
console.log(chalk.green(`✅ 统计数据已导出\n`));
|
|
388
|
+
console.log(chalk.gray(`文件路径: ${filepath}\n`));
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(chalk.red(`\n❌ 导出失败: ${error.message}\n`));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
handleStats,
|
|
397
|
+
handleStatsExport
|
|
398
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 切换项目命令
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { getAvailableProjects } = require('../utils/session');
|
|
5
|
+
const { promptSelectProject } = require('../ui/prompts');
|
|
6
|
+
const { saveConfig } = require('../config/loader');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 切换项目
|
|
10
|
+
*/
|
|
11
|
+
async function switchProject(config) {
|
|
12
|
+
const projects = await getAvailableProjects(config);
|
|
13
|
+
|
|
14
|
+
if (projects.length === 0) {
|
|
15
|
+
console.log(chalk.yellow('没有找到项目'));
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const selectedProject = await promptSelectProject(projects);
|
|
20
|
+
|
|
21
|
+
// 用户取消切换
|
|
22
|
+
if (!selectedProject) {
|
|
23
|
+
console.log(chalk.gray('\n取消切换\n'));
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 更新配置
|
|
28
|
+
config.currentProject = selectedProject;
|
|
29
|
+
config.defaultProject = selectedProject;
|
|
30
|
+
|
|
31
|
+
// 保存到配置文件(保留其余字段)
|
|
32
|
+
saveConfig({
|
|
33
|
+
...config,
|
|
34
|
+
projectsDir: config.projectsDir.replace(os.homedir(), '~')
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 使用解析后的名称显示
|
|
38
|
+
const { parseRealProjectPath } = require('../server/services/sessions');
|
|
39
|
+
const { displayName, fullPath } = parseRealProjectPath(selectedProject);
|
|
40
|
+
|
|
41
|
+
console.log(chalk.green(`\n✅ 已切换到: ${displayName}\n`));
|
|
42
|
+
console.log(chalk.gray(` 路径: ${fullPath}\n`));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
switchProject,
|
|
48
|
+
};
|