@adversity/coding-tool-x 2.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 +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -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/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -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 +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -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 +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -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 +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -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 +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -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 +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { getGeminiDir } = require('./gemini-config');
|
|
6
|
+
|
|
7
|
+
// 路径映射缓存
|
|
8
|
+
let pathMappingCache = null;
|
|
9
|
+
let pathMappingCacheTime = 0;
|
|
10
|
+
const PATH_MAPPING_CACHE_TTL = 60000; // 1分钟缓存
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 获取 Gemini tmp 目录(包含所有项目)
|
|
14
|
+
*/
|
|
15
|
+
function getTmpDir() {
|
|
16
|
+
return path.join(getGeminiDir(), 'tmp');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 计算路径的 SHA256 hash(与 Gemini CLI 相同的算法)
|
|
21
|
+
* @param {string} filePath - 文件路径
|
|
22
|
+
* @returns {string} hash 值
|
|
23
|
+
*/
|
|
24
|
+
function getFilePathHash(filePath) {
|
|
25
|
+
return crypto.createHash('sha256').update(filePath).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 扫描目录及其子目录,建立 hash → path 映射
|
|
30
|
+
* @param {string} dir - 要扫描的目录
|
|
31
|
+
* @param {Set} targetHashes - 目标 hash 集合
|
|
32
|
+
* @param {number} maxDepth - 最大扫描深度
|
|
33
|
+
* @param {Map} results - 结果映射
|
|
34
|
+
* @param {number} currentDepth - 当前深度
|
|
35
|
+
*/
|
|
36
|
+
function scanDirForHashes(dir, targetHashes, maxDepth, results, currentDepth = 0) {
|
|
37
|
+
if (currentDepth > maxDepth || results.size >= targetHashes.size) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 计算当前目录的 hash
|
|
42
|
+
const hash = getFilePathHash(dir);
|
|
43
|
+
if (targetHashes.has(hash) && !results.has(hash)) {
|
|
44
|
+
results.set(hash, dir);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 如果所有目标都已找到,提前返回
|
|
48
|
+
if (results.size >= targetHashes.size) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 扫描子目录
|
|
53
|
+
try {
|
|
54
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
// 跳过隐藏目录和常见的无关目录
|
|
57
|
+
if (item.name.startsWith('.') ||
|
|
58
|
+
item.name === 'node_modules' ||
|
|
59
|
+
item.name === 'Library' ||
|
|
60
|
+
item.name === 'Applications') {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (item.isDirectory()) {
|
|
65
|
+
scanDirForHashes(
|
|
66
|
+
path.join(dir, item.name),
|
|
67
|
+
targetHashes,
|
|
68
|
+
maxDepth,
|
|
69
|
+
results,
|
|
70
|
+
currentDepth + 1
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 如果所有目标都已找到,提前返回
|
|
74
|
+
if (results.size >= targetHashes.size) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// 忽略权限错误等
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 建立所有项目 hash 到路径的映射(彩虹表方法)
|
|
86
|
+
* @returns {Map} hash → path 映射
|
|
87
|
+
*/
|
|
88
|
+
function buildPathMapping() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
|
|
91
|
+
// 检查缓存是否有效
|
|
92
|
+
if (pathMappingCache && (now - pathMappingCacheTime) < PATH_MAPPING_CACHE_TTL) {
|
|
93
|
+
return pathMappingCache;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const projectHashes = scanProjects();
|
|
97
|
+
if (projectHashes.length === 0) {
|
|
98
|
+
pathMappingCache = new Map();
|
|
99
|
+
pathMappingCacheTime = now;
|
|
100
|
+
return pathMappingCache;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const targetHashes = new Set(projectHashes);
|
|
104
|
+
const results = new Map();
|
|
105
|
+
const homeDir = os.homedir();
|
|
106
|
+
|
|
107
|
+
// 定义要扫描的目录及其最大深度
|
|
108
|
+
// 深度说明:depth=3 表示可以扫描到 Desktop/a/b/c 这样的 4 层目录
|
|
109
|
+
const searchPaths = [
|
|
110
|
+
{ dir: homeDir, depth: 0 }, // 只检查 home 目录本身
|
|
111
|
+
{ dir: path.join(homeDir, 'Desktop'), depth: 4 }, // Desktop 及 4 层子目录
|
|
112
|
+
{ dir: path.join(homeDir, 'Documents'), depth: 4 }, // Documents 及 4 层子目录
|
|
113
|
+
{ dir: path.join(homeDir, 'Downloads'), depth: 3 }, // Downloads 及 3 层子目录
|
|
114
|
+
{ dir: path.join(homeDir, 'Projects'), depth: 4 }, // Projects 及 4 层子目录
|
|
115
|
+
{ dir: path.join(homeDir, 'Code'), depth: 4 }, // Code 及 4 层子目录
|
|
116
|
+
{ dir: path.join(homeDir, 'workspace'), depth: 4 }, // workspace 及 4 层子目录
|
|
117
|
+
{ dir: path.join(homeDir, 'dev'), depth: 4 }, // dev 及 4 层子目录
|
|
118
|
+
{ dir: path.join(homeDir, 'src'), depth: 4 }, // src 及 4 层子目录
|
|
119
|
+
{ dir: path.join(homeDir, 'work'), depth: 4 }, // work 及 4 层子目录
|
|
120
|
+
{ dir: path.join(homeDir, 'repos'), depth: 4 }, // repos 及 4 层子目录
|
|
121
|
+
{ dir: path.join(homeDir, 'github'), depth: 4 }, // github 及 4 层子目录
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
for (const { dir, depth } of searchPaths) {
|
|
125
|
+
if (fs.existsSync(dir)) {
|
|
126
|
+
scanDirForHashes(dir, targetHashes, depth, results);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 如果所有目标都已找到,提前结束
|
|
130
|
+
if (results.size >= targetHashes.size) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pathMappingCache = results;
|
|
136
|
+
pathMappingCacheTime = now;
|
|
137
|
+
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 扫描所有项目目录
|
|
143
|
+
* @returns {Array} 项目 hash 数组
|
|
144
|
+
*/
|
|
145
|
+
function scanProjects() {
|
|
146
|
+
const tmpDir = getTmpDir();
|
|
147
|
+
|
|
148
|
+
if (!fs.existsSync(tmpDir)) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
|
|
153
|
+
|
|
154
|
+
return entries
|
|
155
|
+
.filter(entry => entry.isDirectory())
|
|
156
|
+
// 过滤掉非项目目录(如 bin)- projectHash 是 64 位十六进制字符串
|
|
157
|
+
.filter(entry => /^[a-f0-9]{64}$/.test(entry.name))
|
|
158
|
+
.map(entry => entry.name); // 项目 hash
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 扫描单个项目的所有会话文件
|
|
163
|
+
* @param {string} projectHash - 项目 hash
|
|
164
|
+
* @returns {Array} 会话文件路径数组
|
|
165
|
+
*/
|
|
166
|
+
function scanProjectSessions(projectHash) {
|
|
167
|
+
const chatsDir = path.join(getTmpDir(), projectHash, 'chats');
|
|
168
|
+
|
|
169
|
+
if (!fs.existsSync(chatsDir)) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const entries = fs.readdirSync(chatsDir, { withFileTypes: true });
|
|
174
|
+
|
|
175
|
+
return entries
|
|
176
|
+
.filter(entry => entry.isFile() && entry.name.match(/^session-.*\.json$/))
|
|
177
|
+
.map(entry => {
|
|
178
|
+
const filePath = path.join(chatsDir, entry.name);
|
|
179
|
+
// 文件名格式:session-2025-11-23T02-09-87570eb4.json
|
|
180
|
+
// session-{timestamp}-{shortId}.json
|
|
181
|
+
const match = entry.name.match(/^session-(.*)-([a-f0-9]+)\.json$/);
|
|
182
|
+
|
|
183
|
+
if (!match) return null;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
filePath,
|
|
187
|
+
timestamp: match[1],
|
|
188
|
+
shortId: match[2],
|
|
189
|
+
projectHash
|
|
190
|
+
};
|
|
191
|
+
})
|
|
192
|
+
.filter(Boolean);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 读取会话文件元数据(轻量级)
|
|
197
|
+
* @param {string} filePath - 会话文件路径
|
|
198
|
+
* @returns {Object|null} 会话元数据
|
|
199
|
+
*/
|
|
200
|
+
function readSessionMeta(filePath) {
|
|
201
|
+
try {
|
|
202
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
203
|
+
const session = JSON.parse(content);
|
|
204
|
+
|
|
205
|
+
// Gemini 会话文件结构
|
|
206
|
+
// {
|
|
207
|
+
// sessionId: "uuid",
|
|
208
|
+
// projectHash: "hash",
|
|
209
|
+
// startTime: "ISO timestamp",
|
|
210
|
+
// lastUpdated: "ISO timestamp",
|
|
211
|
+
// messages: [...]
|
|
212
|
+
// }
|
|
213
|
+
|
|
214
|
+
const messages = session.messages || [];
|
|
215
|
+
const firstUserMessage = messages.find(msg => msg.type === 'user');
|
|
216
|
+
|
|
217
|
+
// 计算总 tokens(从所有消息中累加)
|
|
218
|
+
let totalTokens = 0;
|
|
219
|
+
let totalCost = 0;
|
|
220
|
+
let model = '';
|
|
221
|
+
|
|
222
|
+
messages.forEach(msg => {
|
|
223
|
+
if (msg.tokens) {
|
|
224
|
+
totalTokens += msg.tokens.total || 0;
|
|
225
|
+
|
|
226
|
+
// 计算成本(简化版本,使用 gemini-2.5-pro 的定价)
|
|
227
|
+
if (msg.model) {
|
|
228
|
+
model = msg.model;
|
|
229
|
+
const inputTokens = msg.tokens.input || 0;
|
|
230
|
+
const outputTokens = msg.tokens.output || 0;
|
|
231
|
+
// gemini-2.5-pro: $1.25 / 1M input, $5 / 1M output
|
|
232
|
+
totalCost += (inputTokens * 1.25 / 1000000) + (outputTokens * 5 / 1000000);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
sessionId: session.sessionId,
|
|
239
|
+
projectHash: session.projectHash,
|
|
240
|
+
startTime: session.startTime,
|
|
241
|
+
lastUpdated: session.lastUpdated,
|
|
242
|
+
messageCount: messages.length,
|
|
243
|
+
firstMessage: firstUserMessage ? firstUserMessage.content : '',
|
|
244
|
+
tokens: totalTokens,
|
|
245
|
+
cost: totalCost,
|
|
246
|
+
model: model || 'gemini-2.5-pro',
|
|
247
|
+
forkedFrom: session.forkedFrom || null
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`[Gemini Sessions] Failed to read session meta: ${filePath}`, err);
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 读取完整会话内容
|
|
257
|
+
* @param {string} filePath - 会话文件路径
|
|
258
|
+
* @returns {Object|null} 完整会话数据
|
|
259
|
+
*/
|
|
260
|
+
function readSessionFull(filePath) {
|
|
261
|
+
try {
|
|
262
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
263
|
+
return JSON.parse(content);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error(`[Gemini Sessions] Failed to read session: ${filePath}`, err);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 获取所有会话(轻量级,仅元数据)
|
|
272
|
+
* @returns {Array} 会话对象数组
|
|
273
|
+
*/
|
|
274
|
+
function getAllSessions() {
|
|
275
|
+
const projectHashes = scanProjects();
|
|
276
|
+
const allSessions = [];
|
|
277
|
+
|
|
278
|
+
projectHashes.forEach(projectHash => {
|
|
279
|
+
const sessionFiles = scanProjectSessions(projectHash);
|
|
280
|
+
|
|
281
|
+
sessionFiles.forEach(file => {
|
|
282
|
+
const meta = readSessionMeta(file.filePath);
|
|
283
|
+
|
|
284
|
+
if (!meta) return;
|
|
285
|
+
|
|
286
|
+
// 获取文件大小和修改时间
|
|
287
|
+
let size = 0;
|
|
288
|
+
let mtime = meta.lastUpdated;
|
|
289
|
+
try {
|
|
290
|
+
const stats = fs.statSync(file.filePath);
|
|
291
|
+
size = stats.size;
|
|
292
|
+
mtime = stats.mtime.toISOString();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// 忽略错误
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
allSessions.push({
|
|
298
|
+
...meta,
|
|
299
|
+
filePath: file.filePath,
|
|
300
|
+
size,
|
|
301
|
+
mtime,
|
|
302
|
+
source: 'gemini'
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// 按最后更新时间排序(降序,最新的在前)- 前端显示用
|
|
308
|
+
allSessions.sort((a, b) => new Date(b.lastUpdated) - new Date(a.lastUpdated));
|
|
309
|
+
|
|
310
|
+
return allSessions;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 归一化会话数据为 Claude Code 格式
|
|
315
|
+
* @param {Object} geminiSession - Gemini 会话对象
|
|
316
|
+
* @returns {Object} 归一化后的会话对象
|
|
317
|
+
*/
|
|
318
|
+
function normalizeSession(geminiSession) {
|
|
319
|
+
return {
|
|
320
|
+
sessionId: geminiSession.sessionId,
|
|
321
|
+
mtime: geminiSession.mtime,
|
|
322
|
+
size: geminiSession.size,
|
|
323
|
+
filePath: geminiSession.filePath,
|
|
324
|
+
gitBranch: null, // Gemini 不记录 git branch
|
|
325
|
+
firstMessage: geminiSession.firstMessage,
|
|
326
|
+
forkedFrom: geminiSession.forkedFrom || null,
|
|
327
|
+
source: 'gemini',
|
|
328
|
+
tokens: geminiSession.tokens,
|
|
329
|
+
cost: geminiSession.cost,
|
|
330
|
+
model: geminiSession.model,
|
|
331
|
+
projectHash: geminiSession.projectHash,
|
|
332
|
+
projectName: geminiSession.projectHash // 兼容前端统一使用 projectName
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 获取项目的工作目录路径(使用彩虹表方法反推)
|
|
338
|
+
* @param {string} projectHash - 项目 hash
|
|
339
|
+
* @returns {string|null} 项目路径
|
|
340
|
+
*/
|
|
341
|
+
function getProjectPath(projectHash) {
|
|
342
|
+
const pathMapping = buildPathMapping();
|
|
343
|
+
return pathMapping.get(projectHash) || null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 聚合项目列表
|
|
348
|
+
* @returns {Array} 项目对象数组
|
|
349
|
+
*/
|
|
350
|
+
function getProjects() {
|
|
351
|
+
const sessions = getAllSessions();
|
|
352
|
+
const projectMap = new Map();
|
|
353
|
+
|
|
354
|
+
sessions.forEach(session => {
|
|
355
|
+
const projectHash = session.projectHash;
|
|
356
|
+
|
|
357
|
+
if (!projectMap.has(projectHash)) {
|
|
358
|
+
const projectPath = getProjectPath(projectHash);
|
|
359
|
+
|
|
360
|
+
// 如果找到了真实路径,使用目录名作为显示名称
|
|
361
|
+
let displayName;
|
|
362
|
+
if (projectPath) {
|
|
363
|
+
displayName = path.basename(projectPath);
|
|
364
|
+
// 如果是 home 目录,显示 ~
|
|
365
|
+
if (projectPath === os.homedir()) {
|
|
366
|
+
displayName = '~';
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
// 未找到路径,使用 hash 前 8 位
|
|
370
|
+
displayName = `Project ${projectHash.substring(0, 8)}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
projectMap.set(projectHash, {
|
|
374
|
+
name: projectHash,
|
|
375
|
+
displayName,
|
|
376
|
+
path: projectPath,
|
|
377
|
+
sessionCount: 0,
|
|
378
|
+
lastUpdated: session.lastUpdated,
|
|
379
|
+
source: 'gemini'
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const project = projectMap.get(projectHash);
|
|
384
|
+
project.sessionCount++;
|
|
385
|
+
|
|
386
|
+
// 更新最后活动时间
|
|
387
|
+
if (new Date(session.lastUpdated) > new Date(project.lastUpdated)) {
|
|
388
|
+
project.lastUpdated = session.lastUpdated;
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// 转换为数组并排序(按最后活动时间)
|
|
393
|
+
const projects = Array.from(projectMap.values());
|
|
394
|
+
projects.sort((a, b) => new Date(b.lastUpdated) - new Date(a.lastUpdated));
|
|
395
|
+
|
|
396
|
+
return projects;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* 获取指定项目的所有会话
|
|
401
|
+
* @param {string} projectHash - 项目 hash
|
|
402
|
+
* @returns {Array} 会话对象数组
|
|
403
|
+
*/
|
|
404
|
+
function getProjectSessions(projectHash) {
|
|
405
|
+
const allSessions = getAllSessions();
|
|
406
|
+
return allSessions
|
|
407
|
+
.filter(session => session.projectHash === projectHash)
|
|
408
|
+
.map(normalizeSession);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 获取单个会话的完整内容
|
|
413
|
+
* @param {string} sessionId - 会话 ID
|
|
414
|
+
* @returns {Object|null} 完整会话数据
|
|
415
|
+
*/
|
|
416
|
+
function getSession(sessionId) {
|
|
417
|
+
const allSessions = getAllSessions();
|
|
418
|
+
const session = allSessions.find(s => s.sessionId === sessionId);
|
|
419
|
+
|
|
420
|
+
if (!session) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return readSessionFull(session.filePath);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 删除会话
|
|
429
|
+
* @param {string} sessionId - 会话 ID
|
|
430
|
+
* @returns {Object} 删除结果
|
|
431
|
+
*/
|
|
432
|
+
function deleteSession(sessionId) {
|
|
433
|
+
const allSessions = getAllSessions();
|
|
434
|
+
const session = allSessions.find(s => s.sessionId === sessionId);
|
|
435
|
+
|
|
436
|
+
if (!session) {
|
|
437
|
+
throw new Error('Session not found');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
fs.unlinkSync(session.filePath);
|
|
442
|
+
return { success: true, sessionId };
|
|
443
|
+
} catch (err) {
|
|
444
|
+
throw new Error('Failed to delete session: ' + err.message);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 删除项目(删除项目下所有会话)
|
|
450
|
+
* @param {string} projectHash - 项目 hash
|
|
451
|
+
* @returns {Object} 删除结果
|
|
452
|
+
*/
|
|
453
|
+
function deleteProject(projectHash) {
|
|
454
|
+
const projectDir = path.join(getTmpDir(), projectHash);
|
|
455
|
+
|
|
456
|
+
if (!fs.existsSync(projectDir)) {
|
|
457
|
+
throw new Error('Project not found');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
462
|
+
return { success: true, projectHash };
|
|
463
|
+
} catch (err) {
|
|
464
|
+
throw new Error('Failed to delete project: ' + err.message);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 保存项目顺序(Gemini 不需要持久化顺序,前端自行处理)
|
|
470
|
+
* @param {Array} order - 项目 hash 顺序数组
|
|
471
|
+
*/
|
|
472
|
+
function saveProjectOrder(order) {
|
|
473
|
+
// Gemini 不需要持久化项目顺序
|
|
474
|
+
// 前端可以使用 localStorage 保存
|
|
475
|
+
return { success: true };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 获取最近的会话列表
|
|
480
|
+
* @param {number} limit - 限制数量
|
|
481
|
+
* @returns {Array} 会话对象数组
|
|
482
|
+
*/
|
|
483
|
+
function getRecentSessions(limit = 5) {
|
|
484
|
+
const allSessions = getAllSessions();
|
|
485
|
+
return allSessions
|
|
486
|
+
.slice(0, limit)
|
|
487
|
+
.map(normalizeSession);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 按 sessionId 获取会话(返回完整数据用于消息显示)
|
|
492
|
+
* @param {string} sessionId - 会话 ID
|
|
493
|
+
* @returns {Object|null} 完整会话数据
|
|
494
|
+
*/
|
|
495
|
+
function getSessionById(sessionId) {
|
|
496
|
+
const allSessions = getAllSessions();
|
|
497
|
+
const sessionMeta = allSessions.find(s => s.sessionId === sessionId);
|
|
498
|
+
|
|
499
|
+
if (!sessionMeta) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const fullSession = readSessionFull(sessionMeta.filePath);
|
|
504
|
+
|
|
505
|
+
if (!fullSession) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 合并元数据
|
|
510
|
+
return {
|
|
511
|
+
...fullSession,
|
|
512
|
+
filePath: sessionMeta.filePath,
|
|
513
|
+
size: sessionMeta.size,
|
|
514
|
+
mtime: sessionMeta.mtime,
|
|
515
|
+
source: 'gemini'
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 全局搜索会话内容
|
|
521
|
+
* @param {string} keyword - 搜索关键词
|
|
522
|
+
* @param {number} contextLength - 上下文长度(可选)
|
|
523
|
+
* @returns {Array} 搜索结果数组
|
|
524
|
+
*/
|
|
525
|
+
function searchSessions(keyword, contextLength = 35) {
|
|
526
|
+
const allSessions = getAllSessions();
|
|
527
|
+
const results = [];
|
|
528
|
+
|
|
529
|
+
allSessions.forEach(sessionMeta => {
|
|
530
|
+
const fullSession = readSessionFull(sessionMeta.filePath);
|
|
531
|
+
|
|
532
|
+
if (!fullSession || !fullSession.messages) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const matches = [];
|
|
537
|
+
|
|
538
|
+
fullSession.messages.forEach((msg, index) => {
|
|
539
|
+
let content = '';
|
|
540
|
+
|
|
541
|
+
// 提取消息内容
|
|
542
|
+
if (msg.type === 'user' || msg.type === 'assistant') {
|
|
543
|
+
content = msg.content || '';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 搜索关键词
|
|
547
|
+
const lowerContent = content.toLowerCase();
|
|
548
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
549
|
+
|
|
550
|
+
if (lowerContent.includes(lowerKeyword)) {
|
|
551
|
+
const keywordIndex = lowerContent.indexOf(lowerKeyword);
|
|
552
|
+
|
|
553
|
+
// 提取上下文
|
|
554
|
+
const start = Math.max(0, keywordIndex - contextLength);
|
|
555
|
+
const end = Math.min(content.length, keywordIndex + keyword.length + contextLength);
|
|
556
|
+
|
|
557
|
+
let context = content.substring(start, end);
|
|
558
|
+
|
|
559
|
+
// 添加省略号
|
|
560
|
+
if (start > 0) context = '...' + context;
|
|
561
|
+
if (end < content.length) context = context + '...';
|
|
562
|
+
|
|
563
|
+
matches.push({
|
|
564
|
+
messageIndex: index,
|
|
565
|
+
role: msg.type,
|
|
566
|
+
context,
|
|
567
|
+
timestamp: msg.timestamp
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
if (matches.length > 0) {
|
|
573
|
+
results.push({
|
|
574
|
+
sessionId: sessionMeta.sessionId,
|
|
575
|
+
projectHash: sessionMeta.projectHash,
|
|
576
|
+
firstMessage: sessionMeta.firstMessage,
|
|
577
|
+
lastUpdated: sessionMeta.lastUpdated,
|
|
578
|
+
matches,
|
|
579
|
+
matchCount: matches.length,
|
|
580
|
+
source: 'gemini'
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// 按匹配数量排序
|
|
586
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
587
|
+
|
|
588
|
+
return results;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* 保存会话顺序(Gemini 不需要持久化顺序,前端自行处理)
|
|
593
|
+
* @param {string} projectHash - 项目 hash
|
|
594
|
+
* @param {Array} order - 会话 ID 顺序数组
|
|
595
|
+
*/
|
|
596
|
+
function saveSessionOrder(projectHash, order) {
|
|
597
|
+
// Gemini 不需要持久化会话顺序
|
|
598
|
+
// 前端可以使用 localStorage 保存
|
|
599
|
+
return { success: true };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Fork 会话(复制会话文件)
|
|
604
|
+
* @param {string} sessionId - 原会话 ID
|
|
605
|
+
* @returns {Object} Fork 结果
|
|
606
|
+
*/
|
|
607
|
+
function forkSession(sessionId) {
|
|
608
|
+
const allSessions = getAllSessions();
|
|
609
|
+
const sourceSession = allSessions.find(s => s.sessionId === sessionId);
|
|
610
|
+
|
|
611
|
+
if (!sourceSession) {
|
|
612
|
+
throw new Error('Source session not found');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const fullSession = readSessionFull(sourceSession.filePath);
|
|
616
|
+
|
|
617
|
+
if (!fullSession) {
|
|
618
|
+
throw new Error('Failed to read source session');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// 生成新的会话 ID
|
|
622
|
+
const newSessionId = crypto.randomUUID();
|
|
623
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
624
|
+
const shortId = crypto.randomBytes(4).toString('hex');
|
|
625
|
+
const newFileName = `session-${timestamp}-${shortId}.json`;
|
|
626
|
+
|
|
627
|
+
// 创建新会话
|
|
628
|
+
const newSession = {
|
|
629
|
+
...fullSession,
|
|
630
|
+
sessionId: newSessionId,
|
|
631
|
+
startTime: new Date().toISOString(),
|
|
632
|
+
lastUpdated: new Date().toISOString(),
|
|
633
|
+
forkedFrom: sessionId
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// 写入新文件
|
|
637
|
+
const chatsDir = path.join(getTmpDir(), sourceSession.projectHash, 'chats');
|
|
638
|
+
const newFilePath = path.join(chatsDir, newFileName);
|
|
639
|
+
|
|
640
|
+
try {
|
|
641
|
+
fs.writeFileSync(newFilePath, JSON.stringify(newSession, null, 2), 'utf8');
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
success: true,
|
|
645
|
+
sessionId: newSessionId,
|
|
646
|
+
filePath: newFilePath,
|
|
647
|
+
forkedFrom: sessionId
|
|
648
|
+
};
|
|
649
|
+
} catch (err) {
|
|
650
|
+
throw new Error('Failed to fork session: ' + err.message);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* 获取 Gemini 项目与会话数量(仪表盘轻量统计)
|
|
656
|
+
*/
|
|
657
|
+
function getProjectAndSessionCounts() {
|
|
658
|
+
try {
|
|
659
|
+
const projectHashes = scanProjects();
|
|
660
|
+
let sessionCount = 0;
|
|
661
|
+
projectHashes.forEach((hash) => {
|
|
662
|
+
sessionCount += scanProjectSessions(hash).length;
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
projectCount: projectHashes.length,
|
|
666
|
+
sessionCount
|
|
667
|
+
};
|
|
668
|
+
} catch (err) {
|
|
669
|
+
return { projectCount: 0, sessionCount: 0 };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
module.exports = {
|
|
674
|
+
getAllSessions,
|
|
675
|
+
getProjects,
|
|
676
|
+
getProjectSessions,
|
|
677
|
+
getSession,
|
|
678
|
+
getSessionById,
|
|
679
|
+
deleteSession,
|
|
680
|
+
deleteProject,
|
|
681
|
+
normalizeSession,
|
|
682
|
+
saveProjectOrder,
|
|
683
|
+
getRecentSessions,
|
|
684
|
+
searchSessions,
|
|
685
|
+
saveSessionOrder,
|
|
686
|
+
forkSession,
|
|
687
|
+
getProjectPath,
|
|
688
|
+
getProjectAndSessionCounts
|
|
689
|
+
};
|