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.
- package/README.md +46 -33
- package/dist/web/assets/{Analytics-BuBlsbcI.js → Analytics-Dma_j2Rq.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DQczxuOX.js → ConfigTemplates-C49Y1PLH.js} +1 -1
- package/dist/web/assets/Home-DGGC8H4g.js +1 -0
- package/dist/web/assets/Home-ewgeIhF7.css +1 -0
- package/dist/web/assets/{PluginManager-C8gAlaps.js → PluginManager-B1ECTHyY.js} +1 -1
- package/dist/web/assets/{ProjectList-DL4JK6ci.css → ProjectList-B_L55-l9.css} +1 -1
- package/dist/web/assets/ProjectList-DyiGyT-H.js +1 -0
- package/dist/web/assets/SessionList-CH5GwwiJ.js +1 -0
- package/dist/web/assets/{SkillManager-BDbNbNkK.js → SkillManager-CRyVhjYQ.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DGGaGW2c.js → WorkspaceManager-CO9tclVt.js} +1 -1
- package/dist/web/assets/{icons-QFXzEgqM.js → icons-Dop5j7Bf.js} +1 -1
- package/dist/web/assets/index-CM26UrnJ.css +1 -0
- package/dist/web/assets/index-EQq56pRa.js +2 -0
- package/dist/web/assets/{naive-ui-kcWDeJwl.js → naive-ui-CVyvdmg1.js} +1 -1
- package/dist/web/index.html +4 -4
- package/package.json +2 -1
- package/src/commands/channels.js +85 -24
- package/src/commands/daemon.js +7 -4
- package/src/commands/doctor.js +17 -8
- package/src/commands/logs.js +48 -6
- package/src/commands/port-config.js +81 -75
- package/src/commands/proxy-control.js +52 -19
- package/src/commands/stats.js +37 -19
- package/src/commands/toggle-proxy.js +131 -53
- package/src/commands/ui.js +3 -4
- package/src/config/default.js +14 -3
- package/src/config/paths.js +99 -11
- package/src/index.js +11 -16
- package/src/server/api/commands.js +1 -1
- package/src/server/api/config-export.js +4 -2
- package/src/server/api/config-registry.js +40 -34
- package/src/server/api/config.js +2 -1
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/mcp.js +5 -3
- package/src/server/api/pi-channels.js +349 -0
- package/src/server/api/pi-config.js +164 -0
- package/src/server/api/pi-projects.js +76 -0
- package/src/server/api/pi-proxy.js +154 -0
- package/src/server/api/pi-sessions.js +334 -0
- package/src/server/api/pi-statistics.js +40 -0
- package/src/server/api/plugins.js +1 -1
- package/src/server/api/prompts.js +3 -2
- package/src/server/api/skills.js +1 -1
- package/src/server/api/workspaces.js +2 -2
- package/src/server/index.js +41 -0
- package/src/server/pi-proxy-server.js +66 -0
- package/src/server/services/agents-service.js +2 -1
- package/src/server/services/base/proxy-utils.js +1 -0
- package/src/server/services/channel-balance.js +151 -1
- package/src/server/services/channel-scheduler.js +7 -1
- package/src/server/services/commands-service.js +11 -2
- package/src/server/services/config-export-service.js +275 -18
- package/src/server/services/config-registry-service.js +20 -18
- package/src/server/services/config-sync-manager.js +115 -6
- package/src/server/services/config-sync-service.js +4 -2
- package/src/server/services/config-templates-service.js +183 -45
- package/src/server/services/mcp-service.js +217 -33
- package/src/server/services/pi-auth-providers.js +294 -0
- package/src/server/services/pi-channels.js +94 -0
- package/src/server/services/pi-config.js +287 -0
- package/src/server/services/pi-sessions.js +490 -0
- package/src/server/services/pi-settings-manager.js +571 -0
- package/src/server/services/pi-statistics-service.js +317 -0
- package/src/server/services/plugins-service.js +673 -11
- package/src/server/services/prompts-service.js +112 -7
- package/src/server/services/proxy-log-helper.js +1 -0
- package/src/server/services/proxy-runtime.js +5 -1
- package/src/server/services/skill-service.js +13 -3
- package/src/server/services/speed-test.js +9 -3
- package/src/server/services/ui-config.js +50 -12
- package/src/server/services/workspace-service.js +19 -5
- package/src/server/websocket-server.js +7 -1
- package/dist/web/assets/Home-BvAzzWnB.js +0 -1
- package/dist/web/assets/Home-CIua-b-o.css +0 -1
- package/dist/web/assets/ProjectList-nqgi6_cR.js +0 -1
- package/dist/web/assets/SessionList-D8AxMYy6.js +0 -1
- package/dist/web/assets/index-Dsh4Q5Cg.css +0 -1
- package/dist/web/assets/index-y-eEayxR.js +0 -2
- package/src/utils/cli-flags.js +0 -7
package/src/server/api/mcp.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
const express = require('express');
|
|
6
6
|
const router = express.Router();
|
|
7
7
|
const mcpService = require('../services/mcp-service');
|
|
8
|
+
const MCP_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'pi'];
|
|
9
|
+
const MCP_EXPORT_FORMATS = ['json', 'claude', 'codex', 'gemini', 'opencode', 'pi'];
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* GET /api/mcp/servers
|
|
@@ -177,7 +179,7 @@ router.post('/import/:platform', async (req, res) => {
|
|
|
177
179
|
try {
|
|
178
180
|
const { platform } = req.params;
|
|
179
181
|
|
|
180
|
-
if (!
|
|
182
|
+
if (!MCP_PLATFORMS.includes(platform)) {
|
|
181
183
|
return res.status(400).json({
|
|
182
184
|
success: false,
|
|
183
185
|
error: `无效的平台: ${platform}`
|
|
@@ -285,7 +287,7 @@ router.get('/export', (req, res) => {
|
|
|
285
287
|
try {
|
|
286
288
|
const format = req.query.format || 'json';
|
|
287
289
|
|
|
288
|
-
if (!
|
|
290
|
+
if (!MCP_EXPORT_FORMATS.includes(format)) {
|
|
289
291
|
return res.status(400).json({
|
|
290
292
|
success: false,
|
|
291
293
|
error: `无效的导出格式: ${format}`
|
|
@@ -314,7 +316,7 @@ router.get('/export/download', (req, res) => {
|
|
|
314
316
|
try {
|
|
315
317
|
const format = req.query.format || 'json';
|
|
316
318
|
|
|
317
|
-
if (!
|
|
319
|
+
if (!MCP_EXPORT_FORMATS.includes(format)) {
|
|
318
320
|
return res.status(400).json({
|
|
319
321
|
success: false,
|
|
320
322
|
error: `无效的导出格式: ${format}`
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getChannels,
|
|
5
|
+
createChannel,
|
|
6
|
+
updateChannel,
|
|
7
|
+
deleteChannel,
|
|
8
|
+
saveChannelOrder
|
|
9
|
+
} = require('../services/pi-channels');
|
|
10
|
+
const { isPiInstalled } = require('../services/pi-sessions');
|
|
11
|
+
const { getSchedulerState } = require('../services/channel-scheduler');
|
|
12
|
+
const { getChannelHealthStatus, resetChannelHealth } = require('../services/channel-health');
|
|
13
|
+
const { broadcastSchedulerState } = require('../websocket-server');
|
|
14
|
+
const {
|
|
15
|
+
testChannelSpeed,
|
|
16
|
+
sanitizeBatchConcurrency,
|
|
17
|
+
runWithConcurrencyLimit
|
|
18
|
+
} = require('../services/speed-test');
|
|
19
|
+
const {
|
|
20
|
+
fetchModelsFromProvider,
|
|
21
|
+
probeModelAvailability
|
|
22
|
+
} = require('../services/model-detector');
|
|
23
|
+
const {
|
|
24
|
+
findAuthProviderForKey,
|
|
25
|
+
getOmpAuthProviderSnapshot
|
|
26
|
+
} = require('../services/pi-auth-providers');
|
|
27
|
+
|
|
28
|
+
function uniqueModels(models = []) {
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const result = [];
|
|
31
|
+
models.forEach((model) => {
|
|
32
|
+
if (typeof model !== 'string') return;
|
|
33
|
+
const trimmed = model.trim();
|
|
34
|
+
if (!trimmed) return;
|
|
35
|
+
const key = trimmed.toLowerCase();
|
|
36
|
+
if (seen.has(key)) return;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
result.push(trimmed);
|
|
39
|
+
});
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveGatewaySourceType(channel) {
|
|
44
|
+
const value = String(channel?.gatewaySourceType || '').trim().toLowerCase();
|
|
45
|
+
if (value === 'claude') return 'claude';
|
|
46
|
+
if (value === 'codex') return 'codex';
|
|
47
|
+
if (value === 'gemini') return 'gemini';
|
|
48
|
+
if (value === 'opencode') return 'opencode';
|
|
49
|
+
if (value === 'openai_compatible') return 'openai_compatible';
|
|
50
|
+
return 'openai_compatible';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectPreferredModels(channel = {}) {
|
|
54
|
+
return uniqueModels([
|
|
55
|
+
channel.model,
|
|
56
|
+
channel.speedTestModel,
|
|
57
|
+
...(Array.isArray(channel.allowedModels) ? channel.allowedModels : []),
|
|
58
|
+
...(Array.isArray(channel.modelRedirects)
|
|
59
|
+
? channel.modelRedirects.flatMap(rule => [rule?.from, rule?.to])
|
|
60
|
+
: [])
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function attachAuthProvider(channel, snapshot) {
|
|
65
|
+
const authProvider = findAuthProviderForKey(channel.providerKey || channel.provider || channel.name, snapshot);
|
|
66
|
+
if (!authProvider) return channel;
|
|
67
|
+
return {
|
|
68
|
+
...channel,
|
|
69
|
+
ompAuthProvider: {
|
|
70
|
+
id: authProvider.id,
|
|
71
|
+
name: authProvider.name,
|
|
72
|
+
loggedIn: authProvider.loggedIn,
|
|
73
|
+
accountCount: authProvider.accountCount,
|
|
74
|
+
accounts: authProvider.accounts || [],
|
|
75
|
+
checked: authProvider.checked,
|
|
76
|
+
error: authProvider.error || null
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = () => {
|
|
82
|
+
router.get('/', (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
if (!isPiInstalled()) {
|
|
85
|
+
return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
|
|
86
|
+
}
|
|
87
|
+
const data = getChannels();
|
|
88
|
+
const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
|
|
89
|
+
const channels = (data.channels || []).map(ch => ({
|
|
90
|
+
...attachAuthProvider(ch, authSnapshot),
|
|
91
|
+
health: getChannelHealthStatus(ch.id, 'pi')
|
|
92
|
+
}));
|
|
93
|
+
res.json({ channels, installed: true });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error('[OMP Channels API] Failed to get channels:', err);
|
|
96
|
+
res.status(500).json({ error: err.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
router.get('/enabled', (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
if (!isPiInstalled()) {
|
|
103
|
+
return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
|
|
104
|
+
}
|
|
105
|
+
const data = getChannels();
|
|
106
|
+
const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
|
|
107
|
+
const channels = (data.channels || [])
|
|
108
|
+
.filter(ch => ch.enabled !== false)
|
|
109
|
+
.map(ch => ({ ...attachAuthProvider(ch, authSnapshot), health: getChannelHealthStatus(ch.id, 'pi') }));
|
|
110
|
+
res.json({ channels, installed: true });
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('[OMP Channels API] Failed to get enabled channels:', err);
|
|
113
|
+
res.status(500).json({ error: err.message });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
router.post('/probe-models', async (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const { baseUrl, apiKey, gatewaySourceType } = req.body || {};
|
|
120
|
+
if (!baseUrl) {
|
|
121
|
+
return res.status(400).json({ error: 'baseUrl is required' });
|
|
122
|
+
}
|
|
123
|
+
const tempChannel = { baseUrl, apiKey: apiKey || '', gatewaySourceType: gatewaySourceType || 'openai_compatible' };
|
|
124
|
+
const resolvedGatewaySourceType = resolveGatewaySourceType(tempChannel);
|
|
125
|
+
const modelListSourceType = resolvedGatewaySourceType === 'opencode' ? 'openai_compatible' : resolvedGatewaySourceType;
|
|
126
|
+
const listResult = await fetchModelsFromProvider(tempChannel, modelListSourceType, {
|
|
127
|
+
useV1ModelsEndpoint: true,
|
|
128
|
+
forceRefresh: true
|
|
129
|
+
});
|
|
130
|
+
const models = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
|
|
131
|
+
res.json({
|
|
132
|
+
models,
|
|
133
|
+
supported: models.length > 0,
|
|
134
|
+
error: models.length > 0 ? null : (listResult.error || '未返回可用模型列表'),
|
|
135
|
+
errorHint: models.length > 0 ? null : (listResult.errorHint || '请手动填写模型名称')
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('[OMP Channels API] Error probing models:', error);
|
|
139
|
+
res.status(500).json({ error: 'Failed to probe models' });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
router.get('/:channelId/models', async (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const { channelId } = req.params;
|
|
146
|
+
const channels = getChannels().channels || [];
|
|
147
|
+
const channel = channels.find(ch => ch.id === channelId);
|
|
148
|
+
if (!channel) {
|
|
149
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
150
|
+
}
|
|
151
|
+
const forceRefresh = req.query.forceRefresh === 'true';
|
|
152
|
+
const gatewaySourceType = resolveGatewaySourceType(channel);
|
|
153
|
+
const modelListSourceType = gatewaySourceType === 'opencode' ? 'openai_compatible' : gatewaySourceType;
|
|
154
|
+
const listResult = await fetchModelsFromProvider(channel, modelListSourceType, {
|
|
155
|
+
useV1ModelsEndpoint: true,
|
|
156
|
+
forceRefresh
|
|
157
|
+
});
|
|
158
|
+
let models = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
|
|
159
|
+
let error = models.length > 0 ? null : listResult.error;
|
|
160
|
+
let errorHint = models.length > 0 ? null : listResult.errorHint;
|
|
161
|
+
|
|
162
|
+
if (models.length === 0) {
|
|
163
|
+
const probe = await probeModelAvailability(channel, gatewaySourceType, {
|
|
164
|
+
stopOnFirstAvailable: false,
|
|
165
|
+
preferredModels: collectPreferredModels(channel)
|
|
166
|
+
});
|
|
167
|
+
models = Array.isArray(probe.availableModels) ? uniqueModels(probe.availableModels) : [];
|
|
168
|
+
error = models.length > 0 ? null : (error || '无法获取可用模型');
|
|
169
|
+
errorHint = models.length > 0 ? '模型列表接口不可用,已自动切换为模型探测结果' : (errorHint || '请手动填写模型名称');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
res.json({
|
|
173
|
+
channelId,
|
|
174
|
+
gatewaySourceType,
|
|
175
|
+
models,
|
|
176
|
+
supported: models.length > 0,
|
|
177
|
+
cached: !!listResult.cached,
|
|
178
|
+
fallbackUsed: false,
|
|
179
|
+
fetchedAt: listResult.lastChecked || new Date().toISOString(),
|
|
180
|
+
error,
|
|
181
|
+
errorHint
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('[OMP Channels API] Error fetching models:', error);
|
|
185
|
+
res.status(500).json({ error: 'Failed to fetch model list', channelId: req.params.channelId });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
router.post('/', (req, res) => {
|
|
190
|
+
try {
|
|
191
|
+
const {
|
|
192
|
+
name,
|
|
193
|
+
baseUrl,
|
|
194
|
+
apiKey,
|
|
195
|
+
wireApi,
|
|
196
|
+
enabled,
|
|
197
|
+
weight,
|
|
198
|
+
maxConcurrency,
|
|
199
|
+
model,
|
|
200
|
+
gatewaySourceType,
|
|
201
|
+
modelRedirects,
|
|
202
|
+
allowedModels,
|
|
203
|
+
speedTestModel,
|
|
204
|
+
providerKey,
|
|
205
|
+
providerApi,
|
|
206
|
+
presetId,
|
|
207
|
+
websiteUrl,
|
|
208
|
+
balanceToken,
|
|
209
|
+
balanceUserId
|
|
210
|
+
} = req.body || {};
|
|
211
|
+
|
|
212
|
+
if (!name || !baseUrl) {
|
|
213
|
+
return res.status(400).json({ error: 'Missing required fields: name and baseUrl' });
|
|
214
|
+
}
|
|
215
|
+
if (!apiKey) {
|
|
216
|
+
return res.status(400).json({ error: 'API Key is required' });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const channel = createChannel(name, baseUrl, apiKey, {
|
|
220
|
+
wireApi: wireApi || 'openai',
|
|
221
|
+
providerApi: providerApi || wireApi || 'openai-completions',
|
|
222
|
+
providerKey,
|
|
223
|
+
enabled,
|
|
224
|
+
weight,
|
|
225
|
+
maxConcurrency,
|
|
226
|
+
model,
|
|
227
|
+
gatewaySourceType: gatewaySourceType || 'openai_compatible',
|
|
228
|
+
modelRedirects: modelRedirects || [],
|
|
229
|
+
allowedModels: Array.isArray(allowedModels) ? allowedModels : [],
|
|
230
|
+
speedTestModel: speedTestModel || null,
|
|
231
|
+
presetId,
|
|
232
|
+
websiteUrl: websiteUrl || '',
|
|
233
|
+
balanceToken: balanceToken || '',
|
|
234
|
+
balanceUserId: balanceUserId || null
|
|
235
|
+
});
|
|
236
|
+
res.json(channel);
|
|
237
|
+
broadcastSchedulerState('pi', getSchedulerState('pi'));
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error('[OMP Channels API] Failed to create channel:', err);
|
|
240
|
+
res.status(500).json({ error: err.message });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
router.put('/:channelId', (req, res) => {
|
|
245
|
+
try {
|
|
246
|
+
const channel = updateChannel(req.params.channelId, req.body || {});
|
|
247
|
+
res.json(channel);
|
|
248
|
+
broadcastSchedulerState('pi', getSchedulerState('pi'));
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error('[OMP Channels API] Failed to update channel:', err);
|
|
251
|
+
res.status(500).json({ error: err.message });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
router.delete('/:channelId', async (req, res) => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await deleteChannel(req.params.channelId);
|
|
258
|
+
res.json(result);
|
|
259
|
+
broadcastSchedulerState('pi', getSchedulerState('pi'));
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error('[OMP Channels API] Failed to delete channel:', err);
|
|
262
|
+
res.status(500).json({ error: err.message });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
router.post('/order', (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
const { order } = req.body || {};
|
|
269
|
+
if (!Array.isArray(order)) {
|
|
270
|
+
return res.status(400).json({ error: 'Order must be an array' });
|
|
271
|
+
}
|
|
272
|
+
saveChannelOrder(order);
|
|
273
|
+
res.json({ success: true });
|
|
274
|
+
broadcastSchedulerState('pi', getSchedulerState('pi'));
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('[OMP Channels API] Failed to save order:', err);
|
|
277
|
+
res.status(500).json({ error: err.message });
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
router.post('/:channelId/reset-health', (req, res) => {
|
|
282
|
+
try {
|
|
283
|
+
resetChannelHealth(req.params.channelId, 'pi');
|
|
284
|
+
res.json({ success: true });
|
|
285
|
+
broadcastSchedulerState('pi', getSchedulerState('pi'));
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error('[OMP Channels API] Failed to reset health:', err);
|
|
288
|
+
res.status(500).json({ error: err.message });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
router.post('/:channelId/speed-test', async (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const channel = (getChannels().channels || []).find(ch => ch.id === req.params.channelId);
|
|
295
|
+
if (!channel) {
|
|
296
|
+
return res.status(404).json({ error: 'Channel not found' });
|
|
297
|
+
}
|
|
298
|
+
const result = await testChannelSpeed(channel, req.body?.timeout || 20000, resolveGatewaySourceType(channel), {
|
|
299
|
+
authSourceType: 'pi'
|
|
300
|
+
});
|
|
301
|
+
res.json(result);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('[OMP Channels API] Speed test failed:', error);
|
|
304
|
+
res.status(500).json({ error: error.message });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
router.post('/speed-test-all', async (req, res) => {
|
|
309
|
+
try {
|
|
310
|
+
const { timeout = 20000, concurrency } = req.body || {};
|
|
311
|
+
const channels = getChannels().channels || [];
|
|
312
|
+
const safeConcurrency = sanitizeBatchConcurrency(concurrency);
|
|
313
|
+
const results = await runWithConcurrencyLimit(
|
|
314
|
+
channels,
|
|
315
|
+
safeConcurrency,
|
|
316
|
+
channel => testChannelSpeed(channel, timeout, resolveGatewaySourceType(channel), {
|
|
317
|
+
authSourceType: 'pi'
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
results.sort((a, b) => {
|
|
321
|
+
if (a.success && !b.success) return -1;
|
|
322
|
+
if (!a.success && b.success) return 1;
|
|
323
|
+
if (a.success && b.success) {
|
|
324
|
+
return ((a.latency ?? Infinity) - (b.latency ?? Infinity));
|
|
325
|
+
}
|
|
326
|
+
return 0;
|
|
327
|
+
});
|
|
328
|
+
const successResults = results.filter(r => r.success);
|
|
329
|
+
const successWithLatency = successResults.filter(r => r.latency !== null && r.latency !== undefined);
|
|
330
|
+
res.json({
|
|
331
|
+
results,
|
|
332
|
+
summary: {
|
|
333
|
+
total: results.length,
|
|
334
|
+
success: successResults.length,
|
|
335
|
+
failed: results.length - successResults.length,
|
|
336
|
+
avgLatency: successWithLatency.length > 0
|
|
337
|
+
? Math.round(successWithLatency.reduce((sum, r) => sum + r.latency, 0) / successWithLatency.length)
|
|
338
|
+
: null,
|
|
339
|
+
concurrency: safeConcurrency
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error('[OMP Channels API] Speed test all failed:', error);
|
|
344
|
+
res.status(500).json({ error: error.message });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return router;
|
|
349
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const {
|
|
6
|
+
getPiPaths,
|
|
7
|
+
getPiStatus,
|
|
8
|
+
readJsonFile,
|
|
9
|
+
readYamlFile,
|
|
10
|
+
readPiSettings
|
|
11
|
+
} = require('../services/pi-config');
|
|
12
|
+
const {
|
|
13
|
+
getOmpAuthProviderSnapshot
|
|
14
|
+
} = require('../services/pi-auth-providers');
|
|
15
|
+
|
|
16
|
+
function listDirEntries(dirPath, options = {}) {
|
|
17
|
+
const { extensions = null, includeDirectories = true } = options;
|
|
18
|
+
if (!fs.existsSync(dirPath)) return [];
|
|
19
|
+
try {
|
|
20
|
+
return fs.readdirSync(dirPath, { withFileTypes: true })
|
|
21
|
+
.filter(entry => !entry.name.startsWith('.'))
|
|
22
|
+
.filter((entry) => {
|
|
23
|
+
if (entry.isDirectory()) return includeDirectories;
|
|
24
|
+
if (!Array.isArray(extensions) || extensions.length === 0) return true;
|
|
25
|
+
return extensions.includes(path.extname(entry.name).toLowerCase());
|
|
26
|
+
})
|
|
27
|
+
.map((entry) => {
|
|
28
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
29
|
+
const stat = fs.statSync(fullPath);
|
|
30
|
+
return {
|
|
31
|
+
name: entry.name,
|
|
32
|
+
type: entry.isDirectory() ? 'directory' : 'file',
|
|
33
|
+
path: fullPath,
|
|
34
|
+
size: stat.size,
|
|
35
|
+
updatedAt: new Date(stat.mtimeMs).toISOString()
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeArray(value) {
|
|
44
|
+
return Array.isArray(value) ? value : [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildCapabilities() {
|
|
48
|
+
return {
|
|
49
|
+
platform: 'pi',
|
|
50
|
+
runtime: 'omp',
|
|
51
|
+
native: {
|
|
52
|
+
config: true,
|
|
53
|
+
settings: true,
|
|
54
|
+
models: true,
|
|
55
|
+
commands: true,
|
|
56
|
+
notes: true,
|
|
57
|
+
extensions: true,
|
|
58
|
+
skills: true,
|
|
59
|
+
promptTemplates: true,
|
|
60
|
+
packages: true,
|
|
61
|
+
rpc: true,
|
|
62
|
+
sessions: true
|
|
63
|
+
},
|
|
64
|
+
mapped: {
|
|
65
|
+
command: 'commands/prompts',
|
|
66
|
+
plugin: 'packages/extensions',
|
|
67
|
+
mcp: 'mcp.json',
|
|
68
|
+
agent: 'OMP discovery capability'
|
|
69
|
+
},
|
|
70
|
+
writable: {
|
|
71
|
+
config: true,
|
|
72
|
+
models: true,
|
|
73
|
+
commands: true,
|
|
74
|
+
notes: true,
|
|
75
|
+
settings: true,
|
|
76
|
+
skills: true,
|
|
77
|
+
promptTemplates: true,
|
|
78
|
+
packages: true,
|
|
79
|
+
extensions: true,
|
|
80
|
+
mcp: true,
|
|
81
|
+
agent: false
|
|
82
|
+
},
|
|
83
|
+
notes: [
|
|
84
|
+
'The pi channel is backed by OMP. coding-tool-x keeps the pi route/key for compatibility.',
|
|
85
|
+
'Provider channels are enabled by merging managed ctx-* providers into OMP models.yml.'
|
|
86
|
+
]
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
router.get('/', (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
const paths = getPiPaths();
|
|
93
|
+
const settings = readPiSettings();
|
|
94
|
+
const resources = {
|
|
95
|
+
config: readYamlFile(paths.config, {}),
|
|
96
|
+
settings: readYamlFile(paths.settings, {}),
|
|
97
|
+
auth: fs.existsSync(paths.auth) ? { exists: true, path: paths.auth } : { exists: false, path: paths.auth },
|
|
98
|
+
authProviders: getOmpAuthProviderSnapshot({ accountCheck: false }),
|
|
99
|
+
models: readYamlFile(paths.modelsYml, {}),
|
|
100
|
+
legacyModels: readJsonFile(paths.modelsJsonLegacy, {}),
|
|
101
|
+
packages: normalizeArray(settings.packages),
|
|
102
|
+
disabledPackages: normalizeArray(settings.disabledPackages),
|
|
103
|
+
skills: listDirEntries(paths.skills),
|
|
104
|
+
prompts: listDirEntries(paths.prompts, { extensions: ['.md'], includeDirectories: true }),
|
|
105
|
+
commands: listDirEntries(paths.commands, { extensions: ['.md'], includeDirectories: true }),
|
|
106
|
+
notes: listDirEntries(paths.notes, { extensions: ['.md'], includeDirectories: true }),
|
|
107
|
+
extensions: listDirEntries(paths.extensions, { extensions: ['.js', '.mjs', '.cjs', '.ts'], includeDirectories: true }),
|
|
108
|
+
sessionsDir: paths.sessions
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
res.json({
|
|
112
|
+
success: true,
|
|
113
|
+
status: getPiStatus(),
|
|
114
|
+
capabilities: buildCapabilities(),
|
|
115
|
+
resources
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('[OMP Config API] Failed to read config:', error);
|
|
119
|
+
res.status(500).json({ error: error.message });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
router.get('/capabilities', (req, res) => {
|
|
124
|
+
res.json({
|
|
125
|
+
success: true,
|
|
126
|
+
platform: 'pi',
|
|
127
|
+
capabilities: buildCapabilities()
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
router.get('/auth-providers', (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const forceRefresh = req.query.forceRefresh === 'true';
|
|
134
|
+
res.json({
|
|
135
|
+
success: true,
|
|
136
|
+
...getOmpAuthProviderSnapshot({ forceRefresh })
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('[OMP Config API] Failed to list auth providers:', error);
|
|
140
|
+
res.status(500).json({ error: error.message });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
router.get('/resources', (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const paths = getPiPaths();
|
|
147
|
+
const settings = readPiSettings();
|
|
148
|
+
res.json({
|
|
149
|
+
success: true,
|
|
150
|
+
paths,
|
|
151
|
+
packages: normalizeArray(settings.packages),
|
|
152
|
+
skills: listDirEntries(paths.skills),
|
|
153
|
+
prompts: listDirEntries(paths.prompts, { extensions: ['.md'], includeDirectories: true }),
|
|
154
|
+
commands: listDirEntries(paths.commands, { extensions: ['.md'], includeDirectories: true }),
|
|
155
|
+
notes: listDirEntries(paths.notes, { extensions: ['.md'], includeDirectories: true }),
|
|
156
|
+
extensions: listDirEntries(paths.extensions, { extensions: ['.js', '.mjs', '.cjs', '.ts'], includeDirectories: true })
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('[OMP Config API] Failed to list resources:', error);
|
|
160
|
+
res.status(500).json({ error: error.message });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
module.exports = router;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getProjects,
|
|
5
|
+
saveProjectOrder,
|
|
6
|
+
deleteProject,
|
|
7
|
+
isPiInstalled
|
|
8
|
+
} = require('../services/pi-sessions');
|
|
9
|
+
|
|
10
|
+
function isNotFoundError(error) {
|
|
11
|
+
return !!(error && error.message === 'Project not found');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = () => {
|
|
15
|
+
router.get('/', (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
if (!isPiInstalled()) {
|
|
18
|
+
return res.json({
|
|
19
|
+
projects: [],
|
|
20
|
+
currentProject: null,
|
|
21
|
+
error: 'OMP CLI not installed or not found'
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const projects = getProjects();
|
|
26
|
+
res.json({
|
|
27
|
+
projects,
|
|
28
|
+
currentProject: projects[0] ? projects[0].name : null
|
|
29
|
+
});
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('[OMP API] Failed to get projects:', err);
|
|
32
|
+
if (err.code === 'ENOENT') {
|
|
33
|
+
return res.status(404).json({
|
|
34
|
+
error: 'OMP data directory not found',
|
|
35
|
+
projects: []
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
res.status(500).json({ error: err.message, projects: [] });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
router.post('/order', (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
if (!isPiInstalled()) {
|
|
45
|
+
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
46
|
+
}
|
|
47
|
+
const { order } = req.body || {};
|
|
48
|
+
if (!Array.isArray(order)) {
|
|
49
|
+
return res.status(400).json({ error: 'order must be an array' });
|
|
50
|
+
}
|
|
51
|
+
saveProjectOrder(order);
|
|
52
|
+
res.json({ success: true });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error('[OMP API] Failed to save project order:', err);
|
|
55
|
+
res.status(500).json({ error: err.message });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
router.delete('/:projectName', (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
if (!isPiInstalled()) {
|
|
62
|
+
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
63
|
+
}
|
|
64
|
+
const result = deleteProject(req.params.projectName);
|
|
65
|
+
res.json(result);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (isNotFoundError(err)) {
|
|
68
|
+
return res.status(404).json({ error: err.message });
|
|
69
|
+
}
|
|
70
|
+
console.error('[OMP API] Failed to delete project:', err);
|
|
71
|
+
res.status(500).json({ error: err.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return router;
|
|
76
|
+
};
|