@ejazullah/browser-mcp 0.0.56
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/LICENSE +202 -0
- package/README.md +860 -0
- package/cli.js +19 -0
- package/index.d.ts +23 -0
- package/index.js +1061 -0
- package/lib/auth.js +82 -0
- package/lib/browserContextFactory.js +205 -0
- package/lib/browserServerBackend.js +125 -0
- package/lib/config.js +266 -0
- package/lib/context.js +232 -0
- package/lib/databaseLogger.js +264 -0
- package/lib/extension/cdpRelay.js +346 -0
- package/lib/extension/extensionContextFactory.js +56 -0
- package/lib/extension/main.js +26 -0
- package/lib/fileUtils.js +32 -0
- package/lib/httpServer.js +39 -0
- package/lib/index.js +39 -0
- package/lib/javascript.js +49 -0
- package/lib/log.js +21 -0
- package/lib/loop/loop.js +69 -0
- package/lib/loop/loopClaude.js +152 -0
- package/lib/loop/loopOpenAI.js +143 -0
- package/lib/loop/main.js +60 -0
- package/lib/loopTools/context.js +66 -0
- package/lib/loopTools/main.js +49 -0
- package/lib/loopTools/perform.js +32 -0
- package/lib/loopTools/snapshot.js +29 -0
- package/lib/loopTools/tool.js +18 -0
- package/lib/manualPromise.js +111 -0
- package/lib/mcp/inProcessTransport.js +72 -0
- package/lib/mcp/server.js +93 -0
- package/lib/mcp/transport.js +217 -0
- package/lib/mongoDBLogger.js +252 -0
- package/lib/package.js +20 -0
- package/lib/program.js +113 -0
- package/lib/response.js +172 -0
- package/lib/sessionLog.js +156 -0
- package/lib/tab.js +266 -0
- package/lib/tools/cdp.js +169 -0
- package/lib/tools/common.js +55 -0
- package/lib/tools/console.js +33 -0
- package/lib/tools/dialogs.js +47 -0
- package/lib/tools/evaluate.js +53 -0
- package/lib/tools/extraction.js +217 -0
- package/lib/tools/files.js +44 -0
- package/lib/tools/forms.js +180 -0
- package/lib/tools/getext.js +99 -0
- package/lib/tools/install.js +53 -0
- package/lib/tools/interactions.js +191 -0
- package/lib/tools/keyboard.js +86 -0
- package/lib/tools/mouse.js +99 -0
- package/lib/tools/navigate.js +70 -0
- package/lib/tools/network.js +41 -0
- package/lib/tools/pdf.js +40 -0
- package/lib/tools/screenshot.js +75 -0
- package/lib/tools/selectors.js +233 -0
- package/lib/tools/snapshot.js +169 -0
- package/lib/tools/states.js +147 -0
- package/lib/tools/tabs.js +87 -0
- package/lib/tools/tool.js +33 -0
- package/lib/tools/utils.js +74 -0
- package/lib/tools/wait.js +56 -0
- package/lib/tools.js +64 -0
- package/lib/utils.js +26 -0
- package/openapi.json +683 -0
- package/package.json +92 -0
package/index.js
ADDED
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createConnection } from './lib/index.js';
|
|
19
|
+
export { createConnection };
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// main.js (CommonJS) - Router Module
|
|
23
|
+
const BASE_SYSTEM_PROMPT = `
|
|
24
|
+
You are a STRICT browser automation agent.
|
|
25
|
+
|
|
26
|
+
You have access ONLY to browser-related tools.
|
|
27
|
+
You do NOT have access to:
|
|
28
|
+
- filesystem tools
|
|
29
|
+
- shell commands
|
|
30
|
+
- terminal access
|
|
31
|
+
- code execution
|
|
32
|
+
- explanations or suggestions outside tools
|
|
33
|
+
|
|
34
|
+
ABSOLUTE RULES (MANDATORY):
|
|
35
|
+
1. NEVER explain limitations or apologize.
|
|
36
|
+
2. NEVER suggest shell commands, CLI commands, or external solutions.
|
|
37
|
+
3. NEVER say "I cannot do this" without attempting browser actions.
|
|
38
|
+
4. NEVER stop unless the task is fully completed or explicitly impossible via browser.
|
|
39
|
+
|
|
40
|
+
EXECUTION LOOP (STRICT):
|
|
41
|
+
1. Perform ONE browser tool action.
|
|
42
|
+
2. Immediately take a browser_snapshot.
|
|
43
|
+
3. Analyze ONLY the browser_snapshot.
|
|
44
|
+
4. Decide the next browser action.
|
|
45
|
+
|
|
46
|
+
If a task is impossible using browser tools:
|
|
47
|
+
- State ONLY: "Task cannot be completed using browser tools."
|
|
48
|
+
- STOP. No explanation. No alternatives.
|
|
49
|
+
|
|
50
|
+
You MUST follow:
|
|
51
|
+
tool → browser_snapshot → analysis → next action
|
|
52
|
+
|
|
53
|
+
Breaking these rules is considered FAILURE.
|
|
54
|
+
`
|
|
55
|
+
async function createRouter() {
|
|
56
|
+
// Import ES modules dynamically
|
|
57
|
+
const express = await import('express');
|
|
58
|
+
const {
|
|
59
|
+
SmartBrowserAutomation,
|
|
60
|
+
HuggingFaceConfig,
|
|
61
|
+
OllamaConfig,
|
|
62
|
+
} = await import('@ejazullah/smart-browser-automation');
|
|
63
|
+
|
|
64
|
+
const router = express.default.Router();
|
|
65
|
+
|
|
66
|
+
// Store active sessions
|
|
67
|
+
const activeSessions = new Map();
|
|
68
|
+
|
|
69
|
+
// Store running jobs
|
|
70
|
+
const runningJobs = new Map();
|
|
71
|
+
|
|
72
|
+
// Store SSE clients for real-time updates
|
|
73
|
+
const sseClients = new Map(); // jobId -> Set of response objects
|
|
74
|
+
|
|
75
|
+
// Job status enum
|
|
76
|
+
const JobStatus = {
|
|
77
|
+
PENDING: 'pending',
|
|
78
|
+
RUNNING: 'running',
|
|
79
|
+
COMPLETED: 'completed',
|
|
80
|
+
FAILED: 'failed',
|
|
81
|
+
CANCELLED: 'cancelled'
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Generate unique job ID
|
|
85
|
+
function generateJobId() {
|
|
86
|
+
return `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// SSE utility functions
|
|
90
|
+
function sendSSEMessage(jobId, type, data) {
|
|
91
|
+
if (sseClients.has(jobId)) {
|
|
92
|
+
const clients = sseClients.get(jobId);
|
|
93
|
+
const message = `data: ${JSON.stringify({ type, data, timestamp: new Date().toISOString() })}\n\n`;
|
|
94
|
+
|
|
95
|
+
for (const res of clients) {
|
|
96
|
+
try {
|
|
97
|
+
res.write(message);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error(`Error sending SSE message to client:`, error);
|
|
100
|
+
clients.delete(res);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Clean up empty client sets
|
|
105
|
+
if (clients.size === 0) {
|
|
106
|
+
sseClients.delete(jobId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addSSEClient(jobId, res) {
|
|
112
|
+
if (!sseClients.has(jobId)) {
|
|
113
|
+
sseClients.set(jobId, new Set());
|
|
114
|
+
}
|
|
115
|
+
sseClients.get(jobId).add(res);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function removeSSEClient(jobId, res) {
|
|
119
|
+
if (sseClients.has(jobId)) {
|
|
120
|
+
const clients = sseClients.get(jobId);
|
|
121
|
+
clients.delete(res);
|
|
122
|
+
if (clients.size === 0) {
|
|
123
|
+
sseClients.delete(jobId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Session management utilities
|
|
129
|
+
async function createBrowserSession(sessionName = "Smart Browser Automation - CommonJS Session") {
|
|
130
|
+
console.log("🚀 Creating new browser session...");
|
|
131
|
+
|
|
132
|
+
const sessionResponse = await fetch('https://gridnew.doingerp.com/wd/hub/session', {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json'
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
capabilities: {
|
|
139
|
+
alwaysMatch: {
|
|
140
|
+
browserName: "chrome",
|
|
141
|
+
browserVersion: "128.0",
|
|
142
|
+
"selenoid:options": {
|
|
143
|
+
name: sessionName,
|
|
144
|
+
sessionTimeout: "10m",
|
|
145
|
+
enableVNC: true,
|
|
146
|
+
},
|
|
147
|
+
"goog:chromeOptions": {
|
|
148
|
+
"args": ["--start-maximized"]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!sessionResponse.ok) {
|
|
156
|
+
throw new Error(`Failed to create session: ${sessionResponse.statusText}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sessionData = await sessionResponse.json();
|
|
160
|
+
return sessionData.value.sessionId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function deleteBrowserSession(sessionId) {
|
|
164
|
+
try {
|
|
165
|
+
await fetch(`https://gridnew.doingerp.com/wd/hub/session/${sessionId}`, {
|
|
166
|
+
method: 'DELETE'
|
|
167
|
+
});
|
|
168
|
+
console.log(`✅ Session ${sessionId} deleted`);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.log(`⚠️ Session cleanup failed for ${sessionId}:`, error.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Configuration factory
|
|
175
|
+
function createLLMConfig(config = {}) {
|
|
176
|
+
// const {
|
|
177
|
+
// provider = 'ollama',
|
|
178
|
+
// baseUrl = "https://api.z.ai/api/paas/v4",
|
|
179
|
+
// model = "glm-4.5-flash",
|
|
180
|
+
// apiKey = '27a8eae5c4fa43f4949a3e9c74032d8d.twI60j34Bjdsq41V',
|
|
181
|
+
// temperature = 0.7,
|
|
182
|
+
// topP = 0.9,
|
|
183
|
+
// topK = 20,
|
|
184
|
+
// minP = 0.0
|
|
185
|
+
// } = config;
|
|
186
|
+
const {
|
|
187
|
+
provider = 'ollama',
|
|
188
|
+
baseUrl = "http://173.208.155.135:11434/v1",
|
|
189
|
+
model = "gpt-oss:20b",
|
|
190
|
+
apiKey = null,
|
|
191
|
+
temperature = 0.7,
|
|
192
|
+
topP = 0.9,
|
|
193
|
+
topK = 20,
|
|
194
|
+
minP = 0.0
|
|
195
|
+
} = config;
|
|
196
|
+
|
|
197
|
+
switch (provider.toLowerCase()) {
|
|
198
|
+
case 'huggingface':
|
|
199
|
+
return new HuggingFaceConfig(apiKey, model, {
|
|
200
|
+
temperature,
|
|
201
|
+
top_p: topP,
|
|
202
|
+
top_k: topK,
|
|
203
|
+
min_p: minP
|
|
204
|
+
});
|
|
205
|
+
case 'ollama':
|
|
206
|
+
default:
|
|
207
|
+
return new OllamaConfig(baseUrl, model, apiKey, 'openai', {
|
|
208
|
+
temperature,
|
|
209
|
+
top_p: topP,
|
|
210
|
+
top_k: topK,
|
|
211
|
+
min_p: minP
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Routes
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* POST /automation/execute
|
|
220
|
+
* Execute a browser automation task (asynchronous with job tracking)
|
|
221
|
+
*/
|
|
222
|
+
router.post('/automation/execute', async (req, res) => {
|
|
223
|
+
const jobId = generateJobId();
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const {
|
|
228
|
+
task,
|
|
229
|
+
config = {},
|
|
230
|
+
maxSteps = 30,
|
|
231
|
+
temperature = 0.7,
|
|
232
|
+
verbose = true,
|
|
233
|
+
systemPrompt:BASE_SYSTEM_PROMPT,
|
|
234
|
+
sessionName,
|
|
235
|
+
async = true // New parameter to control async behavior
|
|
236
|
+
} = req.body;
|
|
237
|
+
|
|
238
|
+
if (!task) {
|
|
239
|
+
return res.status(400).json({
|
|
240
|
+
success: false,
|
|
241
|
+
error: "Task description is required"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Create job entry
|
|
246
|
+
const job = {
|
|
247
|
+
id: jobId,
|
|
248
|
+
status: JobStatus.PENDING,
|
|
249
|
+
task,
|
|
250
|
+
config,
|
|
251
|
+
maxSteps,
|
|
252
|
+
temperature,
|
|
253
|
+
verbose,
|
|
254
|
+
systemPrompt:BASE_SYSTEM_PROMPT,
|
|
255
|
+
sessionName,
|
|
256
|
+
createdAt: new Date(),
|
|
257
|
+
startedAt: null,
|
|
258
|
+
completedAt: null,
|
|
259
|
+
result: null,
|
|
260
|
+
error: null,
|
|
261
|
+
sessionId: null
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
runningJobs.set(jobId, job);
|
|
265
|
+
|
|
266
|
+
// If async mode, start job in background and return job ID immediately
|
|
267
|
+
if (async) {
|
|
268
|
+
// Start job in background
|
|
269
|
+
executeAutomationJob(jobId).catch(error => {
|
|
270
|
+
console.error(`❌ Background job ${jobId} failed:`, error);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return res.json({
|
|
274
|
+
success: true,
|
|
275
|
+
jobId,
|
|
276
|
+
message: "Job started successfully",
|
|
277
|
+
status: JobStatus.PENDING,
|
|
278
|
+
estimatedTime: "2-5 minutes",
|
|
279
|
+
statusUrl: `/v1/job/${jobId}`,
|
|
280
|
+
resultUrl: `/v1/job/${jobId}/result`
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Synchronous mode (for backward compatibility)
|
|
285
|
+
// Set a longer timeout for this request
|
|
286
|
+
req.setTimeout(10 * 60 * 1000); // 10 minutes
|
|
287
|
+
res.setTimeout(10 * 60 * 1000);
|
|
288
|
+
|
|
289
|
+
const result = await executeAutomationJob(jobId);
|
|
290
|
+
|
|
291
|
+
res.json({
|
|
292
|
+
success: true,
|
|
293
|
+
jobId,
|
|
294
|
+
sessionId: result.sessionId,
|
|
295
|
+
result: {
|
|
296
|
+
completed: result.completed,
|
|
297
|
+
steps: result.steps,
|
|
298
|
+
success: result.success,
|
|
299
|
+
results: result.results
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error("❌ Error:", error);
|
|
305
|
+
|
|
306
|
+
// Update job status
|
|
307
|
+
if (runningJobs.has(jobId)) {
|
|
308
|
+
const job = runningJobs.get(jobId);
|
|
309
|
+
job.status = JobStatus.FAILED;
|
|
310
|
+
job.error = error.message;
|
|
311
|
+
job.completedAt = new Date();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
res.status(500).json({
|
|
315
|
+
success: false,
|
|
316
|
+
error: error.message,
|
|
317
|
+
jobId,
|
|
318
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Async job execution function
|
|
324
|
+
async function executeAutomationJob(jobId) {
|
|
325
|
+
const job = runningJobs.get(jobId);
|
|
326
|
+
if (!job) {
|
|
327
|
+
throw new Error(`Job ${jobId} not found`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let sessionId = null;
|
|
331
|
+
let automation = null;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
// Update job status
|
|
335
|
+
job.status = JobStatus.RUNNING;
|
|
336
|
+
job.startedAt = new Date();
|
|
337
|
+
|
|
338
|
+
console.log(`🚀 Starting job ${jobId}: ${job.task.substring(0, 100)}...`);
|
|
339
|
+
sendSSEMessage(jobId, 'job_started', {
|
|
340
|
+
jobId,
|
|
341
|
+
message: `Starting job: ${job.task.substring(0, 100)}...`,
|
|
342
|
+
status: JobStatus.RUNNING
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Create browser session
|
|
346
|
+
sendSSEMessage(jobId, 'session_creating', {
|
|
347
|
+
message: 'Creating browser session...'
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
sessionId = await createBrowserSession(job.sessionName);
|
|
351
|
+
job.sessionId = sessionId;
|
|
352
|
+
console.log(`✅ Session created for job ${jobId}: ${sessionId}`);
|
|
353
|
+
|
|
354
|
+
sendSSEMessage(jobId, 'session_created', {
|
|
355
|
+
server: "https://gridnew.doingerp.com",
|
|
356
|
+
password: "selenoid",
|
|
357
|
+
sessionId,
|
|
358
|
+
message: `Browser session created: ${sessionId}`
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Configuration
|
|
362
|
+
const llmConfig = createLLMConfig(job.config);
|
|
363
|
+
const mcpEndpoint = job.config.mcpEndpoint || 'http://173.208.155.135:8931/sse';
|
|
364
|
+
const driverUrl = `wss://gridnew.doingerp.com/devtools/${sessionId}`;
|
|
365
|
+
|
|
366
|
+
// Create automation instance
|
|
367
|
+
automation = new SmartBrowserAutomation({
|
|
368
|
+
maxSteps: job.maxSteps,
|
|
369
|
+
temperature: job.temperature,
|
|
370
|
+
transportType: 'sse'
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
sendSSEMessage(jobId, 'automation_initializing', {
|
|
374
|
+
message: 'Initializing automation...'
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Initialize and execute
|
|
378
|
+
await automation.initialize(llmConfig, mcpEndpoint);
|
|
379
|
+
console.log(`✅ Automation initialized for job ${jobId}`);
|
|
380
|
+
const browserSetup = await automation.callTool("browser_connect_cdp", { endpoint: driverUrl });
|
|
381
|
+
|
|
382
|
+
sendSSEMessage(jobId, 'automation_initialized', {
|
|
383
|
+
message: 'Automation initialized successfully'
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Execute task with real-time updates
|
|
387
|
+
const result = await automation.executeTask(job.task, {
|
|
388
|
+
verbose: job.verbose,
|
|
389
|
+
systemPrompt: job.systemPrompt,
|
|
390
|
+
onProgress: async (progressData) => {
|
|
391
|
+
// Send real-time updates via SSE
|
|
392
|
+
sendSSEMessage(jobId, progressData.type, progressData);
|
|
393
|
+
|
|
394
|
+
// Also log to console for server-side monitoring
|
|
395
|
+
const time = new Date().toLocaleTimeString();
|
|
396
|
+
switch (progressData.type) {
|
|
397
|
+
case 'step_started':
|
|
398
|
+
console.log(`🚀 Step ${progressData.step}: Starting...`);
|
|
399
|
+
break;
|
|
400
|
+
case 'tool_call':
|
|
401
|
+
console.log(`🔧 LLM wants to use tool: ${progressData.toolName}`);
|
|
402
|
+
console.log(`📝 Tool arguments:`, progressData.toolArgs);
|
|
403
|
+
break;
|
|
404
|
+
case 'tool_result':
|
|
405
|
+
console.log(`✅ Tool executed successfully`);
|
|
406
|
+
break;
|
|
407
|
+
case 'tool_error':
|
|
408
|
+
console.error(`❌ Tool execution failed:`, progressData.error);
|
|
409
|
+
break;
|
|
410
|
+
case 'task_completed':
|
|
411
|
+
console.log(`🎯 Task completed! LLM response: ${progressData.response}`);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Update job with result
|
|
418
|
+
job.status = JobStatus.COMPLETED;
|
|
419
|
+
job.completedAt = new Date();
|
|
420
|
+
job.result = {
|
|
421
|
+
completed: result.completed,
|
|
422
|
+
steps: result.steps,
|
|
423
|
+
success: result.success,
|
|
424
|
+
results: result.results,
|
|
425
|
+
sessionId: sessionId
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
console.log(`🎉 Job ${jobId} completed successfully in ${job.result.steps} steps`);
|
|
429
|
+
|
|
430
|
+
sendSSEMessage(jobId, 'job_completed', {
|
|
431
|
+
jobId,
|
|
432
|
+
message: `Job completed successfully in ${job.result.steps} steps`,
|
|
433
|
+
status: JobStatus.COMPLETED,
|
|
434
|
+
result: job.result
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return job.result;
|
|
438
|
+
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error(`❌ Job ${jobId} failed:`, error);
|
|
441
|
+
|
|
442
|
+
// Update job with error
|
|
443
|
+
job.status = JobStatus.FAILED;
|
|
444
|
+
job.completedAt = new Date();
|
|
445
|
+
job.error = error.message;
|
|
446
|
+
job.result = {
|
|
447
|
+
success: false,
|
|
448
|
+
error: error.message,
|
|
449
|
+
sessionId: sessionId
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
sendSSEMessage(jobId, 'job_failed', {
|
|
453
|
+
jobId,
|
|
454
|
+
message: `Job failed: ${error.message}`,
|
|
455
|
+
status: JobStatus.FAILED,
|
|
456
|
+
error: error.message
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
throw error;
|
|
460
|
+
} finally {
|
|
461
|
+
// Clean up
|
|
462
|
+
if (automation) {
|
|
463
|
+
try {
|
|
464
|
+
await automation.close();
|
|
465
|
+
sendSSEMessage(jobId, 'automation_closed', {
|
|
466
|
+
message: 'Automation instance closed'
|
|
467
|
+
});
|
|
468
|
+
} catch (closeError) {
|
|
469
|
+
console.error(`Error closing automation for job ${jobId}:`, closeError);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (sessionId) {
|
|
473
|
+
await deleteBrowserSession(sessionId);
|
|
474
|
+
sendSSEMessage(jobId, 'session_deleted', {
|
|
475
|
+
sessionId,
|
|
476
|
+
message: 'Browser session cleaned up'
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Send final SSE message and clean up clients
|
|
481
|
+
sendSSEMessage(jobId, 'stream_ended', {
|
|
482
|
+
message: 'Real-time stream ended'
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Clean up SSE clients for this job after a delay
|
|
486
|
+
setTimeout(() => {
|
|
487
|
+
if (sseClients.has(jobId)) {
|
|
488
|
+
const clients = sseClients.get(jobId);
|
|
489
|
+
for (const res of clients) {
|
|
490
|
+
try {
|
|
491
|
+
res.end();
|
|
492
|
+
} catch (error) {
|
|
493
|
+
// Client already disconnected
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
sseClients.delete(jobId);
|
|
497
|
+
}
|
|
498
|
+
}, 1000);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* GET /job/:jobId/stream
|
|
504
|
+
* Real-time Server-Sent Events stream for job progress
|
|
505
|
+
*/
|
|
506
|
+
router.get('/job/:jobId/stream', (req, res) => {
|
|
507
|
+
const { jobId } = req.params;
|
|
508
|
+
|
|
509
|
+
if (!runningJobs.has(jobId)) {
|
|
510
|
+
return res.status(404).json({
|
|
511
|
+
success: false,
|
|
512
|
+
error: "Job not found"
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const job = runningJobs.get(jobId);
|
|
517
|
+
|
|
518
|
+
// Set up SSE headers
|
|
519
|
+
res.writeHead(200, {
|
|
520
|
+
'Content-Type': 'text/event-stream',
|
|
521
|
+
'Cache-Control': 'no-cache',
|
|
522
|
+
'Connection': 'keep-alive',
|
|
523
|
+
'Access-Control-Allow-Origin': '*',
|
|
524
|
+
'Access-Control-Allow-Headers': 'Cache-Control'
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Send initial connection message
|
|
528
|
+
res.write(`data: ${JSON.stringify({
|
|
529
|
+
type: 'connected',
|
|
530
|
+
data: {
|
|
531
|
+
jobId,
|
|
532
|
+
status: job.status,
|
|
533
|
+
message: 'Real-time stream connected'
|
|
534
|
+
},
|
|
535
|
+
timestamp: new Date().toISOString()
|
|
536
|
+
})}\n\n`);
|
|
537
|
+
|
|
538
|
+
// If job is already completed, send the final result
|
|
539
|
+
if (job.status === JobStatus.COMPLETED) {
|
|
540
|
+
res.write(`data: ${JSON.stringify({
|
|
541
|
+
type: 'job_completed',
|
|
542
|
+
data: {
|
|
543
|
+
jobId,
|
|
544
|
+
status: job.status,
|
|
545
|
+
result: job.result,
|
|
546
|
+
message: 'Job already completed'
|
|
547
|
+
},
|
|
548
|
+
timestamp: new Date().toISOString()
|
|
549
|
+
})}\n\n`);
|
|
550
|
+
|
|
551
|
+
res.write(`data: ${JSON.stringify({
|
|
552
|
+
type: 'stream_ended',
|
|
553
|
+
data: { message: 'Stream ended - job already completed' },
|
|
554
|
+
timestamp: new Date().toISOString()
|
|
555
|
+
})}\n\n`);
|
|
556
|
+
|
|
557
|
+
return res.end();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// If job failed, send error and end
|
|
561
|
+
if (job.status === JobStatus.FAILED) {
|
|
562
|
+
res.write(`data: ${JSON.stringify({
|
|
563
|
+
type: 'job_failed',
|
|
564
|
+
data: {
|
|
565
|
+
jobId,
|
|
566
|
+
status: job.status,
|
|
567
|
+
error: job.error,
|
|
568
|
+
message: 'Job already failed'
|
|
569
|
+
},
|
|
570
|
+
timestamp: new Date().toISOString()
|
|
571
|
+
})}\n\n`);
|
|
572
|
+
|
|
573
|
+
res.write(`data: ${JSON.stringify({
|
|
574
|
+
type: 'stream_ended',
|
|
575
|
+
data: { message: 'Stream ended - job failed' },
|
|
576
|
+
timestamp: new Date().toISOString()
|
|
577
|
+
})}\n\n`);
|
|
578
|
+
|
|
579
|
+
return res.end();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Add client to SSE clients list
|
|
583
|
+
addSSEClient(jobId, res);
|
|
584
|
+
|
|
585
|
+
// Handle client disconnect
|
|
586
|
+
req.on('close', () => {
|
|
587
|
+
removeSSEClient(jobId, res);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
req.on('aborted', () => {
|
|
591
|
+
removeSSEClient(jobId, res);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Keep connection alive with periodic ping
|
|
595
|
+
const pingInterval = setInterval(() => {
|
|
596
|
+
try {
|
|
597
|
+
res.write(`data: ${JSON.stringify({
|
|
598
|
+
type: 'ping',
|
|
599
|
+
data: { message: 'keepalive' },
|
|
600
|
+
timestamp: new Date().toISOString()
|
|
601
|
+
})}\n\n`);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
clearInterval(pingInterval);
|
|
604
|
+
removeSSEClient(jobId, res);
|
|
605
|
+
}
|
|
606
|
+
}, 30000); // Ping every 30 seconds
|
|
607
|
+
|
|
608
|
+
// Clean up interval when client disconnects
|
|
609
|
+
req.on('close', () => {
|
|
610
|
+
clearInterval(pingInterval);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* GET /job/:jobId
|
|
616
|
+
* Get job status and information
|
|
617
|
+
*/
|
|
618
|
+
router.get('/job/:jobId', (req, res) => {
|
|
619
|
+
const { jobId } = req.params;
|
|
620
|
+
|
|
621
|
+
if (!runningJobs.has(jobId)) {
|
|
622
|
+
return res.status(404).json({
|
|
623
|
+
success: false,
|
|
624
|
+
error: "Job not found"
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const job = runningJobs.get(jobId);
|
|
629
|
+
|
|
630
|
+
// Calculate duration
|
|
631
|
+
let duration = null;
|
|
632
|
+
if (job.startedAt) {
|
|
633
|
+
const endTime = job.completedAt || new Date();
|
|
634
|
+
duration = Math.round((endTime - job.startedAt) / 1000); // seconds
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
res.json({
|
|
638
|
+
success: true,
|
|
639
|
+
job: {
|
|
640
|
+
id: job.id,
|
|
641
|
+
status: job.status,
|
|
642
|
+
task: job.task.substring(0, 200) + (job.task.length > 200 ? '...' : ''),
|
|
643
|
+
createdAt: job.createdAt,
|
|
644
|
+
startedAt: job.startedAt,
|
|
645
|
+
completedAt: job.completedAt,
|
|
646
|
+
duration: duration ? `${duration}s` : null,
|
|
647
|
+
sessionId: job.sessionId,
|
|
648
|
+
error: job.error
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* GET /job/:jobId/result
|
|
655
|
+
* Get job result (only available when completed)
|
|
656
|
+
*/
|
|
657
|
+
router.get('/job/:jobId/result', (req, res) => {
|
|
658
|
+
const { jobId } = req.params;
|
|
659
|
+
|
|
660
|
+
if (!runningJobs.has(jobId)) {
|
|
661
|
+
return res.status(404).json({
|
|
662
|
+
success: false,
|
|
663
|
+
error: "Job not found"
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const job = runningJobs.get(jobId);
|
|
668
|
+
|
|
669
|
+
if (job.status === JobStatus.PENDING || job.status === JobStatus.RUNNING) {
|
|
670
|
+
return res.status(202).json({
|
|
671
|
+
success: false,
|
|
672
|
+
error: "Job is still running",
|
|
673
|
+
status: job.status,
|
|
674
|
+
message: "Check back later for results"
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (job.status === JobStatus.FAILED) {
|
|
679
|
+
return res.status(500).json({
|
|
680
|
+
success: false,
|
|
681
|
+
error: job.error,
|
|
682
|
+
status: job.status
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
res.json({
|
|
687
|
+
success: true,
|
|
688
|
+
jobId: job.id,
|
|
689
|
+
status: job.status,
|
|
690
|
+
result: job.result
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* DELETE /job/:jobId
|
|
696
|
+
* Cancel a running job
|
|
697
|
+
*/
|
|
698
|
+
router.delete('/job/:jobId', (req, res) => {
|
|
699
|
+
const { jobId } = req.params;
|
|
700
|
+
|
|
701
|
+
if (!runningJobs.has(jobId)) {
|
|
702
|
+
return res.status(404).json({
|
|
703
|
+
success: false,
|
|
704
|
+
error: "Job not found"
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const job = runningJobs.get(jobId);
|
|
709
|
+
|
|
710
|
+
if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
|
|
711
|
+
// Job already finished, just remove it
|
|
712
|
+
runningJobs.delete(jobId);
|
|
713
|
+
return res.json({
|
|
714
|
+
success: true,
|
|
715
|
+
message: "Job removed from history"
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Mark as cancelled (the actual execution will continue but won't be tracked)
|
|
720
|
+
job.status = JobStatus.CANCELLED;
|
|
721
|
+
job.completedAt = new Date();
|
|
722
|
+
|
|
723
|
+
res.json({
|
|
724
|
+
success: true,
|
|
725
|
+
message: "Job cancelled successfully"
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* GET /jobs
|
|
731
|
+
* Get all jobs with their status
|
|
732
|
+
*/
|
|
733
|
+
router.get('/jobs', (req, res) => {
|
|
734
|
+
const { status, limit = 50 } = req.query;
|
|
735
|
+
|
|
736
|
+
let jobs = Array.from(runningJobs.values());
|
|
737
|
+
|
|
738
|
+
// Filter by status if provided
|
|
739
|
+
if (status) {
|
|
740
|
+
jobs = jobs.filter(job => job.status === status);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Sort by creation time (newest first)
|
|
744
|
+
jobs.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
745
|
+
|
|
746
|
+
// Limit results
|
|
747
|
+
jobs = jobs.slice(0, parseInt(limit));
|
|
748
|
+
|
|
749
|
+
// Format jobs for response
|
|
750
|
+
const formattedJobs = jobs.map(job => {
|
|
751
|
+
let duration = null;
|
|
752
|
+
if (job.startedAt) {
|
|
753
|
+
const endTime = job.completedAt || new Date();
|
|
754
|
+
duration = Math.round((endTime - job.startedAt) / 1000);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
id: job.id,
|
|
759
|
+
status: job.status,
|
|
760
|
+
task: job.task.substring(0, 100) + (job.task.length > 100 ? '...' : ''),
|
|
761
|
+
createdAt: job.createdAt,
|
|
762
|
+
startedAt: job.startedAt,
|
|
763
|
+
completedAt: job.completedAt,
|
|
764
|
+
duration: duration ? `${duration}s` : null,
|
|
765
|
+
sessionId: job.sessionId
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
res.json({
|
|
770
|
+
success: true,
|
|
771
|
+
jobs: formattedJobs,
|
|
772
|
+
count: formattedJobs.length,
|
|
773
|
+
total: runningJobs.size
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* POST /session/create
|
|
779
|
+
* Create a new browser session for persistent operations
|
|
780
|
+
*/
|
|
781
|
+
router.post('/session/create', async (req, res) => {
|
|
782
|
+
try {
|
|
783
|
+
const { sessionName, config = {} } = req.body;
|
|
784
|
+
const sessionId = await createBrowserSession(sessionName);
|
|
785
|
+
|
|
786
|
+
const sessionData = {
|
|
787
|
+
id: sessionId,
|
|
788
|
+
createdAt: new Date(),
|
|
789
|
+
status: 'active',
|
|
790
|
+
config: config,
|
|
791
|
+
lastActivity: new Date()
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
activeSessions.set(sessionId, sessionData);
|
|
795
|
+
|
|
796
|
+
res.json({
|
|
797
|
+
success: true,
|
|
798
|
+
sessionId,
|
|
799
|
+
message: "Session created successfully",
|
|
800
|
+
session: sessionData
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
} catch (error) {
|
|
804
|
+
console.error("❌ Error creating session:", error);
|
|
805
|
+
res.status(500).json({
|
|
806
|
+
success: false,
|
|
807
|
+
error: error.message
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* POST /session/:sessionId/execute
|
|
814
|
+
* Execute task on existing session
|
|
815
|
+
*/
|
|
816
|
+
router.post('/session/:sessionId/execute', async (req, res) => {
|
|
817
|
+
const { sessionId } = req.params;
|
|
818
|
+
let automation = null;
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
const {
|
|
822
|
+
task,
|
|
823
|
+
config = {},
|
|
824
|
+
maxSteps = 30,
|
|
825
|
+
temperature = 0.7,
|
|
826
|
+
verbose = true,
|
|
827
|
+
systemPrompt:BASE_SYSTEM_PROMPT
|
|
828
|
+
} = req.body;
|
|
829
|
+
|
|
830
|
+
if (!task) {
|
|
831
|
+
return res.status(400).json({
|
|
832
|
+
success: false,
|
|
833
|
+
error: "Task description is required"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (!activeSessions.has(sessionId)) {
|
|
838
|
+
return res.status(404).json({
|
|
839
|
+
success: false,
|
|
840
|
+
error: "Session not found or expired"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Update last activity
|
|
845
|
+
const sessionData = activeSessions.get(sessionId);
|
|
846
|
+
sessionData.lastActivity = new Date();
|
|
847
|
+
|
|
848
|
+
// Merge session config with request config
|
|
849
|
+
const mergedConfig = { ...sessionData.config, ...config };
|
|
850
|
+
const llmConfig = createLLMConfig(mergedConfig);
|
|
851
|
+
|
|
852
|
+
const mcpEndpoint = mergedConfig.mcpEndpoint || 'http://173.208.155.135:8931/sse';
|
|
853
|
+
const driverUrl = `wss://gridnew.doingerp.com/devtools/${sessionId}`;
|
|
854
|
+
|
|
855
|
+
// Create automation instance
|
|
856
|
+
automation = new SmartBrowserAutomation({
|
|
857
|
+
maxSteps,
|
|
858
|
+
temperature
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// Initialize and execute
|
|
862
|
+
await automation.initialize(llmConfig, mcpEndpoint, driverUrl);
|
|
863
|
+
const result = await automation.executeTask(task, {
|
|
864
|
+
verbose,
|
|
865
|
+
systemPrompt
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
res.json({
|
|
869
|
+
success: true,
|
|
870
|
+
sessionId,
|
|
871
|
+
result: {
|
|
872
|
+
completed: result.completed,
|
|
873
|
+
steps: result.steps,
|
|
874
|
+
success: result.success,
|
|
875
|
+
results: result.results
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
} catch (error) {
|
|
880
|
+
console.error("❌ Error:", error);
|
|
881
|
+
res.status(500).json({
|
|
882
|
+
success: false,
|
|
883
|
+
error: error.message,
|
|
884
|
+
sessionId
|
|
885
|
+
});
|
|
886
|
+
} finally {
|
|
887
|
+
if (automation) {
|
|
888
|
+
try {
|
|
889
|
+
await automation.close();
|
|
890
|
+
} catch (closeError) {
|
|
891
|
+
console.error("Error closing automation:", closeError);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* GET /session/:sessionId
|
|
899
|
+
* Get session information
|
|
900
|
+
*/
|
|
901
|
+
router.get('/session/:sessionId', (req, res) => {
|
|
902
|
+
const { sessionId } = req.params;
|
|
903
|
+
|
|
904
|
+
if (!activeSessions.has(sessionId)) {
|
|
905
|
+
return res.status(404).json({
|
|
906
|
+
success: false,
|
|
907
|
+
error: "Session not found"
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const sessionData = activeSessions.get(sessionId);
|
|
912
|
+
res.json({
|
|
913
|
+
success: true,
|
|
914
|
+
session: sessionData
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* DELETE /session/:sessionId
|
|
920
|
+
* Delete a browser session
|
|
921
|
+
*/
|
|
922
|
+
router.delete('/session/:sessionId', async (req, res) => {
|
|
923
|
+
const { sessionId } = req.params;
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
await deleteBrowserSession(sessionId);
|
|
927
|
+
activeSessions.delete(sessionId);
|
|
928
|
+
|
|
929
|
+
res.json({
|
|
930
|
+
success: true,
|
|
931
|
+
message: "Session deleted successfully"
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
} catch (error) {
|
|
935
|
+
console.error("❌ Error deleting session:", error);
|
|
936
|
+
res.status(500).json({
|
|
937
|
+
success: false,
|
|
938
|
+
error: error.message
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* GET /sessions
|
|
945
|
+
* Get all active sessions
|
|
946
|
+
*/
|
|
947
|
+
router.get('/sessions', (req, res) => {
|
|
948
|
+
const sessions = Array.from(activeSessions.values());
|
|
949
|
+
res.json({
|
|
950
|
+
success: true,
|
|
951
|
+
sessions,
|
|
952
|
+
count: sessions.length
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* DELETE /sessions
|
|
958
|
+
* Clean up all sessions
|
|
959
|
+
*/
|
|
960
|
+
router.delete('/sessions', async (req, res) => {
|
|
961
|
+
try {
|
|
962
|
+
const sessionIds = Array.from(activeSessions.keys());
|
|
963
|
+
const deletePromises = sessionIds.map(sessionId => deleteBrowserSession(sessionId));
|
|
964
|
+
|
|
965
|
+
await Promise.allSettled(deletePromises);
|
|
966
|
+
activeSessions.clear();
|
|
967
|
+
|
|
968
|
+
res.json({
|
|
969
|
+
success: true,
|
|
970
|
+
message: `Cleaned up ${sessionIds.length} sessions`,
|
|
971
|
+
deletedSessions: sessionIds
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
} catch (error) {
|
|
975
|
+
console.error("❌ Error cleaning up sessions:", error);
|
|
976
|
+
res.status(500).json({
|
|
977
|
+
success: false,
|
|
978
|
+
error: error.message
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* GET /health
|
|
985
|
+
* Health check endpoint
|
|
986
|
+
*/
|
|
987
|
+
router.get('/health', (req, res) => {
|
|
988
|
+
res.json({
|
|
989
|
+
success: true,
|
|
990
|
+
message: "Smart Browser Automation CommonJS API is running",
|
|
991
|
+
timestamp: new Date(),
|
|
992
|
+
activeSessions: activeSessions.size,
|
|
993
|
+
uptime: process.uptime(),
|
|
994
|
+
version: "1.0.0-commonjs"
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* GET /config/providers
|
|
1000
|
+
* Get available LLM providers
|
|
1001
|
+
*/
|
|
1002
|
+
router.get('/config/providers', (req, res) => {
|
|
1003
|
+
res.json({
|
|
1004
|
+
success: true,
|
|
1005
|
+
providers: [
|
|
1006
|
+
{
|
|
1007
|
+
name: 'ollama',
|
|
1008
|
+
description: 'Ollama local or remote server',
|
|
1009
|
+
defaultModel: 'openrouter/horizon-beta',
|
|
1010
|
+
requiresApiKey: true
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: 'huggingface',
|
|
1014
|
+
description: 'Hugging Face Inference API',
|
|
1015
|
+
defaultModel: 'Qwen/Qwen3-Coder-480B-A35B-Instruct',
|
|
1016
|
+
requiresApiKey: true
|
|
1017
|
+
}
|
|
1018
|
+
]
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// Error handling middleware
|
|
1023
|
+
router.use((error, req, res, next) => {
|
|
1024
|
+
console.error('Unhandled error:', error);
|
|
1025
|
+
res.status(500).json({
|
|
1026
|
+
success: false,
|
|
1027
|
+
error: 'Internal server error',
|
|
1028
|
+
message: error.message,
|
|
1029
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// Return the configured router
|
|
1034
|
+
return router;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Create a middleware function that initializes the router on first use
|
|
1038
|
+
module.exports = (function () {
|
|
1039
|
+
let routerPromise = null;
|
|
1040
|
+
let router = null;
|
|
1041
|
+
|
|
1042
|
+
return function (req, res, next) {
|
|
1043
|
+
if (router) {
|
|
1044
|
+
// Router already initialized, use it directly
|
|
1045
|
+
return router(req, res, next);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (!routerPromise) {
|
|
1049
|
+
// Initialize router on first request
|
|
1050
|
+
routerPromise = createRouter().then(r => {
|
|
1051
|
+
router = r;
|
|
1052
|
+
return r;
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Wait for router to be ready and then handle the request
|
|
1057
|
+
routerPromise
|
|
1058
|
+
.then(r => r(req, res, next))
|
|
1059
|
+
.catch(next);
|
|
1060
|
+
};
|
|
1061
|
+
})();
|