coding-tool-x 3.6.7 → 3.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/assets/{Analytics-CKycBI0G.js → Analytics-C57ZRDq5.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-B3qqqL05.js} +1 -1
- package/dist/web/assets/Home-CG5XrTNc.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-BhX0LUzr.js} +1 -1
- package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-BQ01OZn9.js} +1 -1
- package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-DPv2IOXG.js} +1 -1
- package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-D5CfQm2O.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-Ce8ggteK.js} +1 -1
- package/dist/web/assets/index-CpnFNWqC.js +2 -0
- package/dist/web/assets/index-DJ9VFidd.css +1 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +40 -65
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +771 -63
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +187 -63
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-fWgvlZQx.js +0 -2
|
@@ -5,6 +5,9 @@ const { HOME_DIR } = require('../../config/paths');
|
|
|
5
5
|
const { getGeminiDir } = require('./gemini-config');
|
|
6
6
|
const { resolveModelPricing } = require('../utils/pricing');
|
|
7
7
|
|
|
8
|
+
const HASH_RE = /^[a-f0-9]{64}$/;
|
|
9
|
+
const SESSION_FILE_RE = /^session-(.*)-([a-f0-9]+)\.(json|jsonl)$/;
|
|
10
|
+
|
|
8
11
|
// 路径映射缓存
|
|
9
12
|
let pathMappingCache = null;
|
|
10
13
|
let pathMappingCacheTime = 0;
|
|
@@ -26,6 +29,266 @@ function getFilePathHash(filePath) {
|
|
|
26
29
|
return crypto.createHash('sha256').update(filePath).digest('hex');
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
function isDirectory(dirPath) {
|
|
33
|
+
try {
|
|
34
|
+
return fs.statSync(dirPath).isDirectory();
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readTextFile(filePath) {
|
|
41
|
+
try {
|
|
42
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
43
|
+
} catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readProjectRootFile(projectDir) {
|
|
49
|
+
const projectRoot = readTextFile(path.join(projectDir, '.project_root')).trim();
|
|
50
|
+
return projectRoot || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function loadProjectRootsByStorageName() {
|
|
54
|
+
const projectsPath = path.join(getGeminiDir(), 'projects.json');
|
|
55
|
+
const content = readTextFile(projectsPath);
|
|
56
|
+
if (!content) {
|
|
57
|
+
return new Map();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(content);
|
|
62
|
+
const projects = parsed && typeof parsed.projects === 'object' && parsed.projects
|
|
63
|
+
? parsed.projects
|
|
64
|
+
: {};
|
|
65
|
+
return new Map(
|
|
66
|
+
Object.entries(projects)
|
|
67
|
+
.filter(([projectRoot, storageName]) => projectRoot && typeof storageName === 'string')
|
|
68
|
+
.map(([projectRoot, storageName]) => [storageName, projectRoot])
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
return new Map();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractContentText(value) {
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(value)) {
|
|
81
|
+
return value
|
|
82
|
+
.map((item) => {
|
|
83
|
+
if (typeof item === 'string') return item;
|
|
84
|
+
if (item && typeof item.text === 'string') return item.text;
|
|
85
|
+
if (item && typeof item.content === 'string') return item.content;
|
|
86
|
+
if (item && typeof item === 'object') return JSON.stringify(item);
|
|
87
|
+
return '';
|
|
88
|
+
})
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (value && typeof value.text === 'string') {
|
|
94
|
+
return value.text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (value && typeof value === 'object') {
|
|
98
|
+
return JSON.stringify(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeMessageRecord(record) {
|
|
105
|
+
if (!record || typeof record !== 'object') {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const normalized = { ...record };
|
|
110
|
+
if (Object.prototype.hasOwnProperty.call(record, 'content')) {
|
|
111
|
+
normalized.content = extractContentText(record.content);
|
|
112
|
+
}
|
|
113
|
+
return normalized;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function upsertMessage(messages, indexById, message) {
|
|
117
|
+
if (!message) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (message.id && indexById.has(message.id)) {
|
|
122
|
+
const index = indexById.get(message.id);
|
|
123
|
+
messages[index] = {
|
|
124
|
+
...messages[index],
|
|
125
|
+
...message
|
|
126
|
+
};
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (message.id) {
|
|
131
|
+
indexById.set(message.id, messages.length);
|
|
132
|
+
}
|
|
133
|
+
messages.push(message);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeSessionObject(session) {
|
|
137
|
+
if (!session || typeof session !== 'object') {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const messages = [];
|
|
142
|
+
const indexById = new Map();
|
|
143
|
+
if (Array.isArray(session.messages)) {
|
|
144
|
+
session.messages.forEach((message) => {
|
|
145
|
+
upsertMessage(messages, indexById, normalizeMessageRecord(message));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
...session,
|
|
151
|
+
messages
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseSessionContent(content) {
|
|
156
|
+
const trimmed = content.trim();
|
|
157
|
+
if (!trimmed) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
return normalizeSessionObject(JSON.parse(trimmed));
|
|
163
|
+
} catch {
|
|
164
|
+
// Current Gemini CLI stores sessions as JSONL. Fall through to line parsing.
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const session = {};
|
|
168
|
+
const messages = [];
|
|
169
|
+
const indexById = new Map();
|
|
170
|
+
const lines = trimmed.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
171
|
+
|
|
172
|
+
lines.forEach((line) => {
|
|
173
|
+
const record = JSON.parse(line);
|
|
174
|
+
if (record.$set && typeof record.$set === 'object') {
|
|
175
|
+
Object.assign(session, record.$set);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (record.type) {
|
|
180
|
+
upsertMessage(messages, indexById, normalizeMessageRecord(record));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (record && typeof record === 'object') {
|
|
185
|
+
Object.assign(session, record);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return normalizeSessionObject({
|
|
190
|
+
...session,
|
|
191
|
+
messages
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function readSessionHeader(filePath) {
|
|
196
|
+
const content = readTextFile(filePath).trim();
|
|
197
|
+
if (!content) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(content);
|
|
203
|
+
} catch {
|
|
204
|
+
const firstLine = content.split(/\r?\n/).find(line => line.trim());
|
|
205
|
+
if (!firstLine) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(firstLine);
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function scanProjectEntries() {
|
|
217
|
+
const tmpDir = getTmpDir();
|
|
218
|
+
|
|
219
|
+
if (!isDirectory(tmpDir)) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const projectRootsByStorageName = loadProjectRootsByStorageName();
|
|
224
|
+
const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
|
|
225
|
+
|
|
226
|
+
return entries
|
|
227
|
+
.filter(entry => entry.isDirectory())
|
|
228
|
+
.map((entry) => {
|
|
229
|
+
const projectDir = path.join(tmpDir, entry.name);
|
|
230
|
+
const chatsDir = path.join(projectDir, 'chats');
|
|
231
|
+
const projectRoot = readProjectRootFile(projectDir) || projectRootsByStorageName.get(entry.name) || null;
|
|
232
|
+
const projectHash = HASH_RE.test(entry.name)
|
|
233
|
+
? entry.name
|
|
234
|
+
: (projectRoot ? getFilePathHash(projectRoot) : null);
|
|
235
|
+
|
|
236
|
+
if (!isDirectory(chatsDir) && !HASH_RE.test(entry.name)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
storageName: entry.name,
|
|
242
|
+
projectDir,
|
|
243
|
+
chatsDir,
|
|
244
|
+
projectRoot,
|
|
245
|
+
projectHash
|
|
246
|
+
};
|
|
247
|
+
})
|
|
248
|
+
.filter(Boolean);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function scanEntrySessionFiles(projectEntry) {
|
|
252
|
+
if (!projectEntry || !isDirectory(projectEntry.chatsDir)) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const entries = fs.readdirSync(projectEntry.chatsDir, { withFileTypes: true });
|
|
257
|
+
|
|
258
|
+
return entries
|
|
259
|
+
.filter(entry => entry.isFile() && SESSION_FILE_RE.test(entry.name))
|
|
260
|
+
.map(entry => {
|
|
261
|
+
const match = entry.name.match(SESSION_FILE_RE);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
filePath: path.join(projectEntry.chatsDir, entry.name),
|
|
265
|
+
timestamp: match[1],
|
|
266
|
+
shortId: match[2],
|
|
267
|
+
extension: match[3],
|
|
268
|
+
projectHash: projectEntry.projectHash,
|
|
269
|
+
storageName: projectEntry.storageName,
|
|
270
|
+
projectDir: projectEntry.projectDir,
|
|
271
|
+
projectRoot: projectEntry.projectRoot
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function resolveProjectHashForEntry(projectEntry) {
|
|
277
|
+
if (projectEntry.projectHash) {
|
|
278
|
+
return projectEntry.projectHash;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const sessionFiles = scanEntrySessionFiles(projectEntry);
|
|
282
|
+
for (const file of sessionFiles) {
|
|
283
|
+
const header = readSessionHeader(file.filePath);
|
|
284
|
+
if (header?.projectHash) {
|
|
285
|
+
return header.projectHash;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return projectEntry.storageName;
|
|
290
|
+
}
|
|
291
|
+
|
|
29
292
|
/**
|
|
30
293
|
* 扫描目录及其子目录,建立 hash → path 映射
|
|
31
294
|
* @param {string} dir - 要扫描的目录
|
|
@@ -94,15 +357,35 @@ function buildPathMapping() {
|
|
|
94
357
|
return pathMappingCache;
|
|
95
358
|
}
|
|
96
359
|
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
360
|
+
const projectEntries = scanProjectEntries();
|
|
361
|
+
if (projectEntries.length === 0) {
|
|
99
362
|
pathMappingCache = new Map();
|
|
100
363
|
pathMappingCacheTime = now;
|
|
101
364
|
return pathMappingCache;
|
|
102
365
|
}
|
|
103
366
|
|
|
104
|
-
const targetHashes = new Set(projectHashes);
|
|
105
367
|
const results = new Map();
|
|
368
|
+
const targetHashes = new Set();
|
|
369
|
+
projectEntries.forEach((entry) => {
|
|
370
|
+
const projectHash = resolveProjectHashForEntry(entry);
|
|
371
|
+
if (!projectHash) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (entry.projectRoot) {
|
|
375
|
+
results.set(projectHash, entry.projectRoot);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (HASH_RE.test(projectHash)) {
|
|
379
|
+
targetHashes.add(projectHash);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (targetHashes.size === 0) {
|
|
384
|
+
pathMappingCache = results;
|
|
385
|
+
pathMappingCacheTime = now;
|
|
386
|
+
return results;
|
|
387
|
+
}
|
|
388
|
+
|
|
106
389
|
const homeDir = HOME_DIR;
|
|
107
390
|
|
|
108
391
|
// 定义要扫描的目录及其最大深度
|
|
@@ -144,19 +427,17 @@ function buildPathMapping() {
|
|
|
144
427
|
* @returns {Array} 项目 hash 数组
|
|
145
428
|
*/
|
|
146
429
|
function scanProjects() {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
.filter(entry => /^[a-f0-9]{64}$/.test(entry.name))
|
|
159
|
-
.map(entry => entry.name); // 项目 hash
|
|
430
|
+
const seen = new Set();
|
|
431
|
+
return scanProjectEntries()
|
|
432
|
+
.map(resolveProjectHashForEntry)
|
|
433
|
+
.filter(Boolean)
|
|
434
|
+
.filter((projectHash) => {
|
|
435
|
+
if (seen.has(projectHash)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
seen.add(projectHash);
|
|
439
|
+
return true;
|
|
440
|
+
});
|
|
160
441
|
}
|
|
161
442
|
|
|
162
443
|
/**
|
|
@@ -165,32 +446,12 @@ function scanProjects() {
|
|
|
165
446
|
* @returns {Array} 会话文件路径数组
|
|
166
447
|
*/
|
|
167
448
|
function scanProjectSessions(projectHash) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const entries = fs.readdirSync(chatsDir, { withFileTypes: true });
|
|
175
|
-
|
|
176
|
-
return entries
|
|
177
|
-
.filter(entry => entry.isFile() && entry.name.match(/^session-.*\.json$/))
|
|
178
|
-
.map(entry => {
|
|
179
|
-
const filePath = path.join(chatsDir, entry.name);
|
|
180
|
-
// 文件名格式:session-2025-11-23T02-09-87570eb4.json
|
|
181
|
-
// session-{timestamp}-{shortId}.json
|
|
182
|
-
const match = entry.name.match(/^session-(.*)-([a-f0-9]+)\.json$/);
|
|
183
|
-
|
|
184
|
-
if (!match) return null;
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
filePath,
|
|
188
|
-
timestamp: match[1],
|
|
189
|
-
shortId: match[2],
|
|
190
|
-
projectHash
|
|
191
|
-
};
|
|
192
|
-
})
|
|
193
|
-
.filter(Boolean);
|
|
449
|
+
return scanProjectEntries()
|
|
450
|
+
.filter((entry) => entry.storageName === projectHash || resolveProjectHashForEntry(entry) === projectHash)
|
|
451
|
+
.flatMap((entry) => scanEntrySessionFiles({
|
|
452
|
+
...entry,
|
|
453
|
+
projectHash: resolveProjectHashForEntry(entry)
|
|
454
|
+
}));
|
|
194
455
|
}
|
|
195
456
|
|
|
196
457
|
/**
|
|
@@ -201,7 +462,10 @@ function scanProjectSessions(projectHash) {
|
|
|
201
462
|
function readSessionMeta(filePath) {
|
|
202
463
|
try {
|
|
203
464
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
204
|
-
const session =
|
|
465
|
+
const session = parseSessionContent(content);
|
|
466
|
+
if (!session) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
205
469
|
|
|
206
470
|
// Gemini 会话文件结构
|
|
207
471
|
// {
|
|
@@ -212,7 +476,7 @@ function readSessionMeta(filePath) {
|
|
|
212
476
|
// messages: [...]
|
|
213
477
|
// }
|
|
214
478
|
|
|
215
|
-
const messages = session.messages
|
|
479
|
+
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
216
480
|
const firstUserMessage = messages.find(msg => msg.type === 'user');
|
|
217
481
|
|
|
218
482
|
// 计算总 tokens(从所有消息中累加)
|
|
@@ -243,7 +507,7 @@ function readSessionMeta(filePath) {
|
|
|
243
507
|
startTime: session.startTime,
|
|
244
508
|
lastUpdated: session.lastUpdated,
|
|
245
509
|
messageCount: messages.length,
|
|
246
|
-
firstMessage: firstUserMessage ? firstUserMessage.content : '',
|
|
510
|
+
firstMessage: firstUserMessage ? extractContentText(firstUserMessage.content) : '',
|
|
247
511
|
tokens: totalTokens,
|
|
248
512
|
cost: totalCost,
|
|
249
513
|
model: model || 'gemini-2.5-pro',
|
|
@@ -263,7 +527,7 @@ function readSessionMeta(filePath) {
|
|
|
263
527
|
function readSessionFull(filePath) {
|
|
264
528
|
try {
|
|
265
529
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
266
|
-
return
|
|
530
|
+
return parseSessionContent(content);
|
|
267
531
|
} catch (err) {
|
|
268
532
|
console.error(`[Gemini Sessions] Failed to read session: ${filePath}`, err);
|
|
269
533
|
return null;
|
|
@@ -275,11 +539,15 @@ function readSessionFull(filePath) {
|
|
|
275
539
|
* @returns {Array} 会话对象数组
|
|
276
540
|
*/
|
|
277
541
|
function getAllSessions() {
|
|
278
|
-
const
|
|
542
|
+
const projectEntries = scanProjectEntries();
|
|
279
543
|
const allSessions = [];
|
|
280
544
|
|
|
281
|
-
|
|
282
|
-
const
|
|
545
|
+
projectEntries.forEach(projectEntry => {
|
|
546
|
+
const resolvedProjectHash = resolveProjectHashForEntry(projectEntry);
|
|
547
|
+
const sessionFiles = scanEntrySessionFiles({
|
|
548
|
+
...projectEntry,
|
|
549
|
+
projectHash: resolvedProjectHash
|
|
550
|
+
});
|
|
283
551
|
|
|
284
552
|
sessionFiles.forEach(file => {
|
|
285
553
|
const meta = readSessionMeta(file.filePath);
|
|
@@ -299,6 +567,10 @@ function getAllSessions() {
|
|
|
299
567
|
|
|
300
568
|
allSessions.push({
|
|
301
569
|
...meta,
|
|
570
|
+
projectHash: meta.projectHash || resolvedProjectHash,
|
|
571
|
+
storageName: projectEntry.storageName,
|
|
572
|
+
projectDir: projectEntry.projectDir,
|
|
573
|
+
projectRoot: projectEntry.projectRoot,
|
|
302
574
|
filePath: file.filePath,
|
|
303
575
|
size,
|
|
304
576
|
mtime,
|
|
@@ -332,7 +604,9 @@ function normalizeSession(geminiSession) {
|
|
|
332
604
|
cost: geminiSession.cost,
|
|
333
605
|
model: geminiSession.model,
|
|
334
606
|
projectHash: geminiSession.projectHash,
|
|
335
|
-
projectName: geminiSession.projectHash // 兼容前端统一使用 projectName
|
|
607
|
+
projectName: geminiSession.projectHash, // 兼容前端统一使用 projectName
|
|
608
|
+
storageName: geminiSession.storageName,
|
|
609
|
+
projectRoot: geminiSession.projectRoot
|
|
336
610
|
};
|
|
337
611
|
}
|
|
338
612
|
|
|
@@ -358,7 +632,7 @@ function getProjects() {
|
|
|
358
632
|
const projectHash = session.projectHash;
|
|
359
633
|
|
|
360
634
|
if (!projectMap.has(projectHash)) {
|
|
361
|
-
const projectPath = getProjectPath(projectHash);
|
|
635
|
+
const projectPath = session.projectRoot || getProjectPath(projectHash);
|
|
362
636
|
|
|
363
637
|
// 如果找到了真实路径,使用目录名作为显示名称
|
|
364
638
|
let displayName;
|
|
@@ -377,6 +651,8 @@ function getProjects() {
|
|
|
377
651
|
name: projectHash,
|
|
378
652
|
displayName,
|
|
379
653
|
path: projectPath,
|
|
654
|
+
fullPath: projectPath,
|
|
655
|
+
storageName: session.storageName,
|
|
380
656
|
sessionCount: 0,
|
|
381
657
|
lastUpdated: session.lastUpdated,
|
|
382
658
|
source: 'gemini'
|
|
@@ -454,7 +730,9 @@ function deleteSession(sessionId) {
|
|
|
454
730
|
* @returns {Object} 删除结果
|
|
455
731
|
*/
|
|
456
732
|
function deleteProject(projectHash) {
|
|
457
|
-
const
|
|
733
|
+
const projectEntry = scanProjectEntries()
|
|
734
|
+
.find(entry => entry.storageName === projectHash || resolveProjectHashForEntry(entry) === projectHash);
|
|
735
|
+
const projectDir = projectEntry ? projectEntry.projectDir : path.join(getTmpDir(), projectHash);
|
|
458
736
|
|
|
459
737
|
if (!fs.existsSync(projectDir)) {
|
|
460
738
|
throw new Error('Project not found');
|
|
@@ -542,8 +820,8 @@ function searchSessions(keyword, contextLength = 35) {
|
|
|
542
820
|
let content = '';
|
|
543
821
|
|
|
544
822
|
// 提取消息内容
|
|
545
|
-
if (msg.type === 'user' || msg.type === 'assistant') {
|
|
546
|
-
content = msg.content
|
|
823
|
+
if (msg.type === 'user' || msg.type === 'assistant' || msg.type === 'gemini') {
|
|
824
|
+
content = extractContentText(msg.content);
|
|
547
825
|
}
|
|
548
826
|
|
|
549
827
|
// 搜索关键词
|
|
@@ -654,7 +932,8 @@ function forkSession(sessionId, options = {}) {
|
|
|
654
932
|
const newSessionId = crypto.randomUUID();
|
|
655
933
|
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
656
934
|
const shortId = crypto.randomBytes(4).toString('hex');
|
|
657
|
-
const
|
|
935
|
+
const newExtension = path.extname(sourceSession.filePath) === '.jsonl' ? 'jsonl' : 'json';
|
|
936
|
+
const newFileName = `session-${timestamp}-${shortId}.${newExtension}`;
|
|
658
937
|
|
|
659
938
|
// 创建新会话
|
|
660
939
|
const newSession = {
|
|
@@ -667,11 +946,16 @@ function forkSession(sessionId, options = {}) {
|
|
|
667
946
|
};
|
|
668
947
|
|
|
669
948
|
// 写入新文件
|
|
670
|
-
const chatsDir = path.
|
|
949
|
+
const chatsDir = path.dirname(sourceSession.filePath);
|
|
671
950
|
const newFilePath = path.join(chatsDir, newFileName);
|
|
672
951
|
|
|
673
952
|
try {
|
|
674
|
-
|
|
953
|
+
if (newExtension === 'jsonl') {
|
|
954
|
+
const { messages, ...sessionHeader } = newSession;
|
|
955
|
+
fs.writeFileSync(newFilePath, `${JSON.stringify(sessionHeader)}\n${truncatedMessages.map(message => JSON.stringify(message)).join('\n')}\n`, 'utf8');
|
|
956
|
+
} else {
|
|
957
|
+
fs.writeFileSync(newFilePath, JSON.stringify(newSession, null, 2), 'utf8');
|
|
958
|
+
}
|
|
675
959
|
if (options.alias) {
|
|
676
960
|
const { setAlias } = require('./alias');
|
|
677
961
|
setAlias(newSessionId, options.alias);
|
|
@@ -695,13 +979,15 @@ function forkSession(sessionId, options = {}) {
|
|
|
695
979
|
*/
|
|
696
980
|
function getProjectAndSessionCounts() {
|
|
697
981
|
try {
|
|
698
|
-
const
|
|
982
|
+
const projectEntries = scanProjectEntries();
|
|
699
983
|
let sessionCount = 0;
|
|
700
|
-
projectHashes
|
|
701
|
-
|
|
984
|
+
const projectHashes = new Set();
|
|
985
|
+
projectEntries.forEach((entry) => {
|
|
986
|
+
projectHashes.add(resolveProjectHashForEntry(entry));
|
|
987
|
+
sessionCount += scanEntrySessionFiles(entry).length;
|
|
702
988
|
});
|
|
703
989
|
return {
|
|
704
|
-
projectCount: projectHashes.length,
|
|
990
|
+
projectCount: Array.from(projectHashes).filter(Boolean).length,
|
|
705
991
|
sessionCount
|
|
706
992
|
};
|
|
707
993
|
} catch (err) {
|