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,514 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
|
|
8
|
+
const { loadAliases } = require('../services/alias');
|
|
9
|
+
const { broadcastLog } = require('../websocket-server');
|
|
10
|
+
|
|
11
|
+
module.exports = (config) => {
|
|
12
|
+
// GET /api/sessions/search/global - Search sessions across all projects
|
|
13
|
+
router.get('/search/global', async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const { keyword, context } = req.query;
|
|
16
|
+
|
|
17
|
+
if (!keyword) {
|
|
18
|
+
return res.status(400).json({ error: 'Keyword is required' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const contextLength = context ? parseInt(context) : 35;
|
|
22
|
+
const results = await searchSessionsAcrossProjects(config, keyword, contextLength);
|
|
23
|
+
|
|
24
|
+
res.json({
|
|
25
|
+
keyword,
|
|
26
|
+
totalMatches: results.reduce((sum, r) => sum + r.matchCount, 0),
|
|
27
|
+
sessions: results
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error searching sessions globally:', error);
|
|
31
|
+
res.status(500).json({ error: error.message });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// GET /api/sessions/recent - Get recent sessions across all projects
|
|
36
|
+
router.get('/recent/list', async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const limit = parseInt(req.query.limit) || 5;
|
|
39
|
+
const sessions = await getRecentSessions(config, limit);
|
|
40
|
+
res.json({ sessions });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error fetching recent sessions:', error);
|
|
43
|
+
res.status(500).json({ error: error.message });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// GET /api/sessions/:projectName - Get sessions for a project
|
|
48
|
+
router.get('/:projectName', async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const { projectName } = req.params;
|
|
51
|
+
const result = await getSessionsForProject(config, projectName);
|
|
52
|
+
const aliases = loadAliases();
|
|
53
|
+
|
|
54
|
+
// Parse project path info
|
|
55
|
+
const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
|
|
56
|
+
|
|
57
|
+
res.json({
|
|
58
|
+
sessions: result.sessions,
|
|
59
|
+
totalSize: result.totalSize,
|
|
60
|
+
aliases,
|
|
61
|
+
projectInfo: {
|
|
62
|
+
name: projectName,
|
|
63
|
+
displayName,
|
|
64
|
+
fullPath
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Error fetching sessions:', error);
|
|
69
|
+
res.status(500).json({ error: error.message });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// DELETE /api/sessions/:projectName/:sessionId - Delete a session
|
|
74
|
+
router.delete('/:projectName/:sessionId', (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
const { projectName, sessionId } = req.params;
|
|
77
|
+
const result = deleteSession(config, projectName, sessionId);
|
|
78
|
+
res.json(result);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error deleting session:', error);
|
|
81
|
+
res.status(500).json({ error: error.message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// POST /api/sessions/:projectName/:sessionId/fork - Fork a session
|
|
86
|
+
router.post('/:projectName/:sessionId/fork', (req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const { projectName, sessionId } = req.params;
|
|
89
|
+
const result = forkSession(config, projectName, sessionId);
|
|
90
|
+
res.json(result);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Error forking session:', error);
|
|
93
|
+
res.status(500).json({ error: error.message });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// POST /api/sessions/:projectName/create - Create a new session
|
|
98
|
+
router.post('/:projectName/create', (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const { projectName } = req.params;
|
|
101
|
+
const { toolType = 'claude' } = req.body; // 'claude', 'codex', 或 'gemini'
|
|
102
|
+
const crypto = require('crypto');
|
|
103
|
+
|
|
104
|
+
// 解析项目路径
|
|
105
|
+
const { fullPath } = parseRealProjectPath(projectName);
|
|
106
|
+
|
|
107
|
+
// 生成新的 session ID
|
|
108
|
+
const newSessionId = crypto.randomUUID();
|
|
109
|
+
|
|
110
|
+
// 根据工具类型决定会话文件路径
|
|
111
|
+
let sessionDir, sessionFile;
|
|
112
|
+
|
|
113
|
+
if (toolType === 'claude') {
|
|
114
|
+
// Claude Code: 直接创建在项目的 .claude/sessions/ 目录(与 Claude Code 默认行为一致)
|
|
115
|
+
sessionDir = path.join(fullPath, '.claude', 'sessions');
|
|
116
|
+
sessionFile = path.join(sessionDir, `${newSessionId}.jsonl`);
|
|
117
|
+
} else if (toolType === 'codex') {
|
|
118
|
+
// Codex: ~/.codex/sessions/YYYY/MM/DD/{sessionId}.jsonl
|
|
119
|
+
const now = new Date();
|
|
120
|
+
const year = now.getFullYear();
|
|
121
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
122
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
123
|
+
sessionDir = path.join(os.homedir(), '.codex', 'sessions', String(year), month, day);
|
|
124
|
+
sessionFile = path.join(sessionDir, `${newSessionId}.jsonl`);
|
|
125
|
+
} else if (toolType === 'gemini') {
|
|
126
|
+
// Gemini: ~/.gemini/tmp/{hash}/chats/{sessionId}.json
|
|
127
|
+
const pathHash = crypto.createHash('sha256').update(fullPath).digest('hex');
|
|
128
|
+
sessionDir = path.join(os.homedir(), '.gemini', 'tmp', pathHash, 'chats');
|
|
129
|
+
sessionFile = path.join(sessionDir, `${newSessionId}.json`);
|
|
130
|
+
} else {
|
|
131
|
+
return res.status(400).json({ error: 'Invalid toolType. Must be claude, codex, or gemini' });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 确保目录存在
|
|
135
|
+
if (!fs.existsSync(sessionDir)) {
|
|
136
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 创建初始化会话文件
|
|
140
|
+
const timestamp = new Date().toISOString();
|
|
141
|
+
let initialContent;
|
|
142
|
+
|
|
143
|
+
if (toolType === 'gemini') {
|
|
144
|
+
// Gemini 使用 JSON 格式
|
|
145
|
+
initialContent = JSON.stringify({
|
|
146
|
+
id: newSessionId,
|
|
147
|
+
projectPath: fullPath,
|
|
148
|
+
createdAt: timestamp,
|
|
149
|
+
messages: []
|
|
150
|
+
}, null, 2);
|
|
151
|
+
} else {
|
|
152
|
+
// Claude 和 Codex 使用 JSONL 格式
|
|
153
|
+
const metadata = {
|
|
154
|
+
type: 'metadata',
|
|
155
|
+
cwd: fullPath,
|
|
156
|
+
gitBranch: null,
|
|
157
|
+
timestamp: timestamp
|
|
158
|
+
};
|
|
159
|
+
initialContent = JSON.stringify(metadata) + '\n';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fs.writeFileSync(sessionFile, initialContent, 'utf8');
|
|
163
|
+
|
|
164
|
+
// 广播日志
|
|
165
|
+
broadcastLog({
|
|
166
|
+
type: 'action',
|
|
167
|
+
action: 'create_session',
|
|
168
|
+
message: `创建新会话: ${newSessionId.substring(0, 8)} (${toolType})`,
|
|
169
|
+
sessionId: newSessionId,
|
|
170
|
+
tool: toolType,
|
|
171
|
+
timestamp: Date.now()
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
res.json({
|
|
175
|
+
success: true,
|
|
176
|
+
sessionId: newSessionId,
|
|
177
|
+
sessionFile,
|
|
178
|
+
toolType,
|
|
179
|
+
projectName
|
|
180
|
+
});
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('Error creating session:', error);
|
|
183
|
+
res.status(500).json({ error: error.message });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// POST /api/sessions/:projectName/order - Save session order
|
|
188
|
+
router.post('/:projectName/order', (req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const { projectName } = req.params;
|
|
191
|
+
const { order } = req.body;
|
|
192
|
+
if (!Array.isArray(order)) {
|
|
193
|
+
return res.status(400).json({ error: 'order must be an array' });
|
|
194
|
+
}
|
|
195
|
+
saveSessionOrder(projectName, order);
|
|
196
|
+
res.json({ success: true });
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error('Error saving session order:', error);
|
|
199
|
+
res.status(500).json({ error: error.message });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// GET /api/sessions/:projectName/search - Search sessions content
|
|
204
|
+
router.get('/:projectName/search', (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const { projectName } = req.params;
|
|
207
|
+
const { keyword, context } = req.query;
|
|
208
|
+
|
|
209
|
+
if (!keyword) {
|
|
210
|
+
return res.status(400).json({ error: 'Keyword is required' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const contextLength = context ? parseInt(context) : 15;
|
|
214
|
+
const results = searchSessions(config, projectName, keyword, contextLength);
|
|
215
|
+
|
|
216
|
+
res.json({
|
|
217
|
+
keyword,
|
|
218
|
+
totalMatches: results.reduce((sum, r) => sum + r.matchCount, 0),
|
|
219
|
+
sessions: results
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('Error searching sessions:', error);
|
|
223
|
+
res.status(500).json({ error: error.message });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// GET /api/sessions/:projectName/:sessionId/messages - Get session messages with pagination
|
|
228
|
+
router.get('/:projectName/:sessionId/messages', async (req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
const { projectName, sessionId } = req.params;
|
|
231
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
232
|
+
|
|
233
|
+
console.log(`[Messages API] Request for ${projectName}/${sessionId}, page=${page}, limit=${limit}`);
|
|
234
|
+
|
|
235
|
+
const pageNum = parseInt(page);
|
|
236
|
+
const limitNum = parseInt(limit);
|
|
237
|
+
|
|
238
|
+
// Parse real project path
|
|
239
|
+
const { fullPath } = parseRealProjectPath(projectName);
|
|
240
|
+
console.log(`[Messages API] Parsed project path: ${fullPath}`);
|
|
241
|
+
|
|
242
|
+
// Try to find session file
|
|
243
|
+
let sessionFile = null;
|
|
244
|
+
const possiblePaths = [
|
|
245
|
+
path.join(fullPath, '.claude', 'sessions', sessionId + '.jsonl'),
|
|
246
|
+
path.join(os.homedir(), '.claude', 'projects', projectName, sessionId + '.jsonl')
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
console.log(`[Messages API] Trying paths:`, possiblePaths);
|
|
250
|
+
|
|
251
|
+
for (const testPath of possiblePaths) {
|
|
252
|
+
if (fs.existsSync(testPath)) {
|
|
253
|
+
sessionFile = testPath;
|
|
254
|
+
console.log(`[Messages API] Found session file: ${sessionFile}`);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!sessionFile) {
|
|
260
|
+
console.error(`[Messages API] Session file not found for: ${sessionId}`);
|
|
261
|
+
return res.status(404).json({
|
|
262
|
+
error: `Session file not found: ${sessionId}`,
|
|
263
|
+
triedPaths: possiblePaths
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check if session has actual messages (not just file-history-snapshots)
|
|
268
|
+
if (!hasActualMessages(sessionFile)) {
|
|
269
|
+
console.warn(`[Messages API] Session ${sessionId} has no actual messages (only file-history-snapshots)`);
|
|
270
|
+
return res.status(404).json({
|
|
271
|
+
error: `Session has no conversation messages: ${sessionId}`,
|
|
272
|
+
reason: 'This session contains only file history snapshots, not actual conversation data'
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Read and parse session file
|
|
277
|
+
const allMessages = [];
|
|
278
|
+
const metadata = {};
|
|
279
|
+
|
|
280
|
+
const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
|
|
281
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
for await (const line of rl) {
|
|
285
|
+
if (!line.trim()) continue;
|
|
286
|
+
try {
|
|
287
|
+
const json = JSON.parse(line);
|
|
288
|
+
|
|
289
|
+
if (json.type === 'summary' && json.summary) {
|
|
290
|
+
metadata.summary = json.summary;
|
|
291
|
+
}
|
|
292
|
+
if (json.gitBranch) {
|
|
293
|
+
metadata.gitBranch = json.gitBranch;
|
|
294
|
+
}
|
|
295
|
+
if (json.cwd) {
|
|
296
|
+
metadata.cwd = json.cwd;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (json.type === 'user' || json.type === 'assistant') {
|
|
300
|
+
const message = {
|
|
301
|
+
type: json.type,
|
|
302
|
+
content: null,
|
|
303
|
+
timestamp: json.timestamp || null,
|
|
304
|
+
model: json.model || null
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (json.type === 'user') {
|
|
308
|
+
if (typeof json.message?.content === 'string') {
|
|
309
|
+
message.content = json.message.content;
|
|
310
|
+
} else if (Array.isArray(json.message?.content)) {
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (const item of json.message.content) {
|
|
313
|
+
if (item.type === 'text' && item.text) {
|
|
314
|
+
parts.push(item.text);
|
|
315
|
+
} else if (item.type === 'tool_result') {
|
|
316
|
+
const resultContent = typeof item.content === 'string'
|
|
317
|
+
? item.content
|
|
318
|
+
: JSON.stringify(item.content, null, 2);
|
|
319
|
+
parts.push(`**[工具结果]**\n\`\`\`\n${resultContent}\n\`\`\``);
|
|
320
|
+
} else if (item.type === 'image') {
|
|
321
|
+
parts.push('[图片]');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
message.content = parts.join('\n\n') || '[工具交互]';
|
|
325
|
+
}
|
|
326
|
+
} else if (json.type === 'assistant') {
|
|
327
|
+
if (Array.isArray(json.message?.content)) {
|
|
328
|
+
const parts = [];
|
|
329
|
+
for (const item of json.message.content) {
|
|
330
|
+
if (item.type === 'text' && item.text) {
|
|
331
|
+
parts.push(item.text);
|
|
332
|
+
} else if (item.type === 'tool_use') {
|
|
333
|
+
const inputStr = JSON.stringify(item.input, null, 2);
|
|
334
|
+
parts.push(`**[调用工具: ${item.name}]**\n\`\`\`json\n${inputStr}\n\`\`\``);
|
|
335
|
+
} else if (item.type === 'thinking' && item.thinking) {
|
|
336
|
+
parts.push(`**[思考]**\n${item.thinking}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
message.content = parts.join('\n\n') || '[处理中...]';
|
|
340
|
+
} else if (typeof json.message?.content === 'string') {
|
|
341
|
+
message.content = json.message.content;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (message.content && message.content !== 'Warmup') {
|
|
346
|
+
allMessages.push(message);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
// Skip invalid lines
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
rl.close();
|
|
355
|
+
stream.destroy();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Sort messages (desc = newest first)
|
|
359
|
+
if (order === 'desc') {
|
|
360
|
+
allMessages.reverse();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log(`[Messages API] Parsed ${allMessages.length} total messages`);
|
|
364
|
+
|
|
365
|
+
// Pagination
|
|
366
|
+
const total = allMessages.length;
|
|
367
|
+
const startIndex = (pageNum - 1) * limitNum;
|
|
368
|
+
const endIndex = startIndex + limitNum;
|
|
369
|
+
const messages = allMessages.slice(startIndex, endIndex);
|
|
370
|
+
const hasMore = endIndex < total;
|
|
371
|
+
|
|
372
|
+
console.log(`[Messages API] Returning ${messages.length} messages (page ${pageNum}, total ${total})`);
|
|
373
|
+
|
|
374
|
+
res.json({
|
|
375
|
+
messages,
|
|
376
|
+
metadata,
|
|
377
|
+
pagination: {
|
|
378
|
+
page: pageNum,
|
|
379
|
+
limit: limitNum,
|
|
380
|
+
total,
|
|
381
|
+
hasMore
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('Error fetching session messages:', error);
|
|
386
|
+
res.status(500).json({ error: error.message });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// POST /api/sessions/:projectName/:sessionId/launch - Return session launch command for copy
|
|
391
|
+
router.post('/:projectName/:sessionId/launch', async (req, res) => {
|
|
392
|
+
try {
|
|
393
|
+
const { projectName, sessionId } = req.params;
|
|
394
|
+
const path = require('path');
|
|
395
|
+
const fs = require('fs');
|
|
396
|
+
const os = require('os');
|
|
397
|
+
|
|
398
|
+
// Parse real project path (important for cross-project sessions)
|
|
399
|
+
const { fullPath } = parseRealProjectPath(projectName);
|
|
400
|
+
|
|
401
|
+
const projectSessionsDir = path.join(fullPath, '.claude', 'sessions');
|
|
402
|
+
const projectSessionFile = path.join(projectSessionsDir, sessionId + '.jsonl');
|
|
403
|
+
|
|
404
|
+
// Try to find session file in multiple possible locations
|
|
405
|
+
let sessionFile = null;
|
|
406
|
+
const possiblePaths = [
|
|
407
|
+
projectSessionFile,
|
|
408
|
+
// Location 2: User's .claude/projects directory (ClaudeCode default)
|
|
409
|
+
path.join(os.homedir(), '.claude', 'projects', projectName, sessionId + '.jsonl')
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
for (const testPath of possiblePaths) {
|
|
413
|
+
if (fs.existsSync(testPath)) {
|
|
414
|
+
sessionFile = testPath;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 如果会话只存在于全局目录,则复制到项目的 .claude/sessions 目录,避免 claude -r 找不到文件
|
|
420
|
+
if (sessionFile && sessionFile !== projectSessionFile) {
|
|
421
|
+
try {
|
|
422
|
+
if (!fs.existsSync(projectSessionsDir)) {
|
|
423
|
+
fs.mkdirSync(projectSessionsDir, { recursive: true });
|
|
424
|
+
}
|
|
425
|
+
fs.copyFileSync(sessionFile, projectSessionFile);
|
|
426
|
+
sessionFile = projectSessionFile;
|
|
427
|
+
} catch (copyError) {
|
|
428
|
+
console.warn('Failed to sync session file to project directory:', copyError.message);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!sessionFile) {
|
|
433
|
+
console.error(`Session file not found in any location for session: ${sessionId}`);
|
|
434
|
+
console.error('Tried paths:', possiblePaths);
|
|
435
|
+
return res.status(404).json({
|
|
436
|
+
error: `No conversation found with session ID: ${sessionId}`,
|
|
437
|
+
details: `Tried locations: ${possiblePaths.join(', ')}`
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Extract working directory from session file
|
|
442
|
+
let cwd = fullPath; // Default to project directory
|
|
443
|
+
try {
|
|
444
|
+
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
445
|
+
const firstLine = content.split('\n')[0];
|
|
446
|
+
if (firstLine) {
|
|
447
|
+
const json = JSON.parse(firstLine);
|
|
448
|
+
if (json.cwd) {
|
|
449
|
+
cwd = json.cwd;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
console.warn('Unable to extract cwd from session, using project path:', e.message);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 确保会话文件在 cwd 的 .claude/sessions/ 目录下
|
|
457
|
+
// 这样 claude -r 才能找到文件
|
|
458
|
+
const cwdSessionsDir = path.join(cwd, '.claude', 'sessions');
|
|
459
|
+
const cwdSessionFile = path.join(cwdSessionsDir, sessionId + '.jsonl');
|
|
460
|
+
|
|
461
|
+
// 如果会话文件不在 cwd 的 sessions 目录,复制过去
|
|
462
|
+
if (sessionFile !== cwdSessionFile && !fs.existsSync(cwdSessionFile)) {
|
|
463
|
+
try {
|
|
464
|
+
if (!fs.existsSync(cwdSessionsDir)) {
|
|
465
|
+
fs.mkdirSync(cwdSessionsDir, { recursive: true });
|
|
466
|
+
}
|
|
467
|
+
fs.copyFileSync(sessionFile, cwdSessionFile);
|
|
468
|
+
console.log(`[Launch] Copied session to cwd: ${cwdSessionFile}`);
|
|
469
|
+
} catch (copyError) {
|
|
470
|
+
console.warn('[Launch] Failed to copy session file to cwd:', copyError.message);
|
|
471
|
+
// 如果复制失败,尝试更新 cwd 为项目目录
|
|
472
|
+
if (fs.existsSync(projectSessionsDir)) {
|
|
473
|
+
cwd = fullPath;
|
|
474
|
+
console.log(`[Launch] Fallback to project directory: ${cwd}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Get alias
|
|
480
|
+
const aliases = loadAliases();
|
|
481
|
+
const alias = aliases[sessionId];
|
|
482
|
+
|
|
483
|
+
// 广播行为日志
|
|
484
|
+
broadcastLog({
|
|
485
|
+
type: 'action',
|
|
486
|
+
action: 'launch_session',
|
|
487
|
+
message: `复制会话启动命令 ${alias || sessionId.substring(0, 8)} (claude)`,
|
|
488
|
+
sessionId,
|
|
489
|
+
alias: alias || null,
|
|
490
|
+
tool: 'claude',
|
|
491
|
+
timestamp: Date.now()
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const command = `claude -r ${sessionId}`;
|
|
495
|
+
const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
|
|
496
|
+
const copyCommand = `cd ${quotedCwd} && ${command}`;
|
|
497
|
+
|
|
498
|
+
res.json({
|
|
499
|
+
success: true,
|
|
500
|
+
cwd,
|
|
501
|
+
sessionFile,
|
|
502
|
+
sessionId,
|
|
503
|
+
tool: 'claude',
|
|
504
|
+
command,
|
|
505
|
+
copyCommand
|
|
506
|
+
});
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error('Error preparing launch command:', error);
|
|
509
|
+
res.status(500).json({ error: error.message });
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return router;
|
|
514
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
MODEL_METADATA,
|
|
5
|
+
METADATA_LAST_UPDATED,
|
|
6
|
+
getDefaultSpeedTestModels,
|
|
7
|
+
saveDefaultSpeedTestModels
|
|
8
|
+
} = require('../../config/model-metadata');
|
|
9
|
+
const { loadConfig, saveConfig } = require('../../config/loader');
|
|
10
|
+
|
|
11
|
+
function handleGetModelSettings(req, res) {
|
|
12
|
+
try {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const overrides = config.modelMetadataOverrides || {};
|
|
15
|
+
const defaultSpeedTestModels = getDefaultSpeedTestModels();
|
|
16
|
+
|
|
17
|
+
// Build merged table: built-in + user overrides
|
|
18
|
+
const merged = {};
|
|
19
|
+
for (const [id, meta] of Object.entries(MODEL_METADATA)) {
|
|
20
|
+
merged[id] = overrides[id]
|
|
21
|
+
? {
|
|
22
|
+
limit: { ...meta.limit, ...(overrides[id].limit || {}) },
|
|
23
|
+
pricing: { ...meta.pricing, ...(overrides[id].pricing || {}) }
|
|
24
|
+
}
|
|
25
|
+
: meta;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Also include any user-added custom models from overrides
|
|
29
|
+
for (const [id, meta] of Object.entries(overrides)) {
|
|
30
|
+
if (!merged[id]) {
|
|
31
|
+
merged[id] = meta;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.json({
|
|
36
|
+
models: merged,
|
|
37
|
+
overrides,
|
|
38
|
+
builtinModelIds: Object.keys(MODEL_METADATA),
|
|
39
|
+
lastUpdated: METADATA_LAST_UPDATED,
|
|
40
|
+
defaultSpeedTestModels
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error getting model metadata:', error);
|
|
44
|
+
res.status(500).json({ error: error.message });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// GET /api/settings/model-settings - 获取模型设置(元数据 + 默认测速模型)
|
|
49
|
+
router.get('/model-settings', handleGetModelSettings);
|
|
50
|
+
// backward compatibility
|
|
51
|
+
router.get('/model-metadata', handleGetModelSettings);
|
|
52
|
+
|
|
53
|
+
function handleSaveModelSettings(req, res) {
|
|
54
|
+
try {
|
|
55
|
+
const { overrides, defaultSpeedTestModels } = req.body || {};
|
|
56
|
+
if (overrides !== undefined && (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides))) {
|
|
57
|
+
return res.status(400).json({ error: 'overrides must be an object' });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate each override entry
|
|
61
|
+
if (overrides && typeof overrides === 'object') {
|
|
62
|
+
for (const [modelId, meta] of Object.entries(overrides)) {
|
|
63
|
+
if (typeof modelId !== 'string' || !modelId.trim()) {
|
|
64
|
+
return res.status(400).json({ error: `Invalid model ID: "${modelId}"` });
|
|
65
|
+
}
|
|
66
|
+
if (meta.limit !== undefined) {
|
|
67
|
+
if (typeof meta.limit !== 'object') {
|
|
68
|
+
return res.status(400).json({ error: `${modelId}: limit must be an object` });
|
|
69
|
+
}
|
|
70
|
+
if (meta.limit.context !== undefined && (typeof meta.limit.context !== 'number' || meta.limit.context <= 0)) {
|
|
71
|
+
return res.status(400).json({ error: `${modelId}: limit.context must be a positive number` });
|
|
72
|
+
}
|
|
73
|
+
if (meta.limit.output !== undefined && (typeof meta.limit.output !== 'number' || meta.limit.output <= 0)) {
|
|
74
|
+
return res.status(400).json({ error: `${modelId}: limit.output must be a positive number` });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (meta.pricing !== undefined) {
|
|
78
|
+
if (typeof meta.pricing !== 'object') {
|
|
79
|
+
return res.status(400).json({ error: `${modelId}: pricing must be an object` });
|
|
80
|
+
}
|
|
81
|
+
for (const field of ['input', 'output']) {
|
|
82
|
+
if (meta.pricing[field] !== undefined && (typeof meta.pricing[field] !== 'number' || meta.pricing[field] < 0)) {
|
|
83
|
+
return res.status(400).json({ error: `${modelId}: pricing.${field} must be a non-negative number` });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const config = loadConfig();
|
|
91
|
+
const newConfig = {
|
|
92
|
+
...config,
|
|
93
|
+
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
94
|
+
modelMetadataOverrides: overrides && typeof overrides === 'object'
|
|
95
|
+
? overrides
|
|
96
|
+
: (config.modelMetadataOverrides || {})
|
|
97
|
+
};
|
|
98
|
+
saveConfig(newConfig);
|
|
99
|
+
const persistedDefaultSpeedTestModels = saveDefaultSpeedTestModels(defaultSpeedTestModels);
|
|
100
|
+
|
|
101
|
+
res.json({
|
|
102
|
+
success: true,
|
|
103
|
+
overrides: newConfig.modelMetadataOverrides,
|
|
104
|
+
defaultSpeedTestModels: persistedDefaultSpeedTestModels
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Error saving model metadata:', error);
|
|
108
|
+
res.status(500).json({ error: error.message });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// POST /api/settings/model-settings - 保存模型设置
|
|
113
|
+
router.post('/model-settings', handleSaveModelSettings);
|
|
114
|
+
// backward compatibility
|
|
115
|
+
router.post('/model-metadata', handleSaveModelSettings);
|
|
116
|
+
|
|
117
|
+
// DELETE /api/settings/model-metadata/:modelId - 删除单个模型覆盖项(恢复内置默认值)
|
|
118
|
+
function handleDeleteModelOverride(req, res) {
|
|
119
|
+
try {
|
|
120
|
+
const modelId = decodeURIComponent(req.params.modelId);
|
|
121
|
+
const config = loadConfig();
|
|
122
|
+
const overrides = { ...(config.modelMetadataOverrides || {}) };
|
|
123
|
+
delete overrides[modelId];
|
|
124
|
+
|
|
125
|
+
const newConfig = {
|
|
126
|
+
...config,
|
|
127
|
+
projectsDir: config.projectsDir.replace(require('os').homedir(), '~'),
|
|
128
|
+
modelMetadataOverrides: overrides
|
|
129
|
+
};
|
|
130
|
+
saveConfig(newConfig);
|
|
131
|
+
|
|
132
|
+
res.json({ success: true, modelId });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error('Error deleting model metadata override:', error);
|
|
135
|
+
res.status(500).json({ error: error.message });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
router.delete('/model-settings/:modelId', handleDeleteModelOverride);
|
|
140
|
+
router.delete('/model-metadata/:modelId', handleDeleteModelOverride);
|
|
141
|
+
|
|
142
|
+
module.exports = router;
|