@adversity/coding-tool-x 3.0.6 → 3.1.1
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 +38 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.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-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +92 -13
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/ui.js +8 -1
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +67 -4
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- 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 +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +32 -19
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +17 -3
- package/src/server/index.js +164 -48
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +30 -19
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +70 -12
- package/src/server/services/codex-channels.js +61 -23
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +37 -15
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-BxudHPiX.js +0 -1
- package/dist/web/assets/index-D2VfwJBa.js +0 -14
- package/dist/web/assets/index-oXBzu0bd.css +0 -41
- package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/permissions.js +0 -385
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OpenCode 会话服务
|
|
8
|
+
* 读取 OpenCode CLI 的原生会话数据
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const PROJECT_ORDER_FILE = path.join(PATHS.base, 'opencode-project-order.json');
|
|
12
|
+
const SESSION_ORDER_FILE = path.join(PATHS.base, 'opencode-session-order.json');
|
|
13
|
+
|
|
14
|
+
function ensureParentDir(filePath) {
|
|
15
|
+
const dir = path.dirname(filePath);
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readJsonSafe(filePath, fallback) {
|
|
22
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeJsonSafe(filePath, data) {
|
|
33
|
+
ensureParentDir(filePath);
|
|
34
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
38
|
+
if (!fs.existsSync(sourceDir)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(targetDir)) {
|
|
43
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
49
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
copyDirectoryRecursive(sourcePath, targetPath);
|
|
52
|
+
} else {
|
|
53
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function sortByOrder(items, order, fallbackCompare) {
|
|
59
|
+
const fallbackSorted = [...items].sort(fallbackCompare);
|
|
60
|
+
if (!Array.isArray(order) || order.length === 0) {
|
|
61
|
+
return fallbackSorted;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const orderMap = new Map(order.map((name, idx) => [name, idx]));
|
|
65
|
+
return fallbackSorted.sort((a, b) => {
|
|
66
|
+
const aIndex = orderMap.has(a.name) ? orderMap.get(a.name) : Number.MAX_SAFE_INTEGER;
|
|
67
|
+
const bIndex = orderMap.has(b.name) ? orderMap.get(b.name) : Number.MAX_SAFE_INTEGER;
|
|
68
|
+
if (aIndex === bIndex) {
|
|
69
|
+
return fallbackCompare(a, b);
|
|
70
|
+
}
|
|
71
|
+
return aIndex - bIndex;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function extractTextContent(content) {
|
|
76
|
+
if (typeof content === 'string') {
|
|
77
|
+
return content;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(content)) {
|
|
80
|
+
return content
|
|
81
|
+
.filter(item => item && item.type === 'text' && typeof item.text === 'string')
|
|
82
|
+
.map(item => item.text)
|
|
83
|
+
.join('\n')
|
|
84
|
+
.trim();
|
|
85
|
+
}
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildContext(text, keyword, contextLength = 35) {
|
|
90
|
+
if (!text || !keyword) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsedContextLength = Number(contextLength);
|
|
95
|
+
const safeContextLength = Number.isFinite(parsedContextLength) && parsedContextLength >= 0
|
|
96
|
+
? parsedContextLength
|
|
97
|
+
: 35;
|
|
98
|
+
|
|
99
|
+
const lowerText = text.toLowerCase();
|
|
100
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
101
|
+
const index = lowerText.indexOf(lowerKeyword);
|
|
102
|
+
if (index === -1) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const start = Math.max(0, index - safeContextLength);
|
|
107
|
+
const end = Math.min(text.length, index + keyword.length + safeContextLength);
|
|
108
|
+
let context = text.slice(start, end);
|
|
109
|
+
if (start > 0) context = `...${context}`;
|
|
110
|
+
if (end < text.length) context = `${context}...`;
|
|
111
|
+
return context;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 检查 OpenCode 是否安装
|
|
115
|
+
function isOpenCodeInstalled() {
|
|
116
|
+
return fs.existsSync(NATIVE_PATHS.opencode.data);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 获取 OpenCode 数据目录
|
|
120
|
+
function getOpenCodeDataDir() {
|
|
121
|
+
return NATIVE_PATHS.opencode.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 获取会话存储目录
|
|
125
|
+
function getSessionsDir() {
|
|
126
|
+
return path.join(getOpenCodeDataDir(), 'storage', 'session');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 获取项目存储目录
|
|
130
|
+
function getProjectsDir() {
|
|
131
|
+
return path.join(getOpenCodeDataDir(), 'storage', 'project');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 获取消息存储目录
|
|
135
|
+
function getMessagesRootDir() {
|
|
136
|
+
return NATIVE_PATHS.opencode.messages;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getMessageDir(sessionId) {
|
|
140
|
+
return path.join(getMessagesRootDir(), sessionId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getProjectOrder() {
|
|
144
|
+
const order = readJsonSafe(PROJECT_ORDER_FILE, []);
|
|
145
|
+
return Array.isArray(order) ? order : [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function saveProjectOrder(order) {
|
|
149
|
+
if (!Array.isArray(order)) {
|
|
150
|
+
throw new Error('order must be an array');
|
|
151
|
+
}
|
|
152
|
+
writeJsonSafe(PROJECT_ORDER_FILE, order);
|
|
153
|
+
return { success: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getSessionOrderMap() {
|
|
157
|
+
const map = readJsonSafe(SESSION_ORDER_FILE, {});
|
|
158
|
+
return map && typeof map === 'object' ? map : {};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function saveSessionOrderMap(map) {
|
|
162
|
+
writeJsonSafe(SESSION_ORDER_FILE, map);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getSessionOrder(projectId) {
|
|
166
|
+
const map = getSessionOrderMap();
|
|
167
|
+
const order = map[projectId];
|
|
168
|
+
return Array.isArray(order) ? order : [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function saveSessionOrder(projectId, order) {
|
|
172
|
+
if (!projectId) {
|
|
173
|
+
throw new Error('projectId is required');
|
|
174
|
+
}
|
|
175
|
+
if (!Array.isArray(order)) {
|
|
176
|
+
throw new Error('order must be an array');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const map = getSessionOrderMap();
|
|
180
|
+
map[projectId] = order;
|
|
181
|
+
saveSessionOrderMap(map);
|
|
182
|
+
return { success: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function removeSessionFromOrder(projectId, sessionId) {
|
|
186
|
+
const map = getSessionOrderMap();
|
|
187
|
+
if (!Array.isArray(map[projectId])) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
map[projectId] = map[projectId].filter(id => id !== sessionId);
|
|
191
|
+
saveSessionOrderMap(map);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function removeProjectFromOrder(projectId) {
|
|
195
|
+
const currentOrder = getProjectOrder().filter(name => name !== projectId);
|
|
196
|
+
saveProjectOrder(currentOrder);
|
|
197
|
+
|
|
198
|
+
const map = getSessionOrderMap();
|
|
199
|
+
if (map[projectId] !== undefined) {
|
|
200
|
+
delete map[projectId];
|
|
201
|
+
saveSessionOrderMap(map);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function getProjectEntries() {
|
|
206
|
+
const projectsDir = getProjectsDir();
|
|
207
|
+
if (!fs.existsSync(projectsDir)) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const files = fs.readdirSync(projectsDir).filter(file => file.endsWith('.json'));
|
|
212
|
+
const entries = [];
|
|
213
|
+
for (const file of files) {
|
|
214
|
+
const filePath = path.join(projectsDir, file);
|
|
215
|
+
const data = readJsonSafe(filePath, null);
|
|
216
|
+
if (!data || !data.id) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
entries.push({ filePath, data });
|
|
220
|
+
}
|
|
221
|
+
return entries;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 获取所有项目
|
|
225
|
+
function getProjects() {
|
|
226
|
+
const projects = [];
|
|
227
|
+
const entries = getProjectEntries();
|
|
228
|
+
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
const project = entry.data;
|
|
231
|
+
const projectSessions = getSessionsByProjectId(project.id);
|
|
232
|
+
projects.push({
|
|
233
|
+
name: project.id,
|
|
234
|
+
displayName: project.id,
|
|
235
|
+
fullPath: project.worktree || '/',
|
|
236
|
+
path: project.worktree || '/',
|
|
237
|
+
sessionCount: projectSessions.length,
|
|
238
|
+
lastUsed: project.time?.updated || project.time?.created || 0,
|
|
239
|
+
source: 'opencode'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return sortByOrder(
|
|
244
|
+
projects,
|
|
245
|
+
getProjectOrder(),
|
|
246
|
+
(a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 根据项目ID获取会话列表
|
|
251
|
+
function getSessionsByProjectId(projectId) {
|
|
252
|
+
const sessionsDir = path.join(getSessionsDir(), projectId);
|
|
253
|
+
const sessions = [];
|
|
254
|
+
|
|
255
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
256
|
+
return sessions;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const files = fs.readdirSync(sessionsDir).filter(file => file.endsWith('.json'));
|
|
260
|
+
for (const file of files) {
|
|
261
|
+
const filePath = path.join(sessionsDir, file);
|
|
262
|
+
try {
|
|
263
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
264
|
+
const session = JSON.parse(content);
|
|
265
|
+
sessions.push(normalizeSession(session, filePath, projectId));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
console.error(`[OpenCode Sessions] Failed to parse session file ${file}:`, err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const fallbackSorted = sessions.sort(
|
|
272
|
+
(a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime()
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const order = getSessionOrder(projectId);
|
|
276
|
+
if (order.length === 0) {
|
|
277
|
+
return fallbackSorted;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const orderMap = new Map(order.map((id, idx) => [id, idx]));
|
|
281
|
+
return [...fallbackSorted].sort((a, b) => {
|
|
282
|
+
const aIndex = orderMap.has(a.sessionId) ? orderMap.get(a.sessionId) : Number.MAX_SAFE_INTEGER;
|
|
283
|
+
const bIndex = orderMap.has(b.sessionId) ? orderMap.get(b.sessionId) : Number.MAX_SAFE_INTEGER;
|
|
284
|
+
if (aIndex === bIndex) {
|
|
285
|
+
return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
|
|
286
|
+
}
|
|
287
|
+
return aIndex - bIndex;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 归一化会话格式(与 Claude Code 格式一致)
|
|
292
|
+
function normalizeSession(session, filePath, projectId = null) {
|
|
293
|
+
const mtime = session.time?.updated
|
|
294
|
+
? new Date(session.time.updated).toISOString()
|
|
295
|
+
: new Date().toISOString();
|
|
296
|
+
|
|
297
|
+
let size = 0;
|
|
298
|
+
try {
|
|
299
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
300
|
+
const stats = fs.statSync(filePath);
|
|
301
|
+
size = stats.size;
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
// 忽略错误
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
sessionId: session.id,
|
|
309
|
+
projectName: projectId,
|
|
310
|
+
mtime,
|
|
311
|
+
size,
|
|
312
|
+
filePath: filePath || '',
|
|
313
|
+
gitBranch: null,
|
|
314
|
+
firstMessage: session.title || session.slug || null,
|
|
315
|
+
forkedFrom: null,
|
|
316
|
+
directory: session.directory,
|
|
317
|
+
slug: session.slug,
|
|
318
|
+
source: 'opencode'
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 根据项目名获取会话列表
|
|
323
|
+
function getSessionsByProject(projectName) {
|
|
324
|
+
return getSessionsByProjectId(projectName);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getSessionLocation(sessionId) {
|
|
328
|
+
const sessionsRoot = getSessionsDir();
|
|
329
|
+
if (!fs.existsSync(sessionsRoot)) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const projectDirs = fs.readdirSync(sessionsRoot, { withFileTypes: true });
|
|
334
|
+
for (const projectDir of projectDirs) {
|
|
335
|
+
if (!projectDir.isDirectory()) continue;
|
|
336
|
+
|
|
337
|
+
const projectPath = path.join(sessionsRoot, projectDir.name);
|
|
338
|
+
const directPath = path.join(projectPath, `${sessionId}.json`);
|
|
339
|
+
if (fs.existsSync(directPath)) {
|
|
340
|
+
const sessionData = readJsonSafe(directPath, null);
|
|
341
|
+
if (sessionData && sessionData.id === sessionId) {
|
|
342
|
+
return { projectId: projectDir.name, sessionPath: directPath, sessionData };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const files = fs.readdirSync(projectPath).filter(file => file.endsWith('.json'));
|
|
347
|
+
for (const file of files) {
|
|
348
|
+
const sessionPath = path.join(projectPath, file);
|
|
349
|
+
const sessionData = readJsonSafe(sessionPath, null);
|
|
350
|
+
if (sessionData && sessionData.id === sessionId) {
|
|
351
|
+
return { projectId: projectDir.name, sessionPath, sessionData };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 根据会话ID获取会话详情
|
|
360
|
+
function getSessionById(sessionId) {
|
|
361
|
+
const location = getSessionLocation(sessionId);
|
|
362
|
+
if (!location) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return normalizeSession(location.sessionData, location.sessionPath, location.projectId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 获取项目和会话数量统计
|
|
370
|
+
function getProjectAndSessionCounts() {
|
|
371
|
+
try {
|
|
372
|
+
const projects = getProjects();
|
|
373
|
+
let sessionCount = 0;
|
|
374
|
+
|
|
375
|
+
for (const project of projects) {
|
|
376
|
+
sessionCount += project.sessionCount || 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
projectCount: projects.length,
|
|
381
|
+
sessionCount
|
|
382
|
+
};
|
|
383
|
+
} catch (err) {
|
|
384
|
+
console.error('[OpenCode Sessions] Failed to get counts:', err);
|
|
385
|
+
return { projectCount: 0, sessionCount: 0 };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function getRecentSessions(limit = 5) {
|
|
390
|
+
const projects = getProjects();
|
|
391
|
+
const { loadAliases } = require('./alias');
|
|
392
|
+
const { getForkRelations } = require('./sessions');
|
|
393
|
+
const aliases = loadAliases();
|
|
394
|
+
const forkRelations = getForkRelations();
|
|
395
|
+
const allSessions = [];
|
|
396
|
+
|
|
397
|
+
for (const project of projects) {
|
|
398
|
+
const sessions = getSessionsByProjectId(project.name);
|
|
399
|
+
sessions.forEach(session => {
|
|
400
|
+
allSessions.push({
|
|
401
|
+
...session,
|
|
402
|
+
alias: aliases[session.sessionId] || null,
|
|
403
|
+
forkedFrom: forkRelations[session.sessionId] || null,
|
|
404
|
+
projectName: project.name,
|
|
405
|
+
projectDisplayName: project.displayName,
|
|
406
|
+
projectFullPath: project.fullPath
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return allSessions
|
|
412
|
+
.sort((a, b) => new Date(b.mtime).getTime() - new Date(a.mtime).getTime())
|
|
413
|
+
.slice(0, limit);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 删除会话
|
|
417
|
+
function deleteSession(sessionId) {
|
|
418
|
+
const location = getSessionLocation(sessionId);
|
|
419
|
+
if (!location) {
|
|
420
|
+
throw new Error('Session not found');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fs.unlinkSync(location.sessionPath);
|
|
424
|
+
|
|
425
|
+
const messageDir = getMessageDir(sessionId);
|
|
426
|
+
if (fs.existsSync(messageDir)) {
|
|
427
|
+
fs.rmSync(messageDir, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const { deleteAlias } = require('./alias');
|
|
432
|
+
deleteAlias(sessionId);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
// ignore alias cleanup errors
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
removeSessionFromOrder(location.projectId, sessionId);
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const { getForkRelations, saveForkRelations } = require('./sessions');
|
|
441
|
+
const relations = getForkRelations();
|
|
442
|
+
delete relations[sessionId];
|
|
443
|
+
Object.keys(relations).forEach((key) => {
|
|
444
|
+
if (relations[key] === sessionId) {
|
|
445
|
+
delete relations[key];
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
saveForkRelations(relations);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
// ignore fork relation cleanup errors
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { success: true, projectName: location.projectId, sessionId };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function forkSession(sessionId) {
|
|
457
|
+
const location = getSessionLocation(sessionId);
|
|
458
|
+
if (!location) {
|
|
459
|
+
throw new Error('Session not found');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const now = new Date().toISOString();
|
|
463
|
+
const newSessionId = crypto.randomUUID();
|
|
464
|
+
const source = location.sessionData;
|
|
465
|
+
const nextSession = {
|
|
466
|
+
...source,
|
|
467
|
+
id: newSessionId,
|
|
468
|
+
time: {
|
|
469
|
+
...(source.time || {}),
|
|
470
|
+
created: now,
|
|
471
|
+
updated: now
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const targetPath = path.join(path.dirname(location.sessionPath), `${newSessionId}.json`);
|
|
476
|
+
fs.writeFileSync(targetPath, JSON.stringify(nextSession, null, 2), 'utf8');
|
|
477
|
+
|
|
478
|
+
const sourceMessageDir = getMessageDir(sessionId);
|
|
479
|
+
const targetMessageDir = getMessageDir(newSessionId);
|
|
480
|
+
if (fs.existsSync(sourceMessageDir)) {
|
|
481
|
+
copyDirectoryRecursive(sourceMessageDir, targetMessageDir);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const { getForkRelations, saveForkRelations } = require('./sessions');
|
|
486
|
+
const relations = getForkRelations();
|
|
487
|
+
relations[newSessionId] = sessionId;
|
|
488
|
+
saveForkRelations(relations);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
// ignore fork relation save errors
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const existingOrder = getSessionOrder(location.projectId);
|
|
494
|
+
saveSessionOrder(location.projectId, [newSessionId, ...existingOrder.filter(id => id !== newSessionId)]);
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
newSessionId,
|
|
499
|
+
forkedFrom: sessionId,
|
|
500
|
+
projectName: location.projectId,
|
|
501
|
+
newFilePath: targetPath
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function deleteProject(projectId) {
|
|
506
|
+
const projectSessionDir = path.join(getSessionsDir(), projectId);
|
|
507
|
+
if (!fs.existsSync(projectSessionDir)) {
|
|
508
|
+
throw new Error('Project not found');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const sessionFiles = fs.readdirSync(projectSessionDir).filter(file => file.endsWith('.json'));
|
|
512
|
+
const deletedSessionIds = [];
|
|
513
|
+
|
|
514
|
+
for (const file of sessionFiles) {
|
|
515
|
+
const sessionPath = path.join(projectSessionDir, file);
|
|
516
|
+
const session = readJsonSafe(sessionPath, null);
|
|
517
|
+
const sessionId = session?.id || path.basename(file, '.json');
|
|
518
|
+
deletedSessionIds.push(sessionId);
|
|
519
|
+
|
|
520
|
+
const messageDir = getMessageDir(sessionId);
|
|
521
|
+
if (fs.existsSync(messageDir)) {
|
|
522
|
+
fs.rmSync(messageDir, { recursive: true, force: true });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const { deleteAlias } = require('./alias');
|
|
527
|
+
deleteAlias(sessionId);
|
|
528
|
+
} catch (err) {
|
|
529
|
+
// ignore alias cleanup errors
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fs.rmSync(projectSessionDir, { recursive: true, force: true });
|
|
534
|
+
|
|
535
|
+
const projectEntries = getProjectEntries();
|
|
536
|
+
for (const entry of projectEntries) {
|
|
537
|
+
if (entry.data.id === projectId) {
|
|
538
|
+
fs.rmSync(entry.filePath, { force: true });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
removeProjectFromOrder(projectId);
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const { getForkRelations, saveForkRelations } = require('./sessions');
|
|
546
|
+
const deletedSet = new Set(deletedSessionIds);
|
|
547
|
+
const relations = getForkRelations();
|
|
548
|
+
Object.keys(relations).forEach((key) => {
|
|
549
|
+
if (deletedSet.has(key) || deletedSet.has(relations[key])) {
|
|
550
|
+
delete relations[key];
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
saveForkRelations(relations);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
// ignore relation cleanup errors
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
success: true,
|
|
560
|
+
projectName: projectId,
|
|
561
|
+
deletedCount: deletedSessionIds.length
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 搜索会话
|
|
566
|
+
function searchSessions(keyword, contextLength = 35, projectFilter = null) {
|
|
567
|
+
if (!keyword || !String(keyword).trim()) {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const searchKeyword = String(keyword).trim();
|
|
572
|
+
const projects = getProjects();
|
|
573
|
+
const { loadAliases } = require('./alias');
|
|
574
|
+
const aliases = loadAliases();
|
|
575
|
+
const results = [];
|
|
576
|
+
|
|
577
|
+
for (const project of projects) {
|
|
578
|
+
if (projectFilter && project.name !== projectFilter) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const sessions = getSessionsByProjectId(project.name);
|
|
583
|
+
for (const session of sessions) {
|
|
584
|
+
const matches = [];
|
|
585
|
+
|
|
586
|
+
const quickChecks = [
|
|
587
|
+
session.sessionId,
|
|
588
|
+
session.firstMessage,
|
|
589
|
+
session.slug,
|
|
590
|
+
session.directory
|
|
591
|
+
];
|
|
592
|
+
for (const text of quickChecks) {
|
|
593
|
+
const context = buildContext(text, searchKeyword, contextLength);
|
|
594
|
+
if (context) {
|
|
595
|
+
matches.push({
|
|
596
|
+
role: 'assistant',
|
|
597
|
+
context,
|
|
598
|
+
timestamp: session.mtime
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const messageDir = getMessageDir(session.sessionId);
|
|
604
|
+
if (fs.existsSync(messageDir)) {
|
|
605
|
+
const messageFiles = fs.readdirSync(messageDir)
|
|
606
|
+
.filter(file => file.endsWith('.json'))
|
|
607
|
+
.sort();
|
|
608
|
+
for (const messageFile of messageFiles) {
|
|
609
|
+
const messagePath = path.join(messageDir, messageFile);
|
|
610
|
+
const message = readJsonSafe(messagePath, null);
|
|
611
|
+
if (!message) continue;
|
|
612
|
+
|
|
613
|
+
const text = extractTextContent(message.content);
|
|
614
|
+
const context = buildContext(text, searchKeyword, contextLength);
|
|
615
|
+
if (!context) continue;
|
|
616
|
+
|
|
617
|
+
matches.push({
|
|
618
|
+
role: message.role === 'user' ? 'user' : 'assistant',
|
|
619
|
+
context,
|
|
620
|
+
timestamp: message.time?.created || null
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (matches.length > 0) {
|
|
626
|
+
results.push({
|
|
627
|
+
sessionId: session.sessionId,
|
|
628
|
+
projectName: project.name,
|
|
629
|
+
projectDisplayName: project.displayName,
|
|
630
|
+
projectFullPath: project.fullPath,
|
|
631
|
+
alias: aliases[session.sessionId] || null,
|
|
632
|
+
matchCount: matches.length,
|
|
633
|
+
matches: matches.slice(0, 5),
|
|
634
|
+
source: 'opencode'
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return results.sort((a, b) => b.matchCount - a.matchCount);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
module.exports = {
|
|
644
|
+
isOpenCodeInstalled,
|
|
645
|
+
getOpenCodeDataDir,
|
|
646
|
+
getSessionsDir,
|
|
647
|
+
getProjectsDir,
|
|
648
|
+
getProjects,
|
|
649
|
+
getProjectOrder,
|
|
650
|
+
saveProjectOrder,
|
|
651
|
+
getSessionsByProject,
|
|
652
|
+
getSessionsByProjectId,
|
|
653
|
+
getSessionById,
|
|
654
|
+
getRecentSessions,
|
|
655
|
+
normalizeSession,
|
|
656
|
+
getProjectAndSessionCounts,
|
|
657
|
+
deleteSession,
|
|
658
|
+
deleteProject,
|
|
659
|
+
forkSession,
|
|
660
|
+
getSessionOrder,
|
|
661
|
+
saveSessionOrder,
|
|
662
|
+
searchSessions
|
|
663
|
+
};
|