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
package/src/ui/menu.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// 菜单显示
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const packageInfo = require('../../package.json');
|
|
5
|
+
|
|
6
|
+
function normalizeCliType(type) {
|
|
7
|
+
if (type === 'claude' || type === 'codex' || type === 'gemini' || type === 'opencode') {
|
|
8
|
+
return type;
|
|
9
|
+
}
|
|
10
|
+
return 'claude';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pickActiveChannel(channels) {
|
|
14
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return channels.find(channel => channel.enabled !== false) || channels[0];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getChannelAndProxyStatus(cliType) {
|
|
21
|
+
const currentType = normalizeCliType(cliType);
|
|
22
|
+
|
|
23
|
+
if (currentType === 'claude') {
|
|
24
|
+
const { getCurrentChannel } = require('../server/services/channels');
|
|
25
|
+
const { getProxyStatus } = require('../server/proxy-server');
|
|
26
|
+
return { channel: getCurrentChannel(), proxyStatus: getProxyStatus() };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (currentType === 'codex') {
|
|
30
|
+
const { getChannels } = require('../server/services/codex-channels');
|
|
31
|
+
const { getCodexProxyStatus } = require('../server/codex-proxy-server');
|
|
32
|
+
const data = getChannels();
|
|
33
|
+
return { channel: pickActiveChannel(data?.channels), proxyStatus: getCodexProxyStatus() };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (currentType === 'gemini') {
|
|
37
|
+
const { getChannels } = require('../server/services/gemini-channels');
|
|
38
|
+
const { getGeminiProxyStatus } = require('../server/gemini-proxy-server');
|
|
39
|
+
const data = getChannels();
|
|
40
|
+
return { channel: pickActiveChannel(data?.channels), proxyStatus: getGeminiProxyStatus() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { getChannels } = require('../server/services/opencode-channels');
|
|
44
|
+
const { getOpenCodeProxyStatus } = require('../server/opencode-proxy-server');
|
|
45
|
+
const data = getChannels();
|
|
46
|
+
return { channel: pickActiveChannel(data?.channels), proxyStatus: getOpenCodeProxyStatus() };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 显示主菜单
|
|
51
|
+
*/
|
|
52
|
+
async function showMainMenu(config) {
|
|
53
|
+
console.log(chalk.bold.cyan('\n╔═══════════════════════════════════════════════╗'));
|
|
54
|
+
console.log(chalk.bold.cyan(`║ Claude Code 会话管理工具 v${packageInfo.version} ║`));
|
|
55
|
+
console.log(chalk.bold.cyan('╚═══════════════════════════════════════════════╝\n'));
|
|
56
|
+
|
|
57
|
+
// 显示当前CLI类型
|
|
58
|
+
const cliTypes = {
|
|
59
|
+
claude: { name: 'Claude Code', color: 'cyan' },
|
|
60
|
+
codex: { name: 'Codex', color: 'green' },
|
|
61
|
+
gemini: { name: 'Gemini', color: 'magenta' },
|
|
62
|
+
opencode: { name: 'OpenCode', color: 'yellow' }
|
|
63
|
+
};
|
|
64
|
+
const currentType = normalizeCliType(config.currentCliType || 'claude');
|
|
65
|
+
const typeInfo = cliTypes[currentType] || cliTypes.claude;
|
|
66
|
+
console.log(chalk[typeInfo.color](`当前类型: ${typeInfo.name}`));
|
|
67
|
+
|
|
68
|
+
const projectName = config.currentProject
|
|
69
|
+
? config.currentProject.replace(/-/g, '/').substring(1)
|
|
70
|
+
: '未设置';
|
|
71
|
+
console.log(chalk.gray(`当前项目: ${projectName}`));
|
|
72
|
+
|
|
73
|
+
// 显示当前渠道和代理状态(根据类型显示对应的渠道和代理)
|
|
74
|
+
try {
|
|
75
|
+
const { channel: currentChannel, proxyStatus } = getChannelAndProxyStatus(currentType);
|
|
76
|
+
|
|
77
|
+
if (currentChannel) {
|
|
78
|
+
console.log(chalk.gray(`当前渠道: ${currentChannel.name}`));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (proxyStatus.running) {
|
|
82
|
+
console.log(chalk.green(`动态切换: 已开启 (端口 ${proxyStatus.port})`));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(chalk.gray('动态切换: 未开启'));
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// 忽略错误
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
91
|
+
|
|
92
|
+
// 获取代理状态,用于显示动态切换的状态(根据当前类型)
|
|
93
|
+
let proxyStatusText = '未开启';
|
|
94
|
+
try {
|
|
95
|
+
const { proxyStatus } = getChannelAndProxyStatus(currentType);
|
|
96
|
+
|
|
97
|
+
if (proxyStatus && proxyStatus.running) {
|
|
98
|
+
proxyStatusText = '已开启';
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
// 忽略错误
|
|
102
|
+
console.error('获取代理状态失败:', err.message);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { action } = await inquirer.prompt([
|
|
106
|
+
{
|
|
107
|
+
type: 'list',
|
|
108
|
+
name: 'action',
|
|
109
|
+
message: '请选择操作:',
|
|
110
|
+
pageSize: 16,
|
|
111
|
+
choices: [
|
|
112
|
+
{ name: chalk.bold.yellow('切换 CLI 类型'), value: 'switch-cli-type' },
|
|
113
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
114
|
+
{ name: chalk.bold.hex('#00D9FF')('启动 Web UI'), value: 'ui' },
|
|
115
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
116
|
+
{ name: chalk.cyan('列出最新对话'), value: 'list' },
|
|
117
|
+
{ name: chalk.green('搜索会话'), value: 'search' },
|
|
118
|
+
{ name: chalk.magenta('切换项目'), value: 'switch' },
|
|
119
|
+
{ name: chalk.hex('#FF6B35')('工作区管理'), value: 'workspace' },
|
|
120
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
121
|
+
{ name: chalk.cyan('渠道管理'), value: 'switch-channel' },
|
|
122
|
+
{ name: chalk.cyan('查看调度状态'), value: 'channel-status' },
|
|
123
|
+
{ name: chalk.cyan(`是否开启动态切换 (${proxyStatusText})`), value: 'toggle-proxy' },
|
|
124
|
+
{ name: chalk.cyan('添加渠道'), value: 'add-channel' },
|
|
125
|
+
{ name: chalk.blue('插件管理'), value: 'plugin-menu' },
|
|
126
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
127
|
+
{ name: chalk.magenta('配置端口'), value: 'port-config' },
|
|
128
|
+
{ name: chalk.yellow('恢复默认配置'), value: 'reset' },
|
|
129
|
+
{ name: chalk.gray('退出程序'), value: 'exit' },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
return action;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
showMainMenu,
|
|
139
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// 交互提示
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 选择会话提示
|
|
7
|
+
*/
|
|
8
|
+
async function promptSelectSession(choices) {
|
|
9
|
+
const { sessionId } = await inquirer.prompt([
|
|
10
|
+
{
|
|
11
|
+
type: 'list',
|
|
12
|
+
name: 'sessionId',
|
|
13
|
+
message: '选择会话或操作:',
|
|
14
|
+
pageSize: 15,
|
|
15
|
+
choices: choices,
|
|
16
|
+
},
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
return sessionId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fork 确认提示
|
|
24
|
+
*/
|
|
25
|
+
async function promptForkConfirm() {
|
|
26
|
+
const { action } = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: 'list',
|
|
29
|
+
name: 'action',
|
|
30
|
+
message: chalk.bold('选择恢复方式:'),
|
|
31
|
+
default: 'continue',
|
|
32
|
+
choices: [
|
|
33
|
+
{
|
|
34
|
+
name: chalk.green('📝 继续原会话 (推荐) - 在原会话上继续对话,所有内容会追加到原文件'),
|
|
35
|
+
value: 'continue',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: chalk.yellow('🌿 创建新分支 (Fork) - 基于原会话创建新会话,保留原会话不变'),
|
|
39
|
+
value: 'fork',
|
|
40
|
+
},
|
|
41
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
42
|
+
{ name: chalk.blue('↩️ 返回重新选择'), value: 'back' },
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
return action;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 搜索关键词提示
|
|
52
|
+
*/
|
|
53
|
+
async function promptSearchKeyword() {
|
|
54
|
+
const { keyword } = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'keyword',
|
|
58
|
+
message: chalk.cyan('🔎 输入搜索关键词:'),
|
|
59
|
+
validate: (input) => {
|
|
60
|
+
if (!input.trim()) {
|
|
61
|
+
return '请输入搜索关键词';
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
return keyword.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 选择项目提示
|
|
73
|
+
*/
|
|
74
|
+
async function promptSelectProject(projects) {
|
|
75
|
+
// 添加取消选项
|
|
76
|
+
const choices = [
|
|
77
|
+
...projects,
|
|
78
|
+
new inquirer.Separator(chalk.gray('─'.repeat(14))),
|
|
79
|
+
{ name: chalk.gray('↩️ 取消切换'), value: null }
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const { project } = await inquirer.prompt([
|
|
83
|
+
{
|
|
84
|
+
type: 'list',
|
|
85
|
+
name: 'project',
|
|
86
|
+
message: chalk.cyan('选择项目:'),
|
|
87
|
+
pageSize: 15,
|
|
88
|
+
choices: choices,
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
return project;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
promptSelectSession,
|
|
97
|
+
promptForkConfirm,
|
|
98
|
+
promptSearchKeyword,
|
|
99
|
+
promptSelectProject,
|
|
100
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// 格式化工具函数
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 格式化时间
|
|
5
|
+
*/
|
|
6
|
+
function formatTime(date) {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const diff = now - new Date(date);
|
|
9
|
+
const minutes = Math.floor(diff / 60000);
|
|
10
|
+
const hours = Math.floor(diff / 3600000);
|
|
11
|
+
const days = Math.floor(diff / 86400000);
|
|
12
|
+
|
|
13
|
+
if (minutes < 1) return '刚刚';
|
|
14
|
+
if (minutes < 60) return `${minutes}分钟前`;
|
|
15
|
+
if (hours < 24) return `${hours}小时前`;
|
|
16
|
+
if (days < 30) return `${days}天前`;
|
|
17
|
+
return new Date(date).toLocaleDateString('zh-CN');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 格式化文件大小
|
|
22
|
+
*/
|
|
23
|
+
function formatSize(bytes) {
|
|
24
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
25
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 截断文本
|
|
31
|
+
*/
|
|
32
|
+
function truncate(text, maxLength) {
|
|
33
|
+
if (!text) return '';
|
|
34
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
35
|
+
if (text.length <= maxLength) return text;
|
|
36
|
+
return text.substring(0, maxLength) + '...';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
formatTime,
|
|
41
|
+
formatSize,
|
|
42
|
+
truncate,
|
|
43
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 检查端口是否被占用
|
|
6
|
+
*/
|
|
7
|
+
function isPortInUse(port, host = '127.0.0.1') {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const server = net.createServer();
|
|
10
|
+
let resolved = false;
|
|
11
|
+
|
|
12
|
+
const finish = (value) => {
|
|
13
|
+
if (!resolved) {
|
|
14
|
+
resolved = true;
|
|
15
|
+
resolve(value);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
server.once('error', (err) => {
|
|
20
|
+
if (err.code === 'EADDRINUSE') {
|
|
21
|
+
finish(true);
|
|
22
|
+
} else {
|
|
23
|
+
finish(false);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
server.once('listening', () => {
|
|
28
|
+
// 等待 close 完成后再返回,避免紧接着重新监听同端口时触发竞态
|
|
29
|
+
server.close((err) => {
|
|
30
|
+
if (err) {
|
|
31
|
+
finish(true);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
finish(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
server.listen(port, host);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 查找占用端口的进程PID
|
|
44
|
+
*/
|
|
45
|
+
function findProcessByPort(port) {
|
|
46
|
+
try {
|
|
47
|
+
// macOS/Linux 使用 lsof
|
|
48
|
+
const result = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
|
|
49
|
+
return result.split('\n').filter(pid => pid);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// 如果 lsof 失败,尝试使用其他命令
|
|
52
|
+
try {
|
|
53
|
+
// 适用于某些 Linux 系统
|
|
54
|
+
const result = execSync(`fuser ${port}/tcp 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
55
|
+
return result.split(/\s+/).filter(pid => pid);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 杀掉占用端口的进程
|
|
64
|
+
*/
|
|
65
|
+
function killProcessByPort(port) {
|
|
66
|
+
try {
|
|
67
|
+
const pids = findProcessByPort(port);
|
|
68
|
+
if (pids.length === 0) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pids.forEach(pid => {
|
|
73
|
+
try {
|
|
74
|
+
execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// 忽略单个进程杀掉失败的错误
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return true;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 等待端口释放
|
|
88
|
+
*/
|
|
89
|
+
async function waitForPortRelease(port, timeout = 3000, host = '127.0.0.1') {
|
|
90
|
+
const startTime = Date.now();
|
|
91
|
+
|
|
92
|
+
while (Date.now() - startTime < timeout) {
|
|
93
|
+
const inUse = await isPortInUse(port, host);
|
|
94
|
+
if (!inUse) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
isPortInUse,
|
|
105
|
+
findProcessByPort,
|
|
106
|
+
killProcessByPort,
|
|
107
|
+
waitForPortRelease
|
|
108
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// 会话相关工具函数
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 获取会话目录
|
|
8
|
+
*/
|
|
9
|
+
function getSessionsDir(config) {
|
|
10
|
+
return path.join(config.projectsDir, config.currentProject);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取所有会话文件
|
|
15
|
+
*/
|
|
16
|
+
function getAllSessions(config) {
|
|
17
|
+
const sessionsDir = getSessionsDir(config);
|
|
18
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
19
|
+
console.log(chalk.red(`会话目录不存在: ${sessionsDir}`));
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const files = fs.readdirSync(sessionsDir)
|
|
24
|
+
.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'))
|
|
25
|
+
.map(file => {
|
|
26
|
+
const filePath = path.join(sessionsDir, file);
|
|
27
|
+
const stats = fs.statSync(filePath);
|
|
28
|
+
return {
|
|
29
|
+
sessionId: file.replace('.jsonl', ''),
|
|
30
|
+
filePath,
|
|
31
|
+
size: stats.size,
|
|
32
|
+
mtime: stats.mtime,
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
36
|
+
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 快速解析会话信息(读取开头和结尾)
|
|
42
|
+
*/
|
|
43
|
+
function parseSessionInfoFast(filePath) {
|
|
44
|
+
try {
|
|
45
|
+
const fileSize = fs.statSync(filePath).size;
|
|
46
|
+
|
|
47
|
+
// 如果文件太大(>10MB),只读取开头和结尾
|
|
48
|
+
if (fileSize > 10 * 1024 * 1024) {
|
|
49
|
+
const fd = fs.openSync(filePath, 'r');
|
|
50
|
+
|
|
51
|
+
// 读取开头 32KB(扩大范围以找到第一条消息)
|
|
52
|
+
const headBuffer = Buffer.alloc(32 * 1024);
|
|
53
|
+
const headBytesRead = fs.readSync(fd, headBuffer, 0, 32 * 1024, 0);
|
|
54
|
+
|
|
55
|
+
// 读取结尾 8KB
|
|
56
|
+
const tailBuffer = Buffer.alloc(8192);
|
|
57
|
+
const tailOffset = Math.max(0, fileSize - 8192);
|
|
58
|
+
fs.readSync(fd, tailBuffer, 0, 8192, tailOffset);
|
|
59
|
+
|
|
60
|
+
fs.closeSync(fd);
|
|
61
|
+
|
|
62
|
+
const headContent = headBuffer.slice(0, headBytesRead).toString('utf8');
|
|
63
|
+
const tailContent = tailBuffer.toString('utf8');
|
|
64
|
+
|
|
65
|
+
const headLines = headContent.split('\n');
|
|
66
|
+
const tailLines = tailContent.split('\n').slice(-20);
|
|
67
|
+
|
|
68
|
+
return parseLinesWithTail(headLines, tailLines);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 小文件直接读取
|
|
72
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
|
|
75
|
+
return parseLinesWithTail(lines, lines.slice(-20));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return { summary: '', gitBranch: '', firstMessage: '', lastMessage: '', messageCount: 0 };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 解析行数据(包含开头和结尾)
|
|
83
|
+
*/
|
|
84
|
+
function parseLinesWithTail(headLines, tailLines) {
|
|
85
|
+
let summary = '';
|
|
86
|
+
let gitBranch = '';
|
|
87
|
+
let firstMessage = '';
|
|
88
|
+
let lastMessage = '';
|
|
89
|
+
let lastUserMessage = '';
|
|
90
|
+
|
|
91
|
+
// 解析开头(查找第一条有效消息,跳过 file-history-snapshot)
|
|
92
|
+
for (const line of headLines) {
|
|
93
|
+
if (!line.trim()) continue;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const json = JSON.parse(line);
|
|
97
|
+
|
|
98
|
+
if (json.type === 'summary' && json.summary) {
|
|
99
|
+
summary = json.summary;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (json.gitBranch && !gitBranch) {
|
|
103
|
+
gitBranch = json.gitBranch;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 优先找用户消息作为首条消息
|
|
107
|
+
if (json.type === 'user' && json.message && json.message.content !== 'Warmup' && !firstMessage) {
|
|
108
|
+
firstMessage = typeof json.message.content === 'string'
|
|
109
|
+
? json.message.content
|
|
110
|
+
: JSON.stringify(json.message.content);
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// 忽略解析错误
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 如果开头找不到用户消息,尝试从尾部向前找
|
|
118
|
+
if (!firstMessage) {
|
|
119
|
+
for (let i = 0; i < tailLines.length; i++) {
|
|
120
|
+
const line = tailLines[i];
|
|
121
|
+
if (!line.trim()) continue;
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const json = JSON.parse(line);
|
|
125
|
+
if (json.type === 'user' && json.message && json.message.content !== 'Warmup') {
|
|
126
|
+
firstMessage = typeof json.message.content === 'string'
|
|
127
|
+
? json.message.content
|
|
128
|
+
: JSON.stringify(json.message.content);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// 忽略
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 解析结尾 - 获取最后的对话
|
|
138
|
+
const validTailLines = [];
|
|
139
|
+
for (const line of tailLines) {
|
|
140
|
+
if (!line.trim()) continue;
|
|
141
|
+
try {
|
|
142
|
+
const json = JSON.parse(line);
|
|
143
|
+
if (json.type === 'user' || json.type === 'assistant') {
|
|
144
|
+
validTailLines.push(json);
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// 忽略
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 从后往前找最后一条有效消息
|
|
152
|
+
for (let i = validTailLines.length - 1; i >= 0; i--) {
|
|
153
|
+
const json = validTailLines[i];
|
|
154
|
+
|
|
155
|
+
if (!lastMessage && json.type === 'assistant' && json.message && json.message.content) {
|
|
156
|
+
const content = json.message.content;
|
|
157
|
+
if (Array.isArray(content)) {
|
|
158
|
+
for (const item of content) {
|
|
159
|
+
if (item.type === 'text' && item.text) {
|
|
160
|
+
lastMessage = item.text;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else if (typeof content === 'string') {
|
|
165
|
+
lastMessage = content;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!lastUserMessage && json.type === 'user' && json.message && json.message.content) {
|
|
170
|
+
const content = json.message.content;
|
|
171
|
+
if (typeof content === 'string' && content !== 'Warmup') {
|
|
172
|
+
lastUserMessage = content;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (lastMessage && lastUserMessage) break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 如果仍然找不到首条消息,使用 summary 或 gitBranch 作为备选
|
|
180
|
+
if (!firstMessage && summary) {
|
|
181
|
+
firstMessage = `[摘要] ${summary}`;
|
|
182
|
+
} else if (!firstMessage && gitBranch) {
|
|
183
|
+
firstMessage = `[分支] ${gitBranch}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
summary,
|
|
188
|
+
gitBranch,
|
|
189
|
+
firstMessage,
|
|
190
|
+
lastMessage: lastMessage || lastUserMessage, // 优先显示助手回复,否则显示用户消息
|
|
191
|
+
messageCount: 0
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 获取所有可用的项目
|
|
197
|
+
*/
|
|
198
|
+
async function getAvailableProjects(config) {
|
|
199
|
+
const projectsDir = config.projectsDir;
|
|
200
|
+
if (!fs.existsSync(projectsDir)) {
|
|
201
|
+
console.log(chalk.red(`项目目录不存在: ${projectsDir}`));
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 获取项目列表和统计信息(包含解析后的名称)
|
|
206
|
+
const { getProjectsWithStats, getProjectOrder } = require('../server/services/sessions');
|
|
207
|
+
const projects = await getProjectsWithStats(config);
|
|
208
|
+
const savedOrder = getProjectOrder(config);
|
|
209
|
+
const projectList = Array.isArray(projects) ? projects : [];
|
|
210
|
+
|
|
211
|
+
// 按保存的顺序排列
|
|
212
|
+
let orderedProjects = [];
|
|
213
|
+
if (savedOrder.length > 0) {
|
|
214
|
+
const projectMap = new Map(projectList.map(p => [p.name, p]));
|
|
215
|
+
for (const name of savedOrder) {
|
|
216
|
+
if (projectMap.has(name)) {
|
|
217
|
+
orderedProjects.push(projectMap.get(name));
|
|
218
|
+
projectMap.delete(name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// 添加不在排序中的新项目
|
|
222
|
+
orderedProjects.push(...projectMap.values());
|
|
223
|
+
} else {
|
|
224
|
+
orderedProjects = projectList;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 转换为选项格式
|
|
228
|
+
return orderedProjects.map(project => ({
|
|
229
|
+
name: `${project.displayName} (${project.sessionCount} 会话)`,
|
|
230
|
+
value: project.name,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
getSessionsDir,
|
|
236
|
+
getAllSessions,
|
|
237
|
+
parseSessionInfoFast,
|
|
238
|
+
parseLinesWithTail,
|
|
239
|
+
getAvailableProjects,
|
|
240
|
+
};
|