coding-tool-x 3.6.12 → 3.7.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.
Files changed (80) hide show
  1. package/README.md +46 -33
  2. package/dist/web/assets/{Analytics-BuBlsbcI.js → Analytics-Dma_j2Rq.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-DQczxuOX.js → ConfigTemplates-C49Y1PLH.js} +1 -1
  4. package/dist/web/assets/Home-DGGC8H4g.js +1 -0
  5. package/dist/web/assets/Home-ewgeIhF7.css +1 -0
  6. package/dist/web/assets/{PluginManager-C8gAlaps.js → PluginManager-B1ECTHyY.js} +1 -1
  7. package/dist/web/assets/{ProjectList-DL4JK6ci.css → ProjectList-B_L55-l9.css} +1 -1
  8. package/dist/web/assets/ProjectList-DyiGyT-H.js +1 -0
  9. package/dist/web/assets/SessionList-CH5GwwiJ.js +1 -0
  10. package/dist/web/assets/{SkillManager-BDbNbNkK.js → SkillManager-CRyVhjYQ.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-DGGaGW2c.js → WorkspaceManager-CO9tclVt.js} +1 -1
  12. package/dist/web/assets/{icons-QFXzEgqM.js → icons-Dop5j7Bf.js} +1 -1
  13. package/dist/web/assets/index-CM26UrnJ.css +1 -0
  14. package/dist/web/assets/index-EQq56pRa.js +2 -0
  15. package/dist/web/assets/{naive-ui-kcWDeJwl.js → naive-ui-CVyvdmg1.js} +1 -1
  16. package/dist/web/index.html +4 -4
  17. package/package.json +2 -1
  18. package/src/commands/channels.js +85 -24
  19. package/src/commands/daemon.js +7 -4
  20. package/src/commands/doctor.js +17 -8
  21. package/src/commands/logs.js +48 -6
  22. package/src/commands/port-config.js +81 -75
  23. package/src/commands/proxy-control.js +52 -19
  24. package/src/commands/stats.js +37 -19
  25. package/src/commands/toggle-proxy.js +131 -53
  26. package/src/commands/ui.js +3 -4
  27. package/src/config/default.js +14 -3
  28. package/src/config/paths.js +99 -11
  29. package/src/index.js +11 -16
  30. package/src/server/api/commands.js +1 -1
  31. package/src/server/api/config-export.js +4 -2
  32. package/src/server/api/config-registry.js +40 -34
  33. package/src/server/api/config.js +2 -1
  34. package/src/server/api/dashboard.js +22 -6
  35. package/src/server/api/mcp.js +5 -3
  36. package/src/server/api/pi-channels.js +349 -0
  37. package/src/server/api/pi-config.js +164 -0
  38. package/src/server/api/pi-projects.js +76 -0
  39. package/src/server/api/pi-proxy.js +154 -0
  40. package/src/server/api/pi-sessions.js +334 -0
  41. package/src/server/api/pi-statistics.js +40 -0
  42. package/src/server/api/plugins.js +1 -1
  43. package/src/server/api/prompts.js +3 -2
  44. package/src/server/api/skills.js +1 -1
  45. package/src/server/api/workspaces.js +2 -2
  46. package/src/server/index.js +41 -0
  47. package/src/server/pi-proxy-server.js +66 -0
  48. package/src/server/services/agents-service.js +2 -1
  49. package/src/server/services/base/proxy-utils.js +1 -0
  50. package/src/server/services/channel-balance.js +151 -1
  51. package/src/server/services/channel-scheduler.js +7 -1
  52. package/src/server/services/commands-service.js +11 -2
  53. package/src/server/services/config-export-service.js +275 -18
  54. package/src/server/services/config-registry-service.js +20 -18
  55. package/src/server/services/config-sync-manager.js +115 -6
  56. package/src/server/services/config-sync-service.js +4 -2
  57. package/src/server/services/config-templates-service.js +183 -45
  58. package/src/server/services/mcp-service.js +217 -33
  59. package/src/server/services/pi-auth-providers.js +294 -0
  60. package/src/server/services/pi-channels.js +94 -0
  61. package/src/server/services/pi-config.js +287 -0
  62. package/src/server/services/pi-sessions.js +490 -0
  63. package/src/server/services/pi-settings-manager.js +571 -0
  64. package/src/server/services/pi-statistics-service.js +317 -0
  65. package/src/server/services/plugins-service.js +673 -11
  66. package/src/server/services/prompts-service.js +112 -7
  67. package/src/server/services/proxy-log-helper.js +1 -0
  68. package/src/server/services/proxy-runtime.js +5 -1
  69. package/src/server/services/skill-service.js +13 -3
  70. package/src/server/services/speed-test.js +9 -3
  71. package/src/server/services/ui-config.js +50 -12
  72. package/src/server/services/workspace-service.js +19 -5
  73. package/src/server/websocket-server.js +7 -1
  74. package/dist/web/assets/Home-BvAzzWnB.js +0 -1
  75. package/dist/web/assets/Home-CIua-b-o.css +0 -1
  76. package/dist/web/assets/ProjectList-nqgi6_cR.js +0 -1
  77. package/dist/web/assets/SessionList-D8AxMYy6.js +0 -1
  78. package/dist/web/assets/index-Dsh4Q5Cg.css +0 -1
  79. package/dist/web/assets/index-y-eEayxR.js +0 -2
  80. package/src/utils/cli-flags.js +0 -7
@@ -0,0 +1,154 @@
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const router = express.Router();
5
+ const {
6
+ startPiProxyServer,
7
+ stopPiProxyServer,
8
+ getPiProxyStatus
9
+ } = require('../pi-proxy-server');
10
+ const {
11
+ getChannels,
12
+ getEnabledChannels,
13
+ markChannelAsRecentlyUsed
14
+ } = require('../services/pi-channels');
15
+ const { getSchedulerState } = require('../services/channel-scheduler');
16
+ const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
17
+
18
+ function sanitizeChannel(channel) {
19
+ if (!channel) return null;
20
+ return {
21
+ id: channel.id,
22
+ name: channel.name,
23
+ baseUrl: channel.baseUrl,
24
+ websiteUrl: channel.websiteUrl
25
+ };
26
+ }
27
+
28
+ function selectLatestEnabledChannel(channels) {
29
+ const enabledChannels = (channels || []).filter(ch => ch.enabled !== false);
30
+ if (enabledChannels.length === 0) return null;
31
+ return enabledChannels.reduce((latest, current) => {
32
+ const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
33
+ const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
34
+ return currentTs > latestTs ? current : latest;
35
+ }, enabledChannels[0]);
36
+ }
37
+
38
+ function saveActiveChannelId(channelId) {
39
+ ensureStorageDirMigrated();
40
+ const filePath = PATHS.activeChannel.pi;
41
+ const dir = path.dirname(filePath);
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+ fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
46
+ }
47
+
48
+ function loadActiveChannelId() {
49
+ ensureStorageDirMigrated();
50
+ try {
51
+ if (!fs.existsSync(PATHS.activeChannel.pi)) return null;
52
+ const data = JSON.parse(fs.readFileSync(PATHS.activeChannel.pi, 'utf8'));
53
+ return data.activeChannelId || null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function removeActiveChannelFile() {
60
+ ensureStorageDirMigrated();
61
+ if (fs.existsSync(PATHS.activeChannel.pi)) {
62
+ fs.unlinkSync(PATHS.activeChannel.pi);
63
+ }
64
+ }
65
+
66
+ function resolveActiveChannel(channels, activeChannelId = null) {
67
+ if (!Array.isArray(channels) || channels.length === 0) return null;
68
+ if (activeChannelId) {
69
+ const matched = channels.find(channel => channel.id === activeChannelId);
70
+ if (matched) return matched;
71
+ }
72
+ return selectLatestEnabledChannel(channels)
73
+ || channels.find(channel => channel.enabled !== false)
74
+ || channels[0]
75
+ || null;
76
+ }
77
+
78
+ router.get('/status', (req, res) => {
79
+ try {
80
+ const proxy = getPiProxyStatus();
81
+ const { channels } = getChannels();
82
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
83
+ const activeChannel = resolveActiveChannel(channels, loadActiveChannelId());
84
+ res.json({
85
+ proxy,
86
+ config: {
87
+ mode: 'models-yml-provider-config',
88
+ nativeProxyProtocol: false
89
+ },
90
+ activeChannel: sanitizeChannel(activeChannel),
91
+ enabledChannelsCount: enabledChannels.length,
92
+ totalChannelsCount: channels.length
93
+ });
94
+ } catch (error) {
95
+ res.status(500).json({ error: error.message });
96
+ }
97
+ });
98
+
99
+ router.post('/start', async (req, res) => {
100
+ try {
101
+ const enabledChannels = getEnabledChannels();
102
+ if (enabledChannels.length === 0) {
103
+ return res.status(400).json({
104
+ error: 'No enabled OMP channel found. Please create and enable a channel first.'
105
+ });
106
+ }
107
+
108
+ let currentChannel = selectLatestEnabledChannel(enabledChannels) || enabledChannels[0];
109
+ currentChannel = markChannelAsRecentlyUsed(currentChannel.id);
110
+ saveActiveChannelId(currentChannel.id);
111
+
112
+ const proxyResult = await startPiProxyServer();
113
+ if (!proxyResult.success) {
114
+ return res.status(500).json({ error: 'Failed to enable OMP models.yml provider config' });
115
+ }
116
+
117
+ const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
118
+ const updatedStatus = getPiProxyStatus();
119
+ const { channels: latestChannels } = getChannels();
120
+ const activeChannel = latestChannels.find(channel => channel.id === currentChannel.id) || currentChannel;
121
+ broadcastProxyState('pi', updatedStatus, activeChannel, latestChannels);
122
+ broadcastSchedulerState('pi', getSchedulerState('pi'));
123
+
124
+ res.json({
125
+ success: true,
126
+ port: proxyResult.port,
127
+ activeChannel: sanitizeChannel(activeChannel),
128
+ mode: 'models-yml-provider-config',
129
+ sync: proxyResult.sync || null,
130
+ warnings: proxyResult.warnings || [],
131
+ message: `OMP models.yml provider config enabled, active channel: ${activeChannel.name}`
132
+ });
133
+ } catch (error) {
134
+ console.error('[OMP Proxy] Start failed:', error);
135
+ res.status(500).json({ error: error.message });
136
+ }
137
+ });
138
+
139
+ router.post('/stop', async (req, res) => {
140
+ try {
141
+ const result = await stopPiProxyServer();
142
+ removeActiveChannelFile();
143
+ const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
144
+ const { channels } = getChannels();
145
+ broadcastProxyState('pi', getPiProxyStatus(), null, channels);
146
+ broadcastSchedulerState('pi', getSchedulerState('pi'));
147
+ res.json({ success: true, ...result });
148
+ } catch (error) {
149
+ console.error('[OMP Proxy] Stop failed:', error);
150
+ res.status(500).json({ error: error.message });
151
+ }
152
+ });
153
+
154
+ module.exports = router;
@@ -0,0 +1,334 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ buildLaunchCommand,
5
+ getProjects,
6
+ getSessionsByProject,
7
+ getSessionById,
8
+ getSessionMessages,
9
+ getRecentSessions,
10
+ searchSessions,
11
+ deleteSession,
12
+ forkSession,
13
+ saveSessionOrder,
14
+ isPiInstalled,
15
+ HOME_DIR
16
+ } = require('../services/pi-sessions');
17
+ const { loadAliases } = require('../services/alias');
18
+ const { broadcastLog } = require('../websocket-server');
19
+
20
+ function isNotFoundError(error) {
21
+ if (!error || !error.message) return false;
22
+ return error.message === 'Session not found' || error.message === 'Project not found';
23
+ }
24
+
25
+ function quoteForShell(value) {
26
+ return `"${String(value).replace(/"/g, '\\"')}"`;
27
+ }
28
+
29
+ module.exports = () => {
30
+ router.get('/search/global', (req, res) => {
31
+ try {
32
+ if (!isPiInstalled()) {
33
+ return res.status(404).json({ error: 'OMP CLI not installed' });
34
+ }
35
+ const { keyword } = req.query;
36
+ const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
37
+ const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
38
+ if (!keyword) {
39
+ return res.status(400).json({ error: 'Keyword is required' });
40
+ }
41
+ const sessions = searchSessions(keyword, contextLength);
42
+ const totalMatches = sessions.reduce((sum, session) => sum + (session.matchCount || 0), 0);
43
+ res.json({ keyword, totalMatches, sessions, source: 'pi' });
44
+ } catch (err) {
45
+ console.error('[OMP API] Failed to search sessions:', err);
46
+ res.status(500).json({ error: err.message });
47
+ }
48
+ });
49
+
50
+ router.get('/recent/list', (req, res) => {
51
+ try {
52
+ if (!isPiInstalled()) {
53
+ return res.status(404).json({ error: 'OMP CLI not installed' });
54
+ }
55
+ const limit = parseInt(req.query.limit, 10) || 5;
56
+ res.json({ sessions: getRecentSessions(limit), source: 'pi' });
57
+ } catch (err) {
58
+ console.error('[OMP API] Failed to get recent sessions:', err);
59
+ res.status(500).json({ error: err.message });
60
+ }
61
+ });
62
+
63
+ router.get('/:projectName/search', (req, res) => {
64
+ try {
65
+ if (!isPiInstalled()) {
66
+ return res.status(404).json({ error: 'OMP CLI not installed' });
67
+ }
68
+ const { projectName } = req.params;
69
+ const { keyword } = req.query;
70
+ const parsedContextLength = req.query.context ? parseInt(req.query.context, 10) : 35;
71
+ const contextLength = Number.isFinite(parsedContextLength) ? parsedContextLength : 35;
72
+ if (!keyword) {
73
+ return res.status(400).json({ error: 'Keyword is required' });
74
+ }
75
+ const sessions = searchSessions(keyword, contextLength, projectName);
76
+ const totalMatches = sessions.reduce((sum, session) => sum + (session.matchCount || 0), 0);
77
+ res.json({ keyword, totalMatches, sessions });
78
+ } catch (err) {
79
+ console.error('[OMP API] Failed to search project sessions:', err);
80
+ res.status(500).json({ error: err.message });
81
+ }
82
+ });
83
+
84
+ router.get('/:projectName', (req, res) => {
85
+ try {
86
+ if (!isPiInstalled()) {
87
+ return res.status(404).json({ error: 'OMP CLI not installed' });
88
+ }
89
+ const { projectName } = req.params;
90
+ const sessions = getSessionsByProject(projectName);
91
+ const aliases = loadAliases();
92
+ const project = getProjects().find(p => p.name === projectName);
93
+ const totalSize = sessions.reduce((sum, session) => sum + (session.size || 0), 0);
94
+ res.json({
95
+ sessions,
96
+ totalSize,
97
+ aliases,
98
+ projectInfo: {
99
+ name: projectName,
100
+ fullPath: project?.fullPath || project?.path || projectName,
101
+ path: project?.path || projectName,
102
+ displayName: project?.displayName || projectName
103
+ }
104
+ });
105
+ } catch (err) {
106
+ console.error('[OMP API] Failed to get sessions:', err);
107
+ res.status(500).json({ error: err.message });
108
+ }
109
+ });
110
+
111
+ router.get('/:projectName/:sessionId/status', (req, res) => {
112
+ try {
113
+ if (!isPiInstalled()) {
114
+ return res.status(404).json({ error: 'OMP CLI not installed' });
115
+ }
116
+ const session = getSessionById(req.params.sessionId);
117
+ if (!session) {
118
+ return res.status(404).json({ error: 'Session not found' });
119
+ }
120
+ res.json({
121
+ sessionId: req.params.sessionId,
122
+ lastModified: session.mtime || null,
123
+ size: session.size || 0
124
+ });
125
+ } catch (err) {
126
+ console.error('[OMP API] Failed to get session status:', err);
127
+ res.status(500).json({ error: err.message });
128
+ }
129
+ });
130
+
131
+ router.get('/:projectName/:sessionId/outline', (req, res) => {
132
+ try {
133
+ if (!isPiInstalled()) {
134
+ return res.status(404).json({ error: 'OMP CLI not installed' });
135
+ }
136
+ const session = getSessionById(req.params.sessionId);
137
+ if (!session) {
138
+ return res.status(404).json({ error: 'Session not found' });
139
+ }
140
+ let userMessageNumber = 0;
141
+ const items = [];
142
+ for (const msg of getSessionMessages(req.params.sessionId)) {
143
+ if (msg.type !== 'user') continue;
144
+ const preview = typeof msg.content === 'string' ? msg.content.trim() : '';
145
+ if (!preview) continue;
146
+ userMessageNumber += 1;
147
+ items.push({
148
+ userMessageNumber,
149
+ preview: preview.length > 42 ? `${preview.slice(0, 42)}...` : preview,
150
+ timestamp: msg.timestamp || null
151
+ });
152
+ }
153
+ res.json({ sessionId: req.params.sessionId, items });
154
+ } catch (err) {
155
+ console.error('[OMP API] Failed to get session outline:', err);
156
+ res.status(500).json({ error: err.message });
157
+ }
158
+ });
159
+
160
+ router.get('/:projectName/:sessionId/messages', (req, res) => {
161
+ try {
162
+ if (!isPiInstalled()) {
163
+ return res.status(404).json({ error: 'OMP CLI not installed' });
164
+ }
165
+ const { sessionId } = req.params;
166
+ const { page = 1, limit = 20, order = 'desc' } = req.query;
167
+ const session = getSessionById(sessionId);
168
+ if (!session) {
169
+ return res.status(404).json({ error: 'Session not found' });
170
+ }
171
+ let userMessageNumber = 0;
172
+ const convertedMessages = getSessionMessages(sessionId).map((message) => {
173
+ if (message.type !== 'user') return message;
174
+ userMessageNumber += 1;
175
+ return { ...message, userMessageNumber };
176
+ });
177
+ const pageNum = parseInt(page, 10) || 1;
178
+ const limitNum = parseInt(limit, 10) || 20;
179
+ const orderedMessages = order === 'desc' ? [...convertedMessages].reverse() : convertedMessages;
180
+ const start = (pageNum - 1) * limitNum;
181
+ const end = start + limitNum;
182
+ res.json({
183
+ messages: orderedMessages.slice(start, end),
184
+ metadata: {
185
+ gitBranch: null,
186
+ gitRepository: null,
187
+ cwd: session?.directory || null,
188
+ provider: session?.provider || '',
189
+ model: session?.model || 'pi'
190
+ },
191
+ pagination: {
192
+ page: pageNum,
193
+ limit: limitNum,
194
+ total: orderedMessages.length,
195
+ hasMore: end < orderedMessages.length
196
+ }
197
+ });
198
+ } catch (err) {
199
+ console.error('[OMP API] Failed to get session messages:', err);
200
+ res.status(500).json({ error: err.message });
201
+ }
202
+ });
203
+
204
+ router.delete('/:projectName/:sessionId', (req, res) => {
205
+ try {
206
+ if (!isPiInstalled()) {
207
+ return res.status(404).json({ error: 'OMP CLI not installed' });
208
+ }
209
+ res.json(deleteSession(req.params.sessionId));
210
+ } catch (err) {
211
+ if (isNotFoundError(err)) {
212
+ return res.status(404).json({ error: err.message });
213
+ }
214
+ console.error('[OMP API] Failed to delete session:', err);
215
+ res.status(500).json({ error: err.message });
216
+ }
217
+ });
218
+
219
+ router.post('/:projectName/batch-delete', (req, res) => {
220
+ try {
221
+ if (!isPiInstalled()) {
222
+ return res.status(404).json({ error: 'OMP CLI not installed' });
223
+ }
224
+ const { sessionIds } = req.body || {};
225
+ if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
226
+ return res.status(400).json({ error: 'sessionIds must be a non-empty array' });
227
+ }
228
+ const uniqueSessionIds = Array.from(new Set(
229
+ sessionIds
230
+ .filter(sessionId => typeof sessionId === 'string')
231
+ .map(sessionId => sessionId.trim())
232
+ .filter(Boolean)
233
+ ));
234
+ if (uniqueSessionIds.length === 0) {
235
+ return res.status(400).json({ error: 'sessionIds must be a non-empty array' });
236
+ }
237
+ const deletedSessionIds = [];
238
+ const failed = [];
239
+ uniqueSessionIds.forEach((sessionId) => {
240
+ try {
241
+ deleteSession(sessionId);
242
+ deletedSessionIds.push(sessionId);
243
+ } catch (err) {
244
+ failed.push({ sessionId, error: err.message });
245
+ }
246
+ });
247
+ res.json({
248
+ success: failed.length === 0,
249
+ requestedCount: uniqueSessionIds.length,
250
+ deletedCount: deletedSessionIds.length,
251
+ deletedSessionIds,
252
+ failed
253
+ });
254
+ } catch (err) {
255
+ console.error('[OMP API] Failed to batch delete sessions:', err);
256
+ res.status(500).json({ error: err.message });
257
+ }
258
+ });
259
+
260
+ router.post('/:projectName/:sessionId/fork', (req, res) => {
261
+ try {
262
+ if (!isPiInstalled()) {
263
+ return res.status(404).json({ error: 'OMP CLI not installed' });
264
+ }
265
+ res.json(forkSession(req.params.sessionId, req.body || {}));
266
+ } catch (err) {
267
+ if (isNotFoundError(err)) {
268
+ return res.status(404).json({ error: err.message });
269
+ }
270
+ console.error('[OMP API] Failed to fork session:', err);
271
+ res.status(500).json({ error: err.message });
272
+ }
273
+ });
274
+
275
+ router.post('/:projectName/order', (req, res) => {
276
+ try {
277
+ if (!isPiInstalled()) {
278
+ return res.status(404).json({ error: 'OMP CLI not installed' });
279
+ }
280
+ const { order } = req.body || {};
281
+ if (!Array.isArray(order)) {
282
+ return res.status(400).json({ error: 'order must be an array' });
283
+ }
284
+ saveSessionOrder(req.params.projectName, order);
285
+ res.json({ success: true });
286
+ } catch (err) {
287
+ console.error('[OMP API] Failed to save session order:', err);
288
+ res.status(500).json({ error: err.message });
289
+ }
290
+ });
291
+
292
+ router.post('/:projectName/:sessionId/launch', (req, res) => {
293
+ try {
294
+ if (!isPiInstalled()) {
295
+ return res.status(404).json({ error: 'OMP CLI not installed' });
296
+ }
297
+ const { projectName, sessionId } = req.params;
298
+ const session = getSessionById(sessionId);
299
+ if (!session) {
300
+ return res.status(404).json({ error: 'Session not found' });
301
+ }
302
+ const project = getProjects().find(p => p.name === projectName);
303
+ const cwd = session.directory || project?.fullPath || HOME_DIR;
304
+ const command = buildLaunchCommand(sessionId, cwd, req.body || {});
305
+ const copyCommand = `cd ${quoteForShell(cwd)} && ${command}`;
306
+
307
+ broadcastLog({
308
+ type: 'action',
309
+ action: 'launch_pi_session',
310
+ message: `复制 OMP 会话启动命令 ${sessionId.substring(0, 8)}`,
311
+ sessionId,
312
+ tool: 'pi',
313
+ toolType: 'pi',
314
+ source: 'pi',
315
+ timestamp: Date.now()
316
+ });
317
+
318
+ res.json({
319
+ success: true,
320
+ cwd,
321
+ sessionFile: session.filePath,
322
+ sessionId,
323
+ tool: 'pi',
324
+ command,
325
+ copyCommand
326
+ });
327
+ } catch (err) {
328
+ console.error('[OMP API] Failed to prepare launch command:', err);
329
+ res.status(500).json({ error: err.message });
330
+ }
331
+ });
332
+
333
+ return router;
334
+ };
@@ -0,0 +1,40 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const {
4
+ getStatistics,
5
+ getDailyStatistics,
6
+ getTodayStatistics
7
+ } = require('../services/pi-statistics-service');
8
+
9
+ router.get('/summary', (req, res) => {
10
+ try {
11
+ res.json(getStatistics());
12
+ } catch (error) {
13
+ console.error('[OMP] Failed to get statistics:', error);
14
+ res.status(500).json({ error: 'Failed to get statistics' });
15
+ }
16
+ });
17
+
18
+ router.get('/today', (req, res) => {
19
+ try {
20
+ res.json(getTodayStatistics());
21
+ } catch (error) {
22
+ console.error('[OMP] Failed to get today statistics:', error);
23
+ res.status(500).json({ error: 'Failed to get today statistics' });
24
+ }
25
+ });
26
+
27
+ router.get('/daily/:date', (req, res) => {
28
+ try {
29
+ const { date } = req.params;
30
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
31
+ return res.status(400).json({ error: 'Invalid date format. Expected YYYY-MM-DD' });
32
+ }
33
+ res.json(getDailyStatistics(date));
34
+ } catch (error) {
35
+ console.error('[OMP] Failed to get daily statistics:', error);
36
+ res.status(500).json({ error: 'Failed to get daily statistics' });
37
+ }
38
+ });
39
+
40
+ module.exports = router;
@@ -10,7 +10,7 @@ const { maskToken } = require('../services/oauth-utils');
10
10
  const { sendApiError } = require('./validation-errors');
11
11
 
12
12
  const router = express.Router();
13
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
13
+ const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'pi'];
14
14
  const pluginServices = new Map();
15
15
 
16
16
  function resolvePlatform(rawPlatform) {
@@ -5,6 +5,7 @@
5
5
  const express = require('express');
6
6
  const router = express.Router();
7
7
  const promptsService = require('../services/prompts-service');
8
+ const PROMPT_FILE_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
8
9
 
9
10
  /**
10
11
  * GET /api/prompts/presets
@@ -203,7 +204,7 @@ router.get('/platform/:platform', (req, res) => {
203
204
  try {
204
205
  const { platform } = req.params;
205
206
 
206
- if (!['claude', 'codex', 'gemini', 'opencode'].includes(platform)) {
207
+ if (!PROMPT_FILE_PLATFORMS.includes(platform)) {
207
208
  return res.status(400).json({
208
209
  success: false,
209
210
  error: `无效的平台: ${platform}`
@@ -234,7 +235,7 @@ router.post('/import/:platform', (req, res) => {
234
235
  const { platform } = req.params;
235
236
  const { name } = req.body;
236
237
 
237
- if (!['claude', 'codex', 'gemini', 'opencode'].includes(platform)) {
238
+ if (!PROMPT_FILE_PLATFORMS.includes(platform)) {
238
239
  return res.status(400).json({
239
240
  success: false,
240
241
  error: `无效的平台: ${platform}`
@@ -8,7 +8,7 @@ const { maskToken } = require('../services/oauth-utils');
8
8
  const { sendApiError } = require('./validation-errors');
9
9
 
10
10
  const router = express.Router();
11
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
11
+ const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'pi'];
12
12
  const skillServices = new Map();
13
13
 
14
14
  function resolvePlatform(rawPlatform) {
@@ -135,7 +135,7 @@ router.get('/check-git/*', (req, res) => {
135
135
 
136
136
  /**
137
137
  * GET /api/workspaces/available-projects
138
- * 获取所有渠道(Claude/Codex/Gemini)的项目并集
138
+ * 获取所有渠道(Claude/Codex/Gemini/OpenCode/OMP)的项目并集
139
139
  */
140
140
  router.get('/available-projects', async (req, res) => {
141
141
  try {
@@ -411,7 +411,7 @@ router.delete('/:id/projects/:projectName', (req, res) => {
411
411
  /**
412
412
  * POST /api/workspaces/:id/launch
413
413
  * 获取在工作区启动 CLI 工具的命令
414
- * Body: { tool: 'claude' | 'codex' | 'gemini', projectName?: string }
414
+ * Body: { tool: 'claude' | 'codex' | 'gemini' | 'opencode' | 'pi' | 'omp', projectName?: string }
415
415
  */
416
416
  router.post('/:id/launch', (req, res) => {
417
417
  try {
@@ -23,6 +23,8 @@ const { startProxyServer } = require('./proxy-server');
23
23
  const { startCodexProxyServer } = require('./codex-proxy-server');
24
24
  const { startGeminiProxyServer } = require('./gemini-proxy-server');
25
25
  const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
26
+ const { startPiProxyServer } = require('./pi-proxy-server');
27
+ const { createRemoteMutationGuard } = require('./services/network-access');
26
28
  const { createApiRequestLogger } = require('./services/request-logger');
27
29
  const { inspectWebBuildState, ensureWebDistReady } = require('./services/web-build');
28
30
  const { ensureHttpsCredentials } = require('./services/https-cert');
@@ -148,6 +150,8 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
148
150
 
149
151
  const app = express();
150
152
  const useHttps = options.useHttps === true || process.argv.includes('--https');
153
+ const lanMode = host === '0.0.0.0';
154
+ const allowRemoteMutation = process.env.CC_TOOL_ALLOW_REMOTE_WRITE === 'true';
151
155
 
152
156
  // Middleware
153
157
  app.use(express.json({ limit: '100mb' }));
@@ -167,6 +171,15 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
167
171
  // API 请求日志(由 CC_TOOL_LOG_API_REQUESTS=true 环境变量控制,默认关闭)
168
172
  app.use('/api', createApiRequestLogger());
169
173
 
174
+ if (lanMode) {
175
+ app.use('/api', createRemoteMutationGuard({
176
+ enabled: true,
177
+ allowRemoteMutation,
178
+ message: '出于安全考虑,LAN 模式默认仅允许本机执行写操作。可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 覆盖。'
179
+ }));
180
+
181
+ }
182
+
170
183
  // API Routes
171
184
  app.use('/api/projects', require('./api/projects')(config));
172
185
  app.use('/api/sessions', require('./api/sessions')(config));
@@ -189,6 +202,14 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
189
202
  app.use('/api/opencode/proxy', require('./api/opencode-proxy'));
190
203
  app.use('/api/opencode/statistics', require('./api/opencode-statistics'));
191
204
 
205
+ // OMP API Routes
206
+ app.use('/api/pi/projects', require('./api/pi-projects')(config));
207
+ app.use('/api/pi/sessions', require('./api/pi-sessions')(config));
208
+ app.use('/api/pi/channels', require('./api/pi-channels')(config));
209
+ app.use('/api/pi/proxy', require('./api/pi-proxy'));
210
+ app.use('/api/pi/statistics', require('./api/pi-statistics'));
211
+ app.use('/api/pi/config', require('./api/pi-config'));
212
+
192
213
  app.use('/api/aliases', require('./api/aliases')());
193
214
  app.use('/api/favorites', require('./api/favorites'));
194
215
  app.use('/api/ui-config', require('./api/ui-config'));
@@ -304,6 +325,9 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
304
325
  console.log(chalk.gray(' [TIP] 首次访问自签名证书时,浏览器可能会提示手动信任本地证书'));
305
326
  }
306
327
 
328
+ if (host === '0.0.0.0' && !allowRemoteMutation) {
329
+ console.log(chalk.yellow(' [LOCK] 已启用 LAN 安全保护:远程写操作默认禁用'));
330
+ }
307
331
  // 自动恢复代理状态
308
332
  autoRestoreProxies();
309
333
 
@@ -425,6 +449,23 @@ function autoRestoreProxies() {
425
449
  console.error(chalk.red(`[ERROR] OpenCode 代理启动失败: ${err.message}`));
426
450
  });
427
451
  }
452
+
453
+ // 检查 OMP 受管 provider 配置状态文件
454
+ const piActiveFile = PATHS.activeChannel.pi;
455
+ if (fs.existsSync(piActiveFile)) {
456
+ console.log(chalk.cyan('\n[SYNC] 检测到 OMP 受管渠道状态文件,正在自动恢复...'));
457
+ startPiProxyServer({ preserveStartTime: true })
458
+ .then((result) => {
459
+ if (result.success) {
460
+ console.log(chalk.green(`[OK] OMP models.yml 受管 provider 已自动启用,端口: ${result.port}`));
461
+ } else {
462
+ console.error(chalk.red(`[ERROR] OMP models.yml 受管 provider 恢复失败: ${result.error || 'Unknown error'}`));
463
+ }
464
+ })
465
+ .catch((err) => {
466
+ console.error(chalk.red(`[ERROR] OMP models.yml 受管 provider 恢复失败: ${err.message}`));
467
+ });
468
+ }
428
469
  }
429
470
 
430
471
  // 启动时执行健康检查