seo-intel 1.0.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/.env.example +41 -0
- package/LICENSE +75 -0
- package/README.md +243 -0
- package/Start SEO Intel.bat +9 -0
- package/Start SEO Intel.command +8 -0
- package/cli.js +3727 -0
- package/config/example.json +29 -0
- package/config/setup-wizard.js +522 -0
- package/crawler/index.js +566 -0
- package/crawler/robots.js +103 -0
- package/crawler/sanitize.js +124 -0
- package/crawler/schema-parser.js +168 -0
- package/crawler/sitemap.js +103 -0
- package/crawler/stealth.js +393 -0
- package/crawler/subdomain-discovery.js +341 -0
- package/db/db.js +213 -0
- package/db/schema.sql +120 -0
- package/exports/competitive.js +186 -0
- package/exports/heuristics.js +67 -0
- package/exports/queries.js +197 -0
- package/exports/suggestive.js +230 -0
- package/exports/technical.js +180 -0
- package/exports/templates.js +77 -0
- package/lib/gate.js +204 -0
- package/lib/license.js +369 -0
- package/lib/oauth.js +432 -0
- package/lib/updater.js +324 -0
- package/package.json +68 -0
- package/reports/generate-html.js +6194 -0
- package/reports/generate-site-graph.js +949 -0
- package/reports/gsc-loader.js +190 -0
- package/scheduler.js +142 -0
- package/seo-audit.js +619 -0
- package/seo-intel.png +0 -0
- package/server.js +602 -0
- package/setup/ROADMAP.md +109 -0
- package/setup/checks.js +483 -0
- package/setup/config-builder.js +227 -0
- package/setup/engine.js +65 -0
- package/setup/installers.js +197 -0
- package/setup/models.js +328 -0
- package/setup/openclaw-bridge.js +329 -0
- package/setup/validator.js +395 -0
- package/setup/web-routes.js +688 -0
- package/setup/wizard.html +2920 -0
- package/start-seo-intel.sh +8 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEO Intel — Web Setup Routes
|
|
3
|
+
*
|
|
4
|
+
* HTTP API endpoints for the web-based setup wizard.
|
|
5
|
+
* Uses raw http (no Express) to match the existing server.js pattern.
|
|
6
|
+
* Long-running operations (install, validate) use SSE for streaming.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
fullSystemCheck,
|
|
15
|
+
checkGscData,
|
|
16
|
+
getModelRecommendations,
|
|
17
|
+
EXTRACTION_MODELS,
|
|
18
|
+
ANALYSIS_MODELS,
|
|
19
|
+
installNpmDeps,
|
|
20
|
+
installPlaywright,
|
|
21
|
+
pullOllamaModel,
|
|
22
|
+
createEnvFile,
|
|
23
|
+
runFullValidation,
|
|
24
|
+
buildProjectConfig,
|
|
25
|
+
writeProjectConfig,
|
|
26
|
+
updateEnvForSetup,
|
|
27
|
+
writeEnvKey,
|
|
28
|
+
validateConfig,
|
|
29
|
+
} from './engine.js';
|
|
30
|
+
|
|
31
|
+
import { getCurrentVersion, getUpdateInfo, checkForUpdates } from '../lib/updater.js';
|
|
32
|
+
|
|
33
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const ROOT = join(__dirname, '..');
|
|
35
|
+
const WIZARD_HTML = join(__dirname, 'wizard.html');
|
|
36
|
+
|
|
37
|
+
// ── CORS / JSON helpers ─────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function jsonResponse(res, data, status = 200) {
|
|
40
|
+
res.writeHead(status, {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'Access-Control-Allow-Origin': '*',
|
|
43
|
+
});
|
|
44
|
+
res.end(JSON.stringify(data));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sseHeaders(res) {
|
|
48
|
+
res.writeHead(200, {
|
|
49
|
+
'Content-Type': 'text/event-stream',
|
|
50
|
+
'Cache-Control': 'no-cache',
|
|
51
|
+
'Connection': 'keep-alive',
|
|
52
|
+
'Access-Control-Allow-Origin': '*',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sseWrite(res, data) {
|
|
57
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readBody(req) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
let body = '';
|
|
63
|
+
req.on('data', chunk => body += chunk);
|
|
64
|
+
req.on('end', () => {
|
|
65
|
+
try {
|
|
66
|
+
resolve(body ? JSON.parse(body) : {});
|
|
67
|
+
} catch (e) {
|
|
68
|
+
reject(new Error('Invalid JSON body'));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
req.on('error', reject);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Route Handler ───────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handle setup-related HTTP requests.
|
|
79
|
+
* Returns true if the request was handled, false to pass through.
|
|
80
|
+
*
|
|
81
|
+
* @param {http.IncomingMessage} req
|
|
82
|
+
* @param {http.ServerResponse} res
|
|
83
|
+
* @param {URL} url
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function handleSetupRequest(req, res, url) {
|
|
87
|
+
const path = url.pathname;
|
|
88
|
+
const method = req.method;
|
|
89
|
+
|
|
90
|
+
// Handle CORS preflight
|
|
91
|
+
if (method === 'OPTIONS' && path.startsWith('/api/setup/')) {
|
|
92
|
+
res.writeHead(204, {
|
|
93
|
+
'Access-Control-Allow-Origin': '*',
|
|
94
|
+
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
|
|
95
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
96
|
+
});
|
|
97
|
+
res.end();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// GET /setup — serve wizard HTML
|
|
102
|
+
if ((path === '/setup' || path === '/setup/') && method === 'GET') {
|
|
103
|
+
serveWizardHtml(res);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// GET /api/setup/status — full system check
|
|
108
|
+
if (path === '/api/setup/status' && method === 'GET') {
|
|
109
|
+
handleStatus(req, res);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// GET /api/setup/models — model recommendations
|
|
114
|
+
if (path === '/api/setup/models' && method === 'GET') {
|
|
115
|
+
handleModels(req, res);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// POST /api/setup/install — install dependencies (SSE)
|
|
120
|
+
if (path === '/api/setup/install' && method === 'POST') {
|
|
121
|
+
handleInstall(req, res);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// POST /api/setup/env — update .env keys
|
|
126
|
+
if (path === '/api/setup/env' && method === 'POST') {
|
|
127
|
+
handleEnv(req, res);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// POST /api/setup/config — create project config
|
|
132
|
+
if (path === '/api/setup/config' && method === 'POST') {
|
|
133
|
+
handleConfig(req, res);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// POST /api/setup/test-pipeline — run validation (SSE)
|
|
138
|
+
if (path === '/api/setup/test-pipeline' && method === 'POST') {
|
|
139
|
+
handleTestPipeline(req, res);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// GET /api/setup/gsc — check GSC data status
|
|
144
|
+
if (path === '/api/setup/gsc' && method === 'GET') {
|
|
145
|
+
handleGscStatus(req, res, url);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// POST /api/setup/gsc/upload — upload GSC CSV files
|
|
150
|
+
if (path === '/api/setup/gsc/upload' && method === 'POST') {
|
|
151
|
+
handleGscUpload(req, res);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// GET /api/setup/version — current version + update info
|
|
156
|
+
if (path === '/api/setup/version' && method === 'GET') {
|
|
157
|
+
handleVersion(req, res);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// GET /api/setup/projects — list all projects
|
|
162
|
+
if (path === '/api/setup/projects' && method === 'GET') {
|
|
163
|
+
handleListProjects(res);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// GET /api/setup/projects/:project — get project config
|
|
168
|
+
if (path.match(/^\/api\/setup\/projects\/[^/]+$/) && method === 'GET') {
|
|
169
|
+
const project = path.split('/').pop();
|
|
170
|
+
handleGetProject(res, project);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// PATCH /api/setup/projects/:project/competitors — add/remove competitors
|
|
175
|
+
if (path.match(/^\/api\/setup\/projects\/[^/]+\/competitors$/) && method === 'PATCH') {
|
|
176
|
+
const project = path.split('/')[4];
|
|
177
|
+
handleUpdateCompetitors(req, res, project);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// GET /api/setup/auth/status — all OAuth connection statuses
|
|
182
|
+
if (path === '/api/setup/auth/status' && method === 'GET') {
|
|
183
|
+
handleAuthStatus(res);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// GET /api/setup/auth/:provider/url — get OAuth authorization URL
|
|
188
|
+
if (path.match(/^\/api\/setup\/auth\/[^/]+\/url$/) && method === 'GET') {
|
|
189
|
+
const provider = path.split('/')[4];
|
|
190
|
+
handleAuthUrl(res, provider);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// POST /api/setup/auth/:provider/callback — exchange OAuth code for tokens
|
|
195
|
+
if (path.match(/^\/api\/setup\/auth\/[^/]+\/callback$/) && method === 'POST') {
|
|
196
|
+
const provider = path.split('/')[4];
|
|
197
|
+
handleAuthCallback(req, res, provider);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// DELETE /api/setup/auth/:provider — disconnect provider
|
|
202
|
+
if (path.match(/^\/api\/setup\/auth\/[^/]+$/) && method === 'DELETE') {
|
|
203
|
+
const provider = path.split('/')[4];
|
|
204
|
+
handleAuthDisconnect(res, provider);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// POST /api/setup/agent/chat — agent-powered setup chat
|
|
209
|
+
if (path === '/api/setup/agent/chat' && method === 'POST') {
|
|
210
|
+
handleAgentChat(req, res);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Route Implementations ───────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
function serveWizardHtml(res) {
|
|
220
|
+
if (!existsSync(WIZARD_HTML)) {
|
|
221
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
222
|
+
res.end('Setup wizard not found');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const html = readFileSync(WIZARD_HTML, 'utf8');
|
|
226
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
227
|
+
res.end(html);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function handleStatus(req, res) {
|
|
231
|
+
try {
|
|
232
|
+
const status = await fullSystemCheck();
|
|
233
|
+
jsonResponse(res, status);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function handleModels(req, res) {
|
|
240
|
+
try {
|
|
241
|
+
const status = await fullSystemCheck();
|
|
242
|
+
const models = getModelRecommendations(
|
|
243
|
+
status.ollama.models,
|
|
244
|
+
status.env.keys,
|
|
245
|
+
status.vram.vramMB
|
|
246
|
+
);
|
|
247
|
+
jsonResponse(res, {
|
|
248
|
+
...models,
|
|
249
|
+
gpu: status.vram,
|
|
250
|
+
ollama: status.ollama,
|
|
251
|
+
});
|
|
252
|
+
} catch (err) {
|
|
253
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function handleInstall(req, res) {
|
|
258
|
+
try {
|
|
259
|
+
const body = await readBody(req);
|
|
260
|
+
const { action, model, host } = body;
|
|
261
|
+
|
|
262
|
+
sseHeaders(res);
|
|
263
|
+
|
|
264
|
+
let generator;
|
|
265
|
+
switch (action) {
|
|
266
|
+
case 'npm':
|
|
267
|
+
generator = installNpmDeps();
|
|
268
|
+
break;
|
|
269
|
+
case 'playwright':
|
|
270
|
+
generator = installPlaywright();
|
|
271
|
+
break;
|
|
272
|
+
case 'ollama-pull':
|
|
273
|
+
if (!model) {
|
|
274
|
+
sseWrite(res, { phase: 'error', status: 'error', message: 'Missing model parameter' });
|
|
275
|
+
res.end();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
generator = pullOllamaModel(model, host || 'http://localhost:11434');
|
|
279
|
+
break;
|
|
280
|
+
case 'env':
|
|
281
|
+
generator = createEnvFile();
|
|
282
|
+
break;
|
|
283
|
+
default:
|
|
284
|
+
sseWrite(res, { phase: 'error', status: 'error', message: `Unknown action: ${action}` });
|
|
285
|
+
res.end();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let hadError = false;
|
|
290
|
+
for await (const ev of generator) {
|
|
291
|
+
if (ev.status === 'error') hadError = true;
|
|
292
|
+
sseWrite(res, ev);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (hadError) {
|
|
296
|
+
sseWrite(res, { phase: 'complete', status: 'error', message: 'Installation finished with errors' });
|
|
297
|
+
} else {
|
|
298
|
+
sseWrite(res, { phase: 'complete', status: 'done', message: 'Installation complete' });
|
|
299
|
+
}
|
|
300
|
+
res.end();
|
|
301
|
+
} catch (err) {
|
|
302
|
+
try {
|
|
303
|
+
sseWrite(res, { phase: 'error', status: 'error', message: err.message });
|
|
304
|
+
res.end();
|
|
305
|
+
} catch {
|
|
306
|
+
// Response already ended
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function handleEnv(req, res) {
|
|
312
|
+
try {
|
|
313
|
+
const body = await readBody(req);
|
|
314
|
+
const { keys } = body;
|
|
315
|
+
|
|
316
|
+
if (!keys || typeof keys !== 'object') {
|
|
317
|
+
jsonResponse(res, { error: 'Missing keys object' }, 400);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const result = updateEnvForSetup(keys);
|
|
322
|
+
jsonResponse(res, { success: true, path: result.path });
|
|
323
|
+
} catch (err) {
|
|
324
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function handleConfig(req, res) {
|
|
329
|
+
try {
|
|
330
|
+
const body = await readBody(req);
|
|
331
|
+
const config = buildProjectConfig(body);
|
|
332
|
+
const validation = validateConfig(config);
|
|
333
|
+
|
|
334
|
+
if (!validation.valid) {
|
|
335
|
+
jsonResponse(res, { success: false, errors: validation.errors }, 400);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const result = writeProjectConfig(config);
|
|
340
|
+
jsonResponse(res, {
|
|
341
|
+
success: true,
|
|
342
|
+
path: result.path,
|
|
343
|
+
overwritten: result.overwritten,
|
|
344
|
+
config,
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function handleTestPipeline(req, res) {
|
|
352
|
+
try {
|
|
353
|
+
const body = await readBody(req);
|
|
354
|
+
sseHeaders(res);
|
|
355
|
+
|
|
356
|
+
for await (const step of runFullValidation(body)) {
|
|
357
|
+
sseWrite(res, step);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
res.end();
|
|
361
|
+
} catch (err) {
|
|
362
|
+
try {
|
|
363
|
+
sseWrite(res, { step: 'error', status: 'fail', detail: err.message });
|
|
364
|
+
res.end();
|
|
365
|
+
} catch {
|
|
366
|
+
// Response already ended
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── GSC Data Handlers ──────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
async function handleGscStatus(req, res, url) {
|
|
374
|
+
try {
|
|
375
|
+
const project = url.searchParams.get('project') || '';
|
|
376
|
+
const gsc = checkGscData(project);
|
|
377
|
+
jsonResponse(res, gsc);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function handleGscUpload(req, res) {
|
|
384
|
+
try {
|
|
385
|
+
// Read multipart form data (simplified — expects JSON with base64 files)
|
|
386
|
+
const body = await readBody(req);
|
|
387
|
+
const { project, files } = body;
|
|
388
|
+
|
|
389
|
+
if (!project || !files || !Array.isArray(files) || files.length === 0) {
|
|
390
|
+
jsonResponse(res, { error: 'Missing project name or files array' }, 400);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Create GSC directory: gsc/<project>/
|
|
395
|
+
const gscDir = join(ROOT, 'gsc');
|
|
396
|
+
const projectDir = join(gscDir, project);
|
|
397
|
+
|
|
398
|
+
if (!existsSync(gscDir)) mkdirSync(gscDir, { recursive: true });
|
|
399
|
+
if (!existsSync(projectDir)) mkdirSync(projectDir, { recursive: true });
|
|
400
|
+
|
|
401
|
+
const saved = [];
|
|
402
|
+
for (const file of files) {
|
|
403
|
+
if (!file.name || !file.content) continue;
|
|
404
|
+
|
|
405
|
+
// Decode base64 content and write CSV
|
|
406
|
+
const content = Buffer.from(file.content, 'base64').toString('utf8');
|
|
407
|
+
const filePath = join(projectDir, file.name);
|
|
408
|
+
writeFileSync(filePath, content, 'utf8');
|
|
409
|
+
saved.push(file.name);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Re-check GSC data status
|
|
413
|
+
const gsc = checkGscData(project);
|
|
414
|
+
|
|
415
|
+
jsonResponse(res, {
|
|
416
|
+
success: true,
|
|
417
|
+
saved,
|
|
418
|
+
folder: project,
|
|
419
|
+
gsc,
|
|
420
|
+
});
|
|
421
|
+
} catch (err) {
|
|
422
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Version / Update Handler ──────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
async function handleVersion(req, res) {
|
|
429
|
+
try {
|
|
430
|
+
checkForUpdates(); // trigger background check if not already
|
|
431
|
+
const info = await getUpdateInfo();
|
|
432
|
+
jsonResponse(res, info);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
jsonResponse(res, {
|
|
435
|
+
current: getCurrentVersion(),
|
|
436
|
+
hasUpdate: false,
|
|
437
|
+
error: err.message,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Project / Competitors Handlers ────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
import { domainFromUrl } from './config-builder.js';
|
|
445
|
+
|
|
446
|
+
function loadProjectConfig(project) {
|
|
447
|
+
const configPath = join(ROOT, 'config', `${project}.json`);
|
|
448
|
+
if (!existsSync(configPath)) return null;
|
|
449
|
+
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function saveProjectConfig(project, config) {
|
|
453
|
+
const configPath = join(ROOT, 'config', `${project}.json`);
|
|
454
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function handleListProjects(res) {
|
|
458
|
+
try {
|
|
459
|
+
const configDir = join(ROOT, 'config');
|
|
460
|
+
const configs = readdirSync(configDir)
|
|
461
|
+
.filter(f => f.endsWith('.json') && !f.startsWith('_'))
|
|
462
|
+
.map(f => {
|
|
463
|
+
try {
|
|
464
|
+
const config = JSON.parse(readFileSync(join(configDir, f), 'utf8'));
|
|
465
|
+
return {
|
|
466
|
+
project: config.project,
|
|
467
|
+
target: config.target?.domain,
|
|
468
|
+
competitors: (config.competitors || []).map(c => c.domain),
|
|
469
|
+
owned: (config.owned || []).map(o => o.domain),
|
|
470
|
+
};
|
|
471
|
+
} catch { return null; }
|
|
472
|
+
})
|
|
473
|
+
.filter(Boolean);
|
|
474
|
+
|
|
475
|
+
jsonResponse(res, { projects: configs });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function handleGetProject(res, project) {
|
|
482
|
+
try {
|
|
483
|
+
const config = loadProjectConfig(project);
|
|
484
|
+
if (!config) {
|
|
485
|
+
jsonResponse(res, { error: `Project '${project}' not found` }, 404);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
jsonResponse(res, config);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function handleUpdateCompetitors(req, res, project) {
|
|
495
|
+
try {
|
|
496
|
+
const config = loadProjectConfig(project);
|
|
497
|
+
if (!config) {
|
|
498
|
+
jsonResponse(res, { error: `Project '${project}' not found` }, 404);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const body = await readBody(req);
|
|
503
|
+
const changes = [];
|
|
504
|
+
|
|
505
|
+
// Add competitors
|
|
506
|
+
if (body.add && Array.isArray(body.add)) {
|
|
507
|
+
for (const entry of body.add) {
|
|
508
|
+
const domain = domainFromUrl(entry);
|
|
509
|
+
const url = entry.startsWith('http') ? entry : `https://${entry}`;
|
|
510
|
+
if (!config.competitors.some(c => c.domain === domain)) {
|
|
511
|
+
config.competitors.push({ url, domain, role: 'competitor' });
|
|
512
|
+
changes.push({ action: 'added', type: 'competitor', domain });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Remove competitors
|
|
518
|
+
if (body.remove && Array.isArray(body.remove)) {
|
|
519
|
+
for (const entry of body.remove) {
|
|
520
|
+
const domain = domainFromUrl(entry);
|
|
521
|
+
const before = config.competitors.length;
|
|
522
|
+
config.competitors = config.competitors.filter(c => c.domain !== domain);
|
|
523
|
+
if (config.competitors.length < before) {
|
|
524
|
+
changes.push({ action: 'removed', type: 'competitor', domain });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Add owned
|
|
530
|
+
if (body.addOwned && Array.isArray(body.addOwned)) {
|
|
531
|
+
if (!config.owned) config.owned = [];
|
|
532
|
+
for (const entry of body.addOwned) {
|
|
533
|
+
const domain = domainFromUrl(entry);
|
|
534
|
+
const url = entry.startsWith('http') ? entry : `https://${entry}`;
|
|
535
|
+
if (!config.owned.some(o => o.domain === domain)) {
|
|
536
|
+
config.owned.push({ url, domain, role: 'owned' });
|
|
537
|
+
changes.push({ action: 'added', type: 'owned', domain });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Remove owned
|
|
543
|
+
if (body.removeOwned && Array.isArray(body.removeOwned)) {
|
|
544
|
+
if (!config.owned) config.owned = [];
|
|
545
|
+
for (const entry of body.removeOwned) {
|
|
546
|
+
const domain = domainFromUrl(entry);
|
|
547
|
+
const before = config.owned.length;
|
|
548
|
+
config.owned = config.owned.filter(o => o.domain !== domain);
|
|
549
|
+
if (config.owned.length < before) {
|
|
550
|
+
changes.push({ action: 'removed', type: 'owned', domain });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (changes.length > 0) {
|
|
556
|
+
saveProjectConfig(project, config);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
jsonResponse(res, {
|
|
560
|
+
success: true,
|
|
561
|
+
changes,
|
|
562
|
+
competitors: config.competitors.map(c => c.domain),
|
|
563
|
+
owned: (config.owned || []).map(o => o.domain),
|
|
564
|
+
});
|
|
565
|
+
} catch (err) {
|
|
566
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ── OAuth Handlers ────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
import {
|
|
573
|
+
getAllConnectionStatus,
|
|
574
|
+
getAuthUrl,
|
|
575
|
+
getProviderRequirements,
|
|
576
|
+
clearTokens,
|
|
577
|
+
} from '../lib/oauth.js';
|
|
578
|
+
|
|
579
|
+
function handleAuthStatus(res) {
|
|
580
|
+
try {
|
|
581
|
+
const statuses = getAllConnectionStatus();
|
|
582
|
+
const requirements = getProviderRequirements();
|
|
583
|
+
|
|
584
|
+
// Also include API key status
|
|
585
|
+
const envPath = join(ROOT, '.env');
|
|
586
|
+
const apiKeys = {};
|
|
587
|
+
if (existsSync(envPath)) {
|
|
588
|
+
const env = readFileSync(envPath, 'utf8');
|
|
589
|
+
for (const key of ['GEMINI_API_KEY', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'DEEPSEEK_API_KEY']) {
|
|
590
|
+
const match = env.match(new RegExp(`^${key}=(.+)$`, 'm'));
|
|
591
|
+
apiKeys[key] = !!(match && match[1]?.trim());
|
|
592
|
+
}
|
|
593
|
+
// Check OAuth credentials too
|
|
594
|
+
for (const key of ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET']) {
|
|
595
|
+
const match = env.match(new RegExp(`^${key}=(.+)$`, 'm'));
|
|
596
|
+
apiKeys[key] = !!(match && match[1]?.trim());
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
jsonResponse(res, {
|
|
601
|
+
oauth: statuses,
|
|
602
|
+
providers: requirements,
|
|
603
|
+
apiKeys,
|
|
604
|
+
});
|
|
605
|
+
} catch (err) {
|
|
606
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function handleAuthUrl(res, provider) {
|
|
611
|
+
try {
|
|
612
|
+
const { url, state } = getAuthUrl(provider, { port: 9876 });
|
|
613
|
+
jsonResponse(res, { url, state, provider });
|
|
614
|
+
} catch (err) {
|
|
615
|
+
// Likely missing credentials
|
|
616
|
+
jsonResponse(res, {
|
|
617
|
+
error: err.message,
|
|
618
|
+
needsSetup: true,
|
|
619
|
+
provider,
|
|
620
|
+
setupUrl: provider === 'google' ? 'https://console.cloud.google.com/apis/credentials' : null,
|
|
621
|
+
envVars: provider === 'google' ? ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'] : [],
|
|
622
|
+
}, 400);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function handleAuthCallback(req, res, provider) {
|
|
627
|
+
try {
|
|
628
|
+
const body = await readBody(req);
|
|
629
|
+
const { code, redirectUri } = body;
|
|
630
|
+
|
|
631
|
+
if (!code) {
|
|
632
|
+
jsonResponse(res, { error: 'Missing authorization code' }, 400);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Dynamically import the exchange function
|
|
637
|
+
const { default: _, ...oauth } = await import('../lib/oauth.js');
|
|
638
|
+
|
|
639
|
+
// We need to call exchangeCode — but it's not exported directly.
|
|
640
|
+
// Instead, the web wizard will use the popup window flow:
|
|
641
|
+
// 1. Open auth URL in popup
|
|
642
|
+
// 2. Google redirects to localhost:9876/oauth/callback
|
|
643
|
+
// 3. The callback server (started by startOAuthFlow) handles the exchange
|
|
644
|
+
//
|
|
645
|
+
// For web-initiated flows, we provide the auth URL and let the CLI
|
|
646
|
+
// callback server handle token exchange.
|
|
647
|
+
|
|
648
|
+
jsonResponse(res, {
|
|
649
|
+
success: true,
|
|
650
|
+
message: 'Use the auth URL flow — tokens are exchanged via the local callback server',
|
|
651
|
+
});
|
|
652
|
+
} catch (err) {
|
|
653
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function handleAuthDisconnect(res, provider) {
|
|
658
|
+
try {
|
|
659
|
+
clearTokens(provider);
|
|
660
|
+
jsonResponse(res, { success: true, provider, disconnected: true });
|
|
661
|
+
} catch (err) {
|
|
662
|
+
jsonResponse(res, { error: err.message }, 500);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ── Agent Chat Handler ────────────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
async function handleAgentChat(req, res) {
|
|
669
|
+
try {
|
|
670
|
+
const { isGatewayReady, handleAgentChat: agentChat } = await import('./openclaw-bridge.js');
|
|
671
|
+
|
|
672
|
+
const ready = await isGatewayReady();
|
|
673
|
+
if (!ready) {
|
|
674
|
+
jsonResponse(res, {
|
|
675
|
+
error: 'OpenClaw gateway not running',
|
|
676
|
+
available: false,
|
|
677
|
+
hint: 'Start OpenClaw with: openclaw gateway',
|
|
678
|
+
}, 503);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const body = await readBody(req);
|
|
683
|
+
const result = await agentChat(body);
|
|
684
|
+
jsonResponse(res, { ...result, available: true });
|
|
685
|
+
} catch (err) {
|
|
686
|
+
jsonResponse(res, { error: err.message, available: false }, 500);
|
|
687
|
+
}
|
|
688
|
+
}
|