@adversity/coding-tool-x 3.1.0 → 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 +15 -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 +81 -12
- 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/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 +64 -3
- 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 +30 -18
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +15 -3
- package/src/server/index.js +165 -58
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +27 -18
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- 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 +33 -44
- 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-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
startOpenCodeProxyServer,
|
|
5
|
+
stopOpenCodeProxyServer,
|
|
6
|
+
getOpenCodeProxyStatus,
|
|
7
|
+
collectProxyModelList
|
|
8
|
+
} = require('../opencode-proxy-server');
|
|
9
|
+
const {
|
|
10
|
+
configExists,
|
|
11
|
+
hasBackup,
|
|
12
|
+
setProxyConfig,
|
|
13
|
+
restoreSettings,
|
|
14
|
+
isProxyConfig,
|
|
15
|
+
getCurrentProxyPort
|
|
16
|
+
} = require('../services/opencode-settings-manager');
|
|
17
|
+
const { getChannels, getEnabledChannels } = require('../services/opencode-channels');
|
|
18
|
+
const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
function pushUniqueModel(allModels, seen, modelId) {
|
|
23
|
+
if (typeof modelId !== 'string') return;
|
|
24
|
+
const trimmed = modelId.trim();
|
|
25
|
+
if (!trimmed) return;
|
|
26
|
+
const key = trimmed.toLowerCase();
|
|
27
|
+
if (seen.has(key)) return;
|
|
28
|
+
seen.add(key);
|
|
29
|
+
allModels.push(trimmed);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizeChannel(channel) {
|
|
33
|
+
if (!channel) return null;
|
|
34
|
+
return {
|
|
35
|
+
id: channel.id,
|
|
36
|
+
name: channel.name,
|
|
37
|
+
baseUrl: channel.baseUrl,
|
|
38
|
+
websiteUrl: channel.websiteUrl
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 保存激活渠道ID
|
|
43
|
+
function saveActiveChannelId(channelId) {
|
|
44
|
+
ensureStorageDirMigrated();
|
|
45
|
+
const filePath = PATHS.activeChannel.opencode;
|
|
46
|
+
const dir = path.dirname(filePath);
|
|
47
|
+
if (!fs.existsSync(dir)) {
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 删除激活渠道文件
|
|
54
|
+
function removeActiveChannelFile() {
|
|
55
|
+
ensureStorageDirMigrated();
|
|
56
|
+
const filePath = PATHS.activeChannel.opencode;
|
|
57
|
+
if (fs.existsSync(filePath)) {
|
|
58
|
+
fs.unlinkSync(filePath);
|
|
59
|
+
console.log('[OpenCode Proxy] Removed opencode-active-channel.json');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 获取代理状态
|
|
64
|
+
router.get('/status', (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const proxyStatus = getOpenCodeProxyStatus();
|
|
67
|
+
const { channels } = getChannels();
|
|
68
|
+
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
69
|
+
const activeChannel = enabledChannels[0];
|
|
70
|
+
const configStatus = {
|
|
71
|
+
isProxyConfig: isProxyConfig(),
|
|
72
|
+
configExists: configExists(),
|
|
73
|
+
hasBackup: hasBackup(),
|
|
74
|
+
currentProxyPort: getCurrentProxyPort()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
res.json({
|
|
78
|
+
proxy: proxyStatus,
|
|
79
|
+
config: configStatus,
|
|
80
|
+
activeChannel: sanitizeChannel(activeChannel),
|
|
81
|
+
enabledChannelsCount: enabledChannels.length,
|
|
82
|
+
totalChannelsCount: channels.length
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
res.status(500).json({ error: error.message });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 启动代理
|
|
90
|
+
router.post('/start', async (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
// 1. 获取当前启用的渠道
|
|
93
|
+
const enabledChannels = getEnabledChannels();
|
|
94
|
+
const currentChannel = enabledChannels[0];
|
|
95
|
+
|
|
96
|
+
if (!currentChannel) {
|
|
97
|
+
return res.status(400).json({
|
|
98
|
+
error: 'No enabled OpenCode channel found. Please create and enable a channel first.'
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. 保存当前激活渠道ID
|
|
103
|
+
saveActiveChannelId(currentChannel.id);
|
|
104
|
+
console.log(`[OpenCode Proxy] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
|
|
105
|
+
|
|
106
|
+
// 3. 启动代理服务器
|
|
107
|
+
const proxyResult = await startOpenCodeProxyServer();
|
|
108
|
+
|
|
109
|
+
if (!proxyResult.success) {
|
|
110
|
+
return res.status(500).json({ error: 'Failed to start OpenCode proxy server' });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. 设置代理配置(写入 OpenCode 配置文件)
|
|
114
|
+
// 收集所有启用渠道配置的模型
|
|
115
|
+
const allModels = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
enabledChannels.forEach((ch) => {
|
|
118
|
+
const candidates = [
|
|
119
|
+
ch.model,
|
|
120
|
+
ch.speedTestModel
|
|
121
|
+
];
|
|
122
|
+
if (ch.modelConfig && typeof ch.modelConfig === 'object') {
|
|
123
|
+
candidates.push(ch.modelConfig.model, ch.modelConfig.opusModel, ch.modelConfig.sonnetModel, ch.modelConfig.haikuModel);
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(ch.modelRedirects)) {
|
|
126
|
+
ch.modelRedirects.forEach(r => { candidates.push(r && r.from); candidates.push(r && r.to); });
|
|
127
|
+
}
|
|
128
|
+
candidates.forEach((m) => pushUniqueModel(allModels, seen, m));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 若渠道未显式填写模型,回退使用代理聚合模型(含 /v1/models 与模型探测结果)。
|
|
132
|
+
try {
|
|
133
|
+
const detectedModels = await collectProxyModelList(enabledChannels, { forceRefresh: false });
|
|
134
|
+
if (Array.isArray(detectedModels)) {
|
|
135
|
+
detectedModels.forEach(modelId => pushUniqueModel(allModels, seen, modelId));
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn('[OpenCode Proxy] Failed to collect proxy models before writing config:', error.message);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const activeModel = currentChannel.model || currentChannel.speedTestModel || allModels[0] || null;
|
|
142
|
+
setProxyConfig(proxyResult.port, { model: activeModel, models: allModels });
|
|
143
|
+
|
|
144
|
+
// 5. 广播状态更新
|
|
145
|
+
const { broadcastProxyState } = require('../websocket-server');
|
|
146
|
+
const updatedStatus = getOpenCodeProxyStatus();
|
|
147
|
+
const { channels: allChannels } = getChannels();
|
|
148
|
+
broadcastProxyState('opencode', updatedStatus, currentChannel, allChannels);
|
|
149
|
+
|
|
150
|
+
res.json({
|
|
151
|
+
success: true,
|
|
152
|
+
port: proxyResult.port,
|
|
153
|
+
activeChannel: sanitizeChannel(currentChannel),
|
|
154
|
+
message: `OpenCode proxy started on port ${proxyResult.port}, active channel: ${currentChannel.name}`
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('[OpenCode Proxy] Error starting proxy:', error);
|
|
158
|
+
res.status(500).json({ error: error.message });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 停止代理
|
|
163
|
+
router.post('/stop', async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
// 1. 获取当前渠道信息
|
|
166
|
+
const { channels } = getChannels();
|
|
167
|
+
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
168
|
+
const activeChannel = enabledChannels[0];
|
|
169
|
+
|
|
170
|
+
// 2. 停止代理服务器
|
|
171
|
+
const proxyResult = await stopOpenCodeProxyServer();
|
|
172
|
+
|
|
173
|
+
// 3. 删除激活渠道文件
|
|
174
|
+
removeActiveChannelFile();
|
|
175
|
+
|
|
176
|
+
// 4. 恢复原始配置
|
|
177
|
+
if (hasBackup()) {
|
|
178
|
+
restoreSettings();
|
|
179
|
+
console.log('[OpenCode Proxy] Restored settings from backup');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 5. 广播状态更新
|
|
183
|
+
const { broadcastProxyState } = require('../websocket-server');
|
|
184
|
+
const updatedStatus = getOpenCodeProxyStatus();
|
|
185
|
+
broadcastProxyState('opencode', updatedStatus, activeChannel, channels);
|
|
186
|
+
|
|
187
|
+
res.json({
|
|
188
|
+
success: true,
|
|
189
|
+
message: `OpenCode proxy stopped${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
|
|
190
|
+
port: proxyResult.port
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error('[OpenCode Proxy] Error stopping proxy:', error);
|
|
194
|
+
res.status(500).json({ error: error.message });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
module.exports = router;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getProjects,
|
|
5
|
+
getSessionsByProject,
|
|
6
|
+
getSessionById,
|
|
7
|
+
getRecentSessions,
|
|
8
|
+
searchSessions,
|
|
9
|
+
deleteSession,
|
|
10
|
+
forkSession,
|
|
11
|
+
saveSessionOrder,
|
|
12
|
+
isOpenCodeInstalled
|
|
13
|
+
} = require('../services/opencode-sessions');
|
|
14
|
+
const { loadAliases } = require('../services/alias');
|
|
15
|
+
const { getTerminalLaunchCommand } = require('../services/terminal-config');
|
|
16
|
+
const { broadcastLog } = require('../websocket-server');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
function isNotFoundError(error) {
|
|
22
|
+
if (!error || !error.message) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return error.message === 'Session not found' || error.message === 'Project not found';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = (config) => {
|
|
29
|
+
/**
|
|
30
|
+
* GET /api/opencode/sessions/search/global?keyword=xxx
|
|
31
|
+
* 全局搜索
|
|
32
|
+
*/
|
|
33
|
+
router.get('/search/global', (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
if (!isOpenCodeInstalled()) {
|
|
36
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { keyword } = req.query;
|
|
40
|
+
const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
|
|
41
|
+
const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
|
|
42
|
+
|
|
43
|
+
if (!keyword) {
|
|
44
|
+
return res.status(400).json({ error: 'Keyword is required' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const results = searchSessions(keyword, contextLength);
|
|
48
|
+
const totalMatches = results.reduce((sum, session) => sum + (session.matchCount || 0), 0);
|
|
49
|
+
|
|
50
|
+
res.json({
|
|
51
|
+
keyword,
|
|
52
|
+
totalMatches,
|
|
53
|
+
sessions: results,
|
|
54
|
+
source: 'opencode'
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error('[OpenCode API] Failed to search sessions:', err);
|
|
58
|
+
res.status(500).json({ error: err.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* GET /api/opencode/sessions/recent/list?limit=10
|
|
64
|
+
* 获取最近会话
|
|
65
|
+
*/
|
|
66
|
+
router.get('/recent/list', (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
if (!isOpenCodeInstalled()) {
|
|
69
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const limit = parseInt(req.query.limit, 10) || 5;
|
|
73
|
+
const sessions = getRecentSessions(limit);
|
|
74
|
+
|
|
75
|
+
res.json({
|
|
76
|
+
sessions,
|
|
77
|
+
source: 'opencode'
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('[OpenCode API] Failed to get recent sessions:', err);
|
|
81
|
+
res.status(500).json({ error: err.message });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* GET /api/opencode/sessions/:projectName/search
|
|
87
|
+
* 项目内搜索
|
|
88
|
+
*/
|
|
89
|
+
router.get('/:projectName/search', (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
if (!isOpenCodeInstalled()) {
|
|
92
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { projectName } = req.params;
|
|
96
|
+
const { keyword } = req.query;
|
|
97
|
+
const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
|
|
98
|
+
const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
|
|
99
|
+
|
|
100
|
+
if (!keyword) {
|
|
101
|
+
return res.status(400).json({ error: 'Keyword is required' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const results = searchSessions(keyword, contextLength, projectName);
|
|
105
|
+
const totalMatches = results.reduce((sum, session) => sum + (session.matchCount || 0), 0);
|
|
106
|
+
|
|
107
|
+
res.json({
|
|
108
|
+
keyword,
|
|
109
|
+
totalMatches,
|
|
110
|
+
sessions: results
|
|
111
|
+
});
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error('[OpenCode API] Failed to search project sessions:', err);
|
|
114
|
+
res.status(500).json({ error: err.message });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* GET /api/opencode/sessions/:projectName
|
|
120
|
+
* 获取项目的所有会话
|
|
121
|
+
*/
|
|
122
|
+
router.get('/:projectName', (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
if (!isOpenCodeInstalled()) {
|
|
125
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { projectName } = req.params;
|
|
129
|
+
const sessions = getSessionsByProject(projectName);
|
|
130
|
+
const aliases = loadAliases();
|
|
131
|
+
const projects = getProjects();
|
|
132
|
+
const project = projects.find(p => p.name === projectName);
|
|
133
|
+
|
|
134
|
+
// 计算总大小
|
|
135
|
+
const totalSize = sessions.reduce((sum, session) => {
|
|
136
|
+
return sum + (session.size || 0);
|
|
137
|
+
}, 0);
|
|
138
|
+
|
|
139
|
+
res.json({
|
|
140
|
+
sessions,
|
|
141
|
+
totalSize,
|
|
142
|
+
aliases,
|
|
143
|
+
projectInfo: {
|
|
144
|
+
name: projectName,
|
|
145
|
+
fullPath: project?.fullPath || projectName,
|
|
146
|
+
path: project?.path || projectName,
|
|
147
|
+
displayName: project?.displayName || projectName
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('[OpenCode API] Failed to get sessions:', err);
|
|
152
|
+
res.status(500).json({ error: err.message });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* GET /api/opencode/sessions/:projectName/:sessionId/messages
|
|
158
|
+
* 获取会话的消息列表
|
|
159
|
+
* Note: OpenCode 的消息存储在单独的 message 目录,暂时返回基本信息
|
|
160
|
+
*/
|
|
161
|
+
router.get('/:projectName/:sessionId/messages', (req, res) => {
|
|
162
|
+
try {
|
|
163
|
+
if (!isOpenCodeInstalled()) {
|
|
164
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { sessionId } = req.params;
|
|
168
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
169
|
+
const session = getSessionById(sessionId);
|
|
170
|
+
|
|
171
|
+
// 读取消息文件
|
|
172
|
+
const messagesDir = path.join(
|
|
173
|
+
os.homedir(), '.local', 'share', 'opencode', 'storage', 'message', sessionId
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const convertedMessages = [];
|
|
177
|
+
|
|
178
|
+
if (fs.existsSync(messagesDir)) {
|
|
179
|
+
const files = fs.readdirSync(messagesDir)
|
|
180
|
+
.filter(f => f.endsWith('.json'))
|
|
181
|
+
.sort();
|
|
182
|
+
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
try {
|
|
185
|
+
const content = fs.readFileSync(path.join(messagesDir, file), 'utf8');
|
|
186
|
+
const msg = JSON.parse(content);
|
|
187
|
+
|
|
188
|
+
if (msg.role === 'user') {
|
|
189
|
+
// 提取用户消息内容
|
|
190
|
+
let textContent = '';
|
|
191
|
+
if (Array.isArray(msg.content)) {
|
|
192
|
+
textContent = msg.content
|
|
193
|
+
.filter(c => c.type === 'text')
|
|
194
|
+
.map(c => c.text || '')
|
|
195
|
+
.join('\n');
|
|
196
|
+
} else if (typeof msg.content === 'string') {
|
|
197
|
+
textContent = msg.content;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
convertedMessages.push({
|
|
201
|
+
type: 'user',
|
|
202
|
+
content: textContent || '[空消息]',
|
|
203
|
+
timestamp: msg.time?.created ? new Date(msg.time.created).toISOString() : null,
|
|
204
|
+
model: null
|
|
205
|
+
});
|
|
206
|
+
} else if (msg.role === 'assistant') {
|
|
207
|
+
// 提取助手消息内容
|
|
208
|
+
let textContent = '';
|
|
209
|
+
if (Array.isArray(msg.content)) {
|
|
210
|
+
textContent = msg.content
|
|
211
|
+
.filter(c => c.type === 'text')
|
|
212
|
+
.map(c => c.text || '')
|
|
213
|
+
.join('\n');
|
|
214
|
+
} else if (typeof msg.content === 'string') {
|
|
215
|
+
textContent = msg.content;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
convertedMessages.push({
|
|
219
|
+
type: 'assistant',
|
|
220
|
+
content: textContent || '[空消息]',
|
|
221
|
+
timestamp: msg.time?.created ? new Date(msg.time.created).toISOString() : null,
|
|
222
|
+
model: msg.model || 'opencode'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} catch (parseErr) {
|
|
226
|
+
// 忽略解析错误
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 分页处理
|
|
232
|
+
const pageNum = parseInt(page);
|
|
233
|
+
const limitNum = parseInt(limit);
|
|
234
|
+
|
|
235
|
+
let messages = convertedMessages;
|
|
236
|
+
if (order === 'desc') {
|
|
237
|
+
messages = [...messages].reverse();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const totalMessages = messages.length;
|
|
241
|
+
const start = (pageNum - 1) * limitNum;
|
|
242
|
+
const end = start + limitNum;
|
|
243
|
+
const paginatedMessages = messages.slice(start, end);
|
|
244
|
+
|
|
245
|
+
res.json({
|
|
246
|
+
messages: paginatedMessages,
|
|
247
|
+
metadata: {
|
|
248
|
+
gitBranch: null,
|
|
249
|
+
gitRepository: null,
|
|
250
|
+
cwd: session?.directory || null,
|
|
251
|
+
model: 'opencode'
|
|
252
|
+
},
|
|
253
|
+
pagination: {
|
|
254
|
+
page: pageNum,
|
|
255
|
+
limit: limitNum,
|
|
256
|
+
total: totalMessages,
|
|
257
|
+
hasMore: end < totalMessages
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error('[OpenCode API] Failed to get session messages:', err);
|
|
262
|
+
res.status(500).json({ error: err.message });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* DELETE /api/opencode/sessions/:projectName/:sessionId
|
|
268
|
+
* 删除会话
|
|
269
|
+
*/
|
|
270
|
+
router.delete('/:projectName/:sessionId', (req, res) => {
|
|
271
|
+
try {
|
|
272
|
+
if (!isOpenCodeInstalled()) {
|
|
273
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const { sessionId } = req.params;
|
|
277
|
+
const result = deleteSession(sessionId);
|
|
278
|
+
|
|
279
|
+
res.json(result);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (isNotFoundError(err)) {
|
|
282
|
+
console.warn('[OpenCode API] Delete session target not found:', err.message);
|
|
283
|
+
return res.status(404).json({ error: err.message });
|
|
284
|
+
}
|
|
285
|
+
console.error('[OpenCode API] Failed to delete session:', err);
|
|
286
|
+
res.status(500).json({ error: err.message });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* POST /api/opencode/sessions/:projectName/:sessionId/fork
|
|
292
|
+
* Fork 一个会话
|
|
293
|
+
*/
|
|
294
|
+
router.post('/:projectName/:sessionId/fork', (req, res) => {
|
|
295
|
+
try {
|
|
296
|
+
if (!isOpenCodeInstalled()) {
|
|
297
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const { sessionId } = req.params;
|
|
301
|
+
const result = forkSession(sessionId);
|
|
302
|
+
res.json(result);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
if (isNotFoundError(err)) {
|
|
305
|
+
console.warn('[OpenCode API] Fork session target not found:', err.message);
|
|
306
|
+
return res.status(404).json({ error: err.message });
|
|
307
|
+
}
|
|
308
|
+
console.error('[OpenCode API] Failed to fork session:', err);
|
|
309
|
+
res.status(500).json({ error: err.message });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* POST /api/opencode/sessions/:projectName/order
|
|
315
|
+
* 保存会话排序
|
|
316
|
+
*/
|
|
317
|
+
router.post('/:projectName/order', (req, res) => {
|
|
318
|
+
try {
|
|
319
|
+
if (!isOpenCodeInstalled()) {
|
|
320
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const { projectName } = req.params;
|
|
324
|
+
const { order } = req.body;
|
|
325
|
+
|
|
326
|
+
if (!Array.isArray(order)) {
|
|
327
|
+
return res.status(400).json({ error: 'order must be an array' });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
saveSessionOrder(projectName, order);
|
|
331
|
+
res.json({ success: true });
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error('[OpenCode API] Failed to save session order:', err);
|
|
334
|
+
res.status(500).json({ error: err.message });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* POST /api/opencode/sessions/:projectName/:sessionId/launch
|
|
340
|
+
* 启动会话(打开终端)
|
|
341
|
+
*/
|
|
342
|
+
router.post('/:projectName/:sessionId/launch', (req, res) => {
|
|
343
|
+
try {
|
|
344
|
+
if (!isOpenCodeInstalled()) {
|
|
345
|
+
return res.status(404).json({ error: 'OpenCode CLI not installed' });
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const { exec } = require('child_process');
|
|
349
|
+
const { projectName, sessionId } = req.params;
|
|
350
|
+
const { targetTool } = req.body || {};
|
|
351
|
+
|
|
352
|
+
if (targetTool && targetTool !== 'opencode') {
|
|
353
|
+
return res.status(400).json({
|
|
354
|
+
error: 'OpenCode 会话暂不支持直接切换到其他 CLI,请使用 OpenCode 启动'
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const session = getSessionById(sessionId);
|
|
359
|
+
if (!session) {
|
|
360
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const projects = getProjects();
|
|
364
|
+
const project = projects.find(p => p.name === projectName);
|
|
365
|
+
const cwd = session.directory || project?.fullPath || os.homedir();
|
|
366
|
+
const normalizedCwd = process.platform === 'win32' ? cwd.replace(/\//g, '\\') : cwd;
|
|
367
|
+
|
|
368
|
+
const { command, terminalId, terminalName } = getTerminalLaunchCommand(
|
|
369
|
+
normalizedCwd,
|
|
370
|
+
sessionId,
|
|
371
|
+
'opencode'
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
broadcastLog({
|
|
375
|
+
type: 'action',
|
|
376
|
+
action: 'launch_opencode_session',
|
|
377
|
+
message: `启动 OpenCode 会话 ${sessionId.substring(0, 8)}`,
|
|
378
|
+
sessionId,
|
|
379
|
+
tool: 'opencode',
|
|
380
|
+
timestamp: Date.now()
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const shellOption = process.platform === 'win32' ? { shell: 'cmd.exe' } : { shell: true };
|
|
384
|
+
exec(command, shellOption, (error) => {
|
|
385
|
+
if (error) {
|
|
386
|
+
console.error('[OpenCode] Failed to launch terminal:', error.message);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
res.json({
|
|
391
|
+
success: true,
|
|
392
|
+
cwd,
|
|
393
|
+
terminal: terminalName,
|
|
394
|
+
terminalId
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error('[OpenCode API] Failed to launch session:', err);
|
|
398
|
+
res.status(500).json({ error: err.message });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
return router;
|
|
403
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getStatistics,
|
|
5
|
+
getDailyStatistics,
|
|
6
|
+
getTodayStatistics
|
|
7
|
+
} = require('../services/opencode-statistics-service');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 获取 OpenCode 总体统计数据
|
|
11
|
+
* GET /api/opencode/statistics/summary
|
|
12
|
+
*/
|
|
13
|
+
router.get('/summary', (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const stats = getStatistics();
|
|
16
|
+
res.json(stats);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('[OpenCode] Failed to get statistics:', error);
|
|
19
|
+
res.status(500).json({ error: 'Failed to get statistics' });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取 OpenCode 今日统计数据
|
|
25
|
+
* GET /api/opencode/statistics/today
|
|
26
|
+
*/
|
|
27
|
+
router.get('/today', (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const stats = getTodayStatistics();
|
|
30
|
+
res.json(stats);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('[OpenCode] Failed to get today statistics:', error);
|
|
33
|
+
res.status(500).json({ error: 'Failed to get today statistics' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 获取 OpenCode 指定日期的统计数据
|
|
39
|
+
* GET /api/opencode/statistics/daily/:date
|
|
40
|
+
*/
|
|
41
|
+
router.get('/daily/:date', (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const { date } = req.params;
|
|
44
|
+
|
|
45
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
46
|
+
return res.status(400).json({ error: 'Invalid date format. Expected YYYY-MM-DD' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const stats = getDailyStatistics(date);
|
|
50
|
+
res.json(stats);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[OpenCode] Failed to get daily statistics:', error);
|
|
53
|
+
res.status(500).json({ error: 'Failed to get daily statistics' });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
module.exports = router;
|