@ainyc/canonry 1.20.1 → 1.21.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 +16 -2
- package/assets/assets/index-3PC6-Oip.css +1 -0
- package/assets/assets/index-Bd0xUz_T.js +246 -0
- package/assets/index.html +2 -2
- package/bin/canonry.mjs +1 -1
- package/dist/{chunk-YXYC7NGJ.js → chunk-FR3LEBGX.js} +1058 -549
- package/dist/cli.js +4191 -3095
- package/dist/index.d.ts +1 -6
- package/dist/index.js +1 -1
- package/package.json +8 -7
- package/assets/assets/index-CaypGSmN.js +0 -246
- package/assets/assets/index-D-2VeJdG.css +0 -1
|
@@ -35,7 +35,7 @@ function loadConfig() {
|
|
|
35
35
|
throw new Error(
|
|
36
36
|
`Config not found at ${configPath}.
|
|
37
37
|
Run "canonry init" to set up interactively, or "canonry init --gemini-key <key>" for non-interactive setup.
|
|
38
|
-
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).`
|
|
38
|
+
For CI/Docker, use "canonry bootstrap" with env vars (GEMINI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET).`
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
@@ -72,6 +72,16 @@ Do not write config.yaml by hand; use "canonry init", "canonry settings", or "ca
|
|
|
72
72
|
} catch {
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
if (parsed.basePath) {
|
|
76
|
+
const normalizedBase = "/" + parsed.basePath.replace(/^\/|\/$/g, "");
|
|
77
|
+
try {
|
|
78
|
+
const url = new URL(parsed.apiUrl);
|
|
79
|
+
if (normalizedBase !== "/" && !url.pathname.startsWith(normalizedBase)) {
|
|
80
|
+
parsed.apiUrl = url.origin + normalizedBase;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
75
85
|
return parsed;
|
|
76
86
|
}
|
|
77
87
|
function saveConfig(config) {
|
|
@@ -167,7 +177,7 @@ import { fileURLToPath } from "url";
|
|
|
167
177
|
import Fastify from "fastify";
|
|
168
178
|
|
|
169
179
|
// ../contracts/src/config-schema.ts
|
|
170
|
-
import { z as
|
|
180
|
+
import { z as z4 } from "zod";
|
|
171
181
|
|
|
172
182
|
// ../contracts/src/provider.ts
|
|
173
183
|
import { z } from "zod";
|
|
@@ -176,30 +186,18 @@ var providerQuotaPolicySchema = z.object({
|
|
|
176
186
|
maxRequestsPerMinute: z.number().int().positive(),
|
|
177
187
|
maxRequestsPerDay: z.number().int().positive()
|
|
178
188
|
});
|
|
179
|
-
var
|
|
180
|
-
var
|
|
181
|
-
var PROVIDER_MODE = {
|
|
182
|
-
gemini: "api",
|
|
183
|
-
openai: "api",
|
|
184
|
-
claude: "api",
|
|
185
|
-
local: "api",
|
|
186
|
-
"cdp:chatgpt": "browser"
|
|
187
|
-
};
|
|
189
|
+
var providerNameSchema = z.string().min(1);
|
|
190
|
+
var apiProviderNameSchema = z.string().min(1);
|
|
188
191
|
function isBrowserProvider(name) {
|
|
189
|
-
return
|
|
192
|
+
return name.startsWith("cdp:");
|
|
190
193
|
}
|
|
191
194
|
var CDP_TARGETS = ["cdp:chatgpt"];
|
|
192
|
-
function parseProviderName(input) {
|
|
193
|
-
const lower = input.trim().toLowerCase();
|
|
194
|
-
return PROVIDER_NAMES.includes(lower) ? lower : void 0;
|
|
195
|
-
}
|
|
196
195
|
function resolveProviderInput(input) {
|
|
197
196
|
const lower = input.trim().toLowerCase();
|
|
198
197
|
if (lower === "cdp") {
|
|
199
198
|
return [...CDP_TARGETS];
|
|
200
199
|
}
|
|
201
|
-
|
|
202
|
-
return parsed ? [parsed] : [];
|
|
200
|
+
return lower ? [lower] : [];
|
|
203
201
|
}
|
|
204
202
|
var locationContextSchema = z.object({
|
|
205
203
|
label: z.string().min(1),
|
|
@@ -229,118 +227,148 @@ var notificationDtoSchema = z2.object({
|
|
|
229
227
|
updatedAt: z2.string()
|
|
230
228
|
});
|
|
231
229
|
|
|
230
|
+
// ../contracts/src/project.ts
|
|
231
|
+
import { z as z3 } from "zod";
|
|
232
|
+
var configSourceSchema = z3.enum(["cli", "api", "config-file"]);
|
|
233
|
+
function findDuplicateLocationLabels(locations) {
|
|
234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
235
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
236
|
+
for (const location of locations) {
|
|
237
|
+
if (seen.has(location.label)) {
|
|
238
|
+
duplicates.add(location.label);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
seen.add(location.label);
|
|
242
|
+
}
|
|
243
|
+
return [...duplicates];
|
|
244
|
+
}
|
|
245
|
+
function hasLocationLabel(locations, label) {
|
|
246
|
+
if (!label) return true;
|
|
247
|
+
return locations.some((location) => location.label === label);
|
|
248
|
+
}
|
|
249
|
+
var projectUpsertRequestSchema = z3.object({
|
|
250
|
+
displayName: z3.string().min(1),
|
|
251
|
+
canonicalDomain: z3.string().min(1),
|
|
252
|
+
ownedDomains: z3.array(z3.string().min(1)).optional(),
|
|
253
|
+
country: z3.string().length(2),
|
|
254
|
+
language: z3.string().min(2),
|
|
255
|
+
tags: z3.array(z3.string()).optional(),
|
|
256
|
+
labels: z3.record(z3.string(), z3.string()).optional(),
|
|
257
|
+
providers: z3.array(providerNameSchema).optional(),
|
|
258
|
+
locations: z3.array(locationContextSchema).optional(),
|
|
259
|
+
defaultLocation: z3.string().nullable().optional(),
|
|
260
|
+
configSource: configSourceSchema.optional()
|
|
261
|
+
});
|
|
262
|
+
var projectDtoSchema = z3.object({
|
|
263
|
+
id: z3.string(),
|
|
264
|
+
name: z3.string(),
|
|
265
|
+
displayName: z3.string().optional(),
|
|
266
|
+
canonicalDomain: z3.string(),
|
|
267
|
+
ownedDomains: z3.array(z3.string()).default([]),
|
|
268
|
+
country: z3.string().length(2),
|
|
269
|
+
language: z3.string().min(2),
|
|
270
|
+
tags: z3.array(z3.string()).default([]),
|
|
271
|
+
labels: z3.record(z3.string(), z3.string()).default({}),
|
|
272
|
+
locations: z3.array(locationContextSchema).default([]),
|
|
273
|
+
defaultLocation: z3.string().nullable().optional(),
|
|
274
|
+
configSource: configSourceSchema.default("cli"),
|
|
275
|
+
configRevision: z3.number().int().positive().default(1),
|
|
276
|
+
createdAt: z3.string().optional(),
|
|
277
|
+
updatedAt: z3.string().optional()
|
|
278
|
+
});
|
|
279
|
+
function normalizeProjectDomain(input) {
|
|
280
|
+
let domain = input.trim().toLowerCase();
|
|
281
|
+
try {
|
|
282
|
+
if (domain.includes("://")) {
|
|
283
|
+
domain = new URL(domain).hostname.toLowerCase();
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
return domain.replace(/^www\./, "");
|
|
288
|
+
}
|
|
289
|
+
function effectiveDomains(project) {
|
|
290
|
+
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
const result = [];
|
|
293
|
+
for (const d of all) {
|
|
294
|
+
const trimmed = d.trim();
|
|
295
|
+
if (!trimmed) continue;
|
|
296
|
+
const norm = normalizeProjectDomain(trimmed);
|
|
297
|
+
if (seen.has(norm)) continue;
|
|
298
|
+
seen.add(norm);
|
|
299
|
+
result.push(trimmed);
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
232
304
|
// ../contracts/src/config-schema.ts
|
|
233
|
-
var configMetadataSchema =
|
|
234
|
-
name:
|
|
305
|
+
var configMetadataSchema = z4.object({
|
|
306
|
+
name: z4.string().min(1).max(63).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, {
|
|
235
307
|
message: "Name must be a lowercase slug (letters, numbers, hyphens)"
|
|
236
308
|
}),
|
|
237
|
-
labels:
|
|
309
|
+
labels: z4.record(z4.string(), z4.string()).optional().default({})
|
|
238
310
|
});
|
|
239
|
-
var configScheduleSchema =
|
|
240
|
-
preset:
|
|
241
|
-
cron:
|
|
242
|
-
timezone:
|
|
243
|
-
providers:
|
|
311
|
+
var configScheduleSchema = z4.object({
|
|
312
|
+
preset: z4.string().optional(),
|
|
313
|
+
cron: z4.string().optional(),
|
|
314
|
+
timezone: z4.string().optional().default("UTC"),
|
|
315
|
+
providers: z4.array(providerNameSchema).optional().default([])
|
|
244
316
|
}).refine(
|
|
245
317
|
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
246
318
|
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
247
319
|
).optional();
|
|
248
|
-
var configNotificationSchema =
|
|
249
|
-
channel:
|
|
250
|
-
url:
|
|
251
|
-
events:
|
|
320
|
+
var configNotificationSchema = z4.object({
|
|
321
|
+
channel: z4.literal("webhook"),
|
|
322
|
+
url: z4.string().url(),
|
|
323
|
+
events: z4.array(notificationEventSchema).min(1)
|
|
252
324
|
});
|
|
253
|
-
var configGoogleSchema =
|
|
254
|
-
gsc:
|
|
255
|
-
propertyUrl:
|
|
325
|
+
var configGoogleSchema = z4.object({
|
|
326
|
+
gsc: z4.object({
|
|
327
|
+
propertyUrl: z4.string()
|
|
256
328
|
}).optional(),
|
|
257
|
-
syncSchedule:
|
|
258
|
-
preset:
|
|
259
|
-
cron:
|
|
329
|
+
syncSchedule: z4.object({
|
|
330
|
+
preset: z4.string().optional(),
|
|
331
|
+
cron: z4.string().optional()
|
|
260
332
|
}).optional()
|
|
261
333
|
}).optional();
|
|
262
|
-
var configSpecSchema =
|
|
263
|
-
displayName:
|
|
264
|
-
canonicalDomain:
|
|
265
|
-
ownedDomains:
|
|
266
|
-
country:
|
|
267
|
-
language:
|
|
268
|
-
keywords:
|
|
269
|
-
competitors:
|
|
270
|
-
providers:
|
|
271
|
-
locations:
|
|
272
|
-
defaultLocation:
|
|
334
|
+
var configSpecSchema = z4.object({
|
|
335
|
+
displayName: z4.string().min(1),
|
|
336
|
+
canonicalDomain: z4.string().min(1),
|
|
337
|
+
ownedDomains: z4.array(z4.string().min(1)).optional().default([]),
|
|
338
|
+
country: z4.string().length(2),
|
|
339
|
+
language: z4.string().min(2),
|
|
340
|
+
keywords: z4.array(z4.string().min(1)).optional().default([]),
|
|
341
|
+
competitors: z4.array(z4.string().min(1)).optional().default([]),
|
|
342
|
+
providers: z4.array(providerNameSchema).optional().default([]),
|
|
343
|
+
locations: z4.array(locationContextSchema).optional().default([]),
|
|
344
|
+
defaultLocation: z4.string().optional(),
|
|
273
345
|
schedule: configScheduleSchema,
|
|
274
|
-
notifications:
|
|
346
|
+
notifications: z4.array(configNotificationSchema).optional().default([]),
|
|
275
347
|
google: configGoogleSchema
|
|
348
|
+
}).superRefine((spec, ctx) => {
|
|
349
|
+
const duplicateLabels = findDuplicateLocationLabels(spec.locations);
|
|
350
|
+
if (duplicateLabels.length > 0) {
|
|
351
|
+
ctx.addIssue({
|
|
352
|
+
code: "custom",
|
|
353
|
+
message: `Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`,
|
|
354
|
+
path: ["locations"]
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (!hasLocationLabel(spec.locations, spec.defaultLocation)) {
|
|
358
|
+
ctx.addIssue({
|
|
359
|
+
code: "custom",
|
|
360
|
+
message: `defaultLocation "${spec.defaultLocation}" must match a configured location label`,
|
|
361
|
+
path: ["defaultLocation"]
|
|
362
|
+
});
|
|
363
|
+
}
|
|
276
364
|
});
|
|
277
|
-
var projectConfigSchema =
|
|
278
|
-
apiVersion:
|
|
279
|
-
kind:
|
|
365
|
+
var projectConfigSchema = z4.object({
|
|
366
|
+
apiVersion: z4.literal("canonry/v1"),
|
|
367
|
+
kind: z4.literal("Project"),
|
|
280
368
|
metadata: configMetadataSchema,
|
|
281
369
|
spec: configSpecSchema
|
|
282
370
|
});
|
|
283
371
|
|
|
284
|
-
// ../contracts/src/models.ts
|
|
285
|
-
var MODEL_REGISTRY = {
|
|
286
|
-
gemini: {
|
|
287
|
-
defaultModel: "gemini-3-flash",
|
|
288
|
-
validationPattern: /^gemini-/,
|
|
289
|
-
validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
|
|
290
|
-
knownModels: [
|
|
291
|
-
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
|
|
292
|
-
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
|
|
293
|
-
{ id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
|
|
294
|
-
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
|
|
295
|
-
]
|
|
296
|
-
},
|
|
297
|
-
openai: {
|
|
298
|
-
defaultModel: "gpt-5.4",
|
|
299
|
-
validationPattern: /^(gpt-|o\d)/,
|
|
300
|
-
validationHint: "expected a GPT or o-series model name (e.g. gpt-5.4, o3)",
|
|
301
|
-
knownModels: [
|
|
302
|
-
{ id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
|
|
303
|
-
{ id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
|
|
304
|
-
{ id: "gpt-5-mini", displayName: "GPT-5 Mini", tier: "fast" },
|
|
305
|
-
{ id: "gpt-5-nano", displayName: "GPT-5 Nano", tier: "economy" },
|
|
306
|
-
{ id: "gpt-5", displayName: "GPT-5", tier: "standard" },
|
|
307
|
-
{ id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
|
|
308
|
-
]
|
|
309
|
-
},
|
|
310
|
-
claude: {
|
|
311
|
-
defaultModel: "claude-sonnet-4-6",
|
|
312
|
-
validationPattern: /^claude-/,
|
|
313
|
-
validationHint: 'model name must start with "claude-" (e.g. claude-sonnet-4-6)',
|
|
314
|
-
knownModels: [
|
|
315
|
-
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6", tier: "flagship" },
|
|
316
|
-
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: "standard" },
|
|
317
|
-
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
|
|
318
|
-
]
|
|
319
|
-
},
|
|
320
|
-
local: {
|
|
321
|
-
defaultModel: "llama3",
|
|
322
|
-
validationPattern: /./,
|
|
323
|
-
validationHint: "any model name accepted",
|
|
324
|
-
knownModels: [
|
|
325
|
-
{ id: "llama3", displayName: "Llama 3", tier: "standard" }
|
|
326
|
-
]
|
|
327
|
-
},
|
|
328
|
-
"cdp:chatgpt": {
|
|
329
|
-
defaultModel: "chatgpt-web",
|
|
330
|
-
validationPattern: /./,
|
|
331
|
-
validationHint: "model is detected from the ChatGPT web UI",
|
|
332
|
-
knownModels: [
|
|
333
|
-
{ id: "chatgpt-web", displayName: "ChatGPT (Web UI)", tier: "standard" }
|
|
334
|
-
]
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
function getDefaultModel(provider) {
|
|
338
|
-
return MODEL_REGISTRY[provider].defaultModel;
|
|
339
|
-
}
|
|
340
|
-
function isValidModelName(provider, model) {
|
|
341
|
-
return MODEL_REGISTRY[provider].validationPattern.test(model);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
372
|
// ../contracts/src/errors.ts
|
|
345
373
|
var AppError = class extends Error {
|
|
346
374
|
code;
|
|
@@ -384,177 +412,135 @@ function runNotCancellable(runId, status) {
|
|
|
384
412
|
function unsupportedKind(kind) {
|
|
385
413
|
return new AppError("UNSUPPORTED_KIND", `Kind '${kind}' is not supported in this version`, 400);
|
|
386
414
|
}
|
|
415
|
+
function notImplemented(message) {
|
|
416
|
+
return new AppError("NOT_IMPLEMENTED", message, 501);
|
|
417
|
+
}
|
|
387
418
|
|
|
388
419
|
// ../contracts/src/google.ts
|
|
389
|
-
import { z as z4 } from "zod";
|
|
390
|
-
var googleConnectionTypeSchema = z4.enum(["gsc", "ga4"]);
|
|
391
|
-
var googleConnectionDtoSchema = z4.object({
|
|
392
|
-
id: z4.string(),
|
|
393
|
-
domain: z4.string(),
|
|
394
|
-
connectionType: googleConnectionTypeSchema,
|
|
395
|
-
propertyId: z4.string().nullable().optional(),
|
|
396
|
-
sitemapUrl: z4.string().nullable().optional(),
|
|
397
|
-
scopes: z4.array(z4.string()).default([]),
|
|
398
|
-
createdAt: z4.string(),
|
|
399
|
-
updatedAt: z4.string()
|
|
400
|
-
});
|
|
401
|
-
var gscSearchDataDtoSchema = z4.object({
|
|
402
|
-
date: z4.string(),
|
|
403
|
-
query: z4.string(),
|
|
404
|
-
page: z4.string(),
|
|
405
|
-
country: z4.string().nullable().optional(),
|
|
406
|
-
device: z4.string().nullable().optional(),
|
|
407
|
-
clicks: z4.number(),
|
|
408
|
-
impressions: z4.number(),
|
|
409
|
-
ctr: z4.number(),
|
|
410
|
-
position: z4.number()
|
|
411
|
-
});
|
|
412
|
-
var gscUrlInspectionDtoSchema = z4.object({
|
|
413
|
-
id: z4.string(),
|
|
414
|
-
url: z4.string(),
|
|
415
|
-
indexingState: z4.string().nullable().optional(),
|
|
416
|
-
verdict: z4.string().nullable().optional(),
|
|
417
|
-
coverageState: z4.string().nullable().optional(),
|
|
418
|
-
pageFetchState: z4.string().nullable().optional(),
|
|
419
|
-
robotsTxtState: z4.string().nullable().optional(),
|
|
420
|
-
crawlTime: z4.string().nullable().optional(),
|
|
421
|
-
lastCrawlResult: z4.string().nullable().optional(),
|
|
422
|
-
isMobileFriendly: z4.boolean().nullable().optional(),
|
|
423
|
-
richResults: z4.array(z4.string()).default([]),
|
|
424
|
-
inspectedAt: z4.string()
|
|
425
|
-
});
|
|
426
|
-
var indexTransitionSchema = z4.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
427
|
-
var gscDeindexedRowSchema = z4.object({
|
|
428
|
-
url: z4.string(),
|
|
429
|
-
previousState: z4.string().nullable(),
|
|
430
|
-
currentState: z4.string().nullable(),
|
|
431
|
-
transitionDate: z4.string()
|
|
432
|
-
});
|
|
433
|
-
var gscReasonGroupSchema = z4.object({
|
|
434
|
-
reason: z4.string(),
|
|
435
|
-
count: z4.number(),
|
|
436
|
-
urls: z4.array(gscUrlInspectionDtoSchema).default([])
|
|
437
|
-
});
|
|
438
|
-
var gscCoverageSummaryDtoSchema = z4.object({
|
|
439
|
-
summary: z4.object({
|
|
440
|
-
total: z4.number(),
|
|
441
|
-
indexed: z4.number(),
|
|
442
|
-
notIndexed: z4.number(),
|
|
443
|
-
deindexed: z4.number(),
|
|
444
|
-
percentage: z4.number()
|
|
445
|
-
}),
|
|
446
|
-
lastInspectedAt: z4.string().nullable(),
|
|
447
|
-
indexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
448
|
-
notIndexed: z4.array(gscUrlInspectionDtoSchema).default([]),
|
|
449
|
-
deindexed: z4.array(gscDeindexedRowSchema).default([]),
|
|
450
|
-
reasonGroups: z4.array(gscReasonGroupSchema).default([])
|
|
451
|
-
});
|
|
452
|
-
var indexingNotificationDtoSchema = z4.object({
|
|
453
|
-
url: z4.string(),
|
|
454
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
455
|
-
notifiedAt: z4.string()
|
|
456
|
-
});
|
|
457
|
-
var indexingRequestResultDtoSchema = z4.object({
|
|
458
|
-
url: z4.string(),
|
|
459
|
-
type: z4.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
460
|
-
notifiedAt: z4.string(),
|
|
461
|
-
status: z4.enum(["success", "error"]),
|
|
462
|
-
error: z4.string().optional()
|
|
463
|
-
});
|
|
464
|
-
var gscCoverageSnapshotDtoSchema = z4.object({
|
|
465
|
-
date: z4.string(),
|
|
466
|
-
indexed: z4.number(),
|
|
467
|
-
notIndexed: z4.number(),
|
|
468
|
-
reasonBreakdown: z4.record(z4.string(), z4.number()).default({})
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// ../contracts/src/bing.ts
|
|
472
420
|
import { z as z5 } from "zod";
|
|
473
|
-
var
|
|
421
|
+
var googleConnectionTypeSchema = z5.enum(["gsc", "ga4"]);
|
|
422
|
+
var googleConnectionDtoSchema = z5.object({
|
|
474
423
|
id: z5.string(),
|
|
475
424
|
domain: z5.string(),
|
|
476
|
-
|
|
425
|
+
connectionType: googleConnectionTypeSchema,
|
|
426
|
+
propertyId: z5.string().nullable().optional(),
|
|
427
|
+
sitemapUrl: z5.string().nullable().optional(),
|
|
428
|
+
scopes: z5.array(z5.string()).default([]),
|
|
477
429
|
createdAt: z5.string(),
|
|
478
430
|
updatedAt: z5.string()
|
|
479
431
|
});
|
|
480
|
-
var
|
|
432
|
+
var gscSearchDataDtoSchema = z5.object({
|
|
433
|
+
date: z5.string(),
|
|
434
|
+
query: z5.string(),
|
|
435
|
+
page: z5.string(),
|
|
436
|
+
country: z5.string().nullable().optional(),
|
|
437
|
+
device: z5.string().nullable().optional(),
|
|
438
|
+
clicks: z5.number(),
|
|
439
|
+
impressions: z5.number(),
|
|
440
|
+
ctr: z5.number(),
|
|
441
|
+
position: z5.number()
|
|
442
|
+
});
|
|
443
|
+
var gscUrlInspectionDtoSchema = z5.object({
|
|
481
444
|
id: z5.string(),
|
|
482
445
|
url: z5.string(),
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
446
|
+
indexingState: z5.string().nullable().optional(),
|
|
447
|
+
verdict: z5.string().nullable().optional(),
|
|
448
|
+
coverageState: z5.string().nullable().optional(),
|
|
449
|
+
pageFetchState: z5.string().nullable().optional(),
|
|
450
|
+
robotsTxtState: z5.string().nullable().optional(),
|
|
451
|
+
crawlTime: z5.string().nullable().optional(),
|
|
452
|
+
lastCrawlResult: z5.string().nullable().optional(),
|
|
453
|
+
isMobileFriendly: z5.boolean().nullable().optional(),
|
|
454
|
+
richResults: z5.array(z5.string()).default([]),
|
|
487
455
|
inspectedAt: z5.string()
|
|
488
456
|
});
|
|
489
|
-
var
|
|
457
|
+
var indexTransitionSchema = z5.enum(["stable", "reindexed", "deindexed", "still-missing", "new"]);
|
|
458
|
+
var gscDeindexedRowSchema = z5.object({
|
|
459
|
+
url: z5.string(),
|
|
460
|
+
previousState: z5.string().nullable(),
|
|
461
|
+
currentState: z5.string().nullable(),
|
|
462
|
+
transitionDate: z5.string()
|
|
463
|
+
});
|
|
464
|
+
var gscReasonGroupSchema = z5.object({
|
|
465
|
+
reason: z5.string(),
|
|
466
|
+
count: z5.number(),
|
|
467
|
+
urls: z5.array(gscUrlInspectionDtoSchema).default([])
|
|
468
|
+
});
|
|
469
|
+
var gscCoverageSummaryDtoSchema = z5.object({
|
|
490
470
|
summary: z5.object({
|
|
491
471
|
total: z5.number(),
|
|
492
472
|
indexed: z5.number(),
|
|
493
473
|
notIndexed: z5.number(),
|
|
474
|
+
deindexed: z5.number(),
|
|
494
475
|
percentage: z5.number()
|
|
495
476
|
}),
|
|
496
477
|
lastInspectedAt: z5.string().nullable(),
|
|
497
|
-
indexed: z5.array(
|
|
498
|
-
notIndexed: z5.array(
|
|
478
|
+
indexed: z5.array(gscUrlInspectionDtoSchema).default([]),
|
|
479
|
+
notIndexed: z5.array(gscUrlInspectionDtoSchema).default([]),
|
|
480
|
+
deindexed: z5.array(gscDeindexedRowSchema).default([]),
|
|
481
|
+
reasonGroups: z5.array(gscReasonGroupSchema).default([])
|
|
499
482
|
});
|
|
500
|
-
var
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
ctr: z5.number(),
|
|
505
|
-
averagePosition: z5.number()
|
|
483
|
+
var indexingNotificationDtoSchema = z5.object({
|
|
484
|
+
url: z5.string(),
|
|
485
|
+
type: z5.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
486
|
+
notifiedAt: z5.string()
|
|
506
487
|
});
|
|
507
|
-
var
|
|
488
|
+
var indexingRequestResultDtoSchema = z5.object({
|
|
508
489
|
url: z5.string(),
|
|
490
|
+
type: z5.enum(["URL_UPDATED", "URL_DELETED"]),
|
|
491
|
+
notifiedAt: z5.string(),
|
|
509
492
|
status: z5.enum(["success", "error"]),
|
|
510
|
-
submittedAt: z5.string(),
|
|
511
493
|
error: z5.string().optional()
|
|
512
494
|
});
|
|
495
|
+
var gscCoverageSnapshotDtoSchema = z5.object({
|
|
496
|
+
date: z5.string(),
|
|
497
|
+
indexed: z5.number(),
|
|
498
|
+
notIndexed: z5.number(),
|
|
499
|
+
reasonBreakdown: z5.record(z5.string(), z5.number()).default({})
|
|
500
|
+
});
|
|
513
501
|
|
|
514
|
-
// ../contracts/src/
|
|
502
|
+
// ../contracts/src/bing.ts
|
|
515
503
|
import { z as z6 } from "zod";
|
|
516
|
-
var
|
|
517
|
-
var projectDtoSchema = z6.object({
|
|
504
|
+
var bingConnectionDtoSchema = z6.object({
|
|
518
505
|
id: z6.string(),
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
506
|
+
domain: z6.string(),
|
|
507
|
+
siteUrl: z6.string().nullable().optional(),
|
|
508
|
+
createdAt: z6.string(),
|
|
509
|
+
updatedAt: z6.string()
|
|
510
|
+
});
|
|
511
|
+
var bingUrlInspectionDtoSchema = z6.object({
|
|
512
|
+
id: z6.string(),
|
|
513
|
+
url: z6.string(),
|
|
514
|
+
httpCode: z6.number().nullable().optional(),
|
|
515
|
+
inIndex: z6.boolean().nullable().optional(),
|
|
516
|
+
lastCrawledDate: z6.string().nullable().optional(),
|
|
517
|
+
inIndexDate: z6.string().nullable().optional(),
|
|
518
|
+
inspectedAt: z6.string()
|
|
519
|
+
});
|
|
520
|
+
var bingCoverageSummaryDtoSchema = z6.object({
|
|
521
|
+
summary: z6.object({
|
|
522
|
+
total: z6.number(),
|
|
523
|
+
indexed: z6.number(),
|
|
524
|
+
notIndexed: z6.number(),
|
|
525
|
+
percentage: z6.number()
|
|
526
|
+
}),
|
|
527
|
+
lastInspectedAt: z6.string().nullable(),
|
|
528
|
+
indexed: z6.array(bingUrlInspectionDtoSchema).default([]),
|
|
529
|
+
notIndexed: z6.array(bingUrlInspectionDtoSchema).default([])
|
|
530
|
+
});
|
|
531
|
+
var bingKeywordStatsDtoSchema = z6.object({
|
|
532
|
+
query: z6.string(),
|
|
533
|
+
impressions: z6.number(),
|
|
534
|
+
clicks: z6.number(),
|
|
535
|
+
ctr: z6.number(),
|
|
536
|
+
averagePosition: z6.number()
|
|
537
|
+
});
|
|
538
|
+
var bingSubmitResultDtoSchema = z6.object({
|
|
539
|
+
url: z6.string(),
|
|
540
|
+
status: z6.enum(["success", "error"]),
|
|
541
|
+
submittedAt: z6.string(),
|
|
542
|
+
error: z6.string().optional()
|
|
533
543
|
});
|
|
534
|
-
function normalizeProjectDomain(input) {
|
|
535
|
-
let domain = input.trim().toLowerCase();
|
|
536
|
-
try {
|
|
537
|
-
if (domain.includes("://")) {
|
|
538
|
-
domain = new URL(domain).hostname.toLowerCase();
|
|
539
|
-
}
|
|
540
|
-
} catch {
|
|
541
|
-
}
|
|
542
|
-
return domain.replace(/^www\./, "");
|
|
543
|
-
}
|
|
544
|
-
function effectiveDomains(project) {
|
|
545
|
-
const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
|
|
546
|
-
const seen = /* @__PURE__ */ new Set();
|
|
547
|
-
const result = [];
|
|
548
|
-
for (const d of all) {
|
|
549
|
-
const trimmed = d.trim();
|
|
550
|
-
if (!trimmed) continue;
|
|
551
|
-
const norm = normalizeProjectDomain(trimmed);
|
|
552
|
-
if (seen.has(norm)) continue;
|
|
553
|
-
seen.add(norm);
|
|
554
|
-
result.push(trimmed);
|
|
555
|
-
}
|
|
556
|
-
return result;
|
|
557
|
-
}
|
|
558
544
|
|
|
559
545
|
// ../contracts/src/run.ts
|
|
560
546
|
import { z as z7 } from "zod";
|
|
@@ -622,6 +608,16 @@ var scheduleDtoSchema = z8.object({
|
|
|
622
608
|
createdAt: z8.string(),
|
|
623
609
|
updatedAt: z8.string()
|
|
624
610
|
});
|
|
611
|
+
var scheduleUpsertRequestSchema = z8.object({
|
|
612
|
+
preset: z8.string().optional(),
|
|
613
|
+
cron: z8.string().optional(),
|
|
614
|
+
timezone: z8.string().optional().default("UTC"),
|
|
615
|
+
enabled: z8.boolean().optional().default(true),
|
|
616
|
+
providers: z8.array(providerNameSchema).optional().default([])
|
|
617
|
+
}).refine(
|
|
618
|
+
(data) => data.preset && !data.cron || !data.preset && data.cron,
|
|
619
|
+
{ message: 'Exactly one of "preset" or "cron" must be provided' }
|
|
620
|
+
);
|
|
625
621
|
|
|
626
622
|
// ../contracts/src/source-categories.ts
|
|
627
623
|
var SOURCE_CATEGORY_RULES = [
|
|
@@ -1214,7 +1210,44 @@ var MIGRATIONS = [
|
|
|
1214
1210
|
// v10: Add sitemapUrl to google_connections for persistent sitemap storage
|
|
1215
1211
|
`ALTER TABLE google_connections ADD COLUMN sitemap_url TEXT`,
|
|
1216
1212
|
// v11: CDP browser provider — screenshot path for captured evidence
|
|
1217
|
-
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT
|
|
1213
|
+
`ALTER TABLE query_snapshots ADD COLUMN screenshot_path TEXT`,
|
|
1214
|
+
// v12: Bing Webmaster Tools — bing_connections table
|
|
1215
|
+
`CREATE TABLE IF NOT EXISTS bing_connections (
|
|
1216
|
+
id TEXT PRIMARY KEY,
|
|
1217
|
+
domain TEXT NOT NULL,
|
|
1218
|
+
site_url TEXT,
|
|
1219
|
+
created_at TEXT NOT NULL,
|
|
1220
|
+
updated_at TEXT NOT NULL
|
|
1221
|
+
)`,
|
|
1222
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS idx_bing_conn_domain ON bing_connections(domain)`,
|
|
1223
|
+
// v12: Bing Webmaster Tools — bing_url_inspections table
|
|
1224
|
+
`CREATE TABLE IF NOT EXISTS bing_url_inspections (
|
|
1225
|
+
id TEXT PRIMARY KEY,
|
|
1226
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1227
|
+
url TEXT NOT NULL,
|
|
1228
|
+
http_code INTEGER,
|
|
1229
|
+
in_index INTEGER,
|
|
1230
|
+
last_crawled_date TEXT,
|
|
1231
|
+
in_index_date TEXT,
|
|
1232
|
+
inspected_at TEXT NOT NULL,
|
|
1233
|
+
created_at TEXT NOT NULL
|
|
1234
|
+
)`,
|
|
1235
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_inspect_project_url ON bing_url_inspections(project_id, url)`,
|
|
1236
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_inspect_url_time ON bing_url_inspections(url, inspected_at)`,
|
|
1237
|
+
// v12: Bing Webmaster Tools — bing_keyword_stats table
|
|
1238
|
+
`CREATE TABLE IF NOT EXISTS bing_keyword_stats (
|
|
1239
|
+
id TEXT PRIMARY KEY,
|
|
1240
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
1241
|
+
query TEXT NOT NULL,
|
|
1242
|
+
impressions INTEGER NOT NULL DEFAULT 0,
|
|
1243
|
+
clicks INTEGER NOT NULL DEFAULT 0,
|
|
1244
|
+
ctr TEXT NOT NULL DEFAULT '0',
|
|
1245
|
+
average_position TEXT NOT NULL DEFAULT '0',
|
|
1246
|
+
synced_at TEXT NOT NULL,
|
|
1247
|
+
created_at TEXT NOT NULL
|
|
1248
|
+
)`,
|
|
1249
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_project ON bing_keyword_stats(project_id)`,
|
|
1250
|
+
`CREATE INDEX IF NOT EXISTS idx_bing_keyword_query ON bing_keyword_stats(query)`
|
|
1218
1251
|
];
|
|
1219
1252
|
function migrate(db) {
|
|
1220
1253
|
const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -1297,17 +1330,46 @@ function writeAuditLog(db, entry) {
|
|
|
1297
1330
|
async function projectRoutes(app, opts) {
|
|
1298
1331
|
app.put("/projects/:name", async (request, reply) => {
|
|
1299
1332
|
const { name } = request.params;
|
|
1300
|
-
const
|
|
1301
|
-
if (!
|
|
1302
|
-
const err = validationError("
|
|
1333
|
+
const parsedBody = projectUpsertRequestSchema.safeParse(request.body);
|
|
1334
|
+
if (!parsedBody.success) {
|
|
1335
|
+
const err = validationError("Invalid project payload", {
|
|
1336
|
+
issues: parsedBody.error.issues.map((issue) => ({
|
|
1337
|
+
path: issue.path.join("."),
|
|
1338
|
+
message: issue.message
|
|
1339
|
+
}))
|
|
1340
|
+
});
|
|
1303
1341
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1304
1342
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1343
|
+
const body = parsedBody.data;
|
|
1344
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1345
|
+
if (validNames.length && body.providers?.length) {
|
|
1346
|
+
const invalid = body.providers.filter((p) => !validNames.includes(p));
|
|
1347
|
+
if (invalid.length) {
|
|
1348
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
1349
|
+
invalidProviders: invalid,
|
|
1350
|
+
validProviders: validNames
|
|
1351
|
+
});
|
|
1352
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1353
|
+
}
|
|
1308
1354
|
}
|
|
1309
1355
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1310
1356
|
const existing = app.db.select().from(projects).where(eq3(projects.name, name)).get();
|
|
1357
|
+
const existingLocations = existing ? JSON.parse(existing.locations || "[]") : [];
|
|
1358
|
+
const nextLocations = body.locations ?? existingLocations;
|
|
1359
|
+
const duplicateLabels = findDuplicateLocationLabels(nextLocations);
|
|
1360
|
+
if (duplicateLabels.length > 0) {
|
|
1361
|
+
const err = validationError(`Duplicate location labels are not allowed: ${duplicateLabels.join(", ")}`, {
|
|
1362
|
+
duplicateLabels
|
|
1363
|
+
});
|
|
1364
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1365
|
+
}
|
|
1366
|
+
const nextDefaultLocation = body.defaultLocation !== void 0 ? body.defaultLocation ?? null : existing?.defaultLocation ?? null;
|
|
1367
|
+
if (!hasLocationLabel(nextLocations, nextDefaultLocation)) {
|
|
1368
|
+
const err = validationError(`defaultLocation "${nextDefaultLocation}" must match a configured location label`, {
|
|
1369
|
+
defaultLocation: nextDefaultLocation
|
|
1370
|
+
});
|
|
1371
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1372
|
+
}
|
|
1311
1373
|
if (existing) {
|
|
1312
1374
|
app.db.update(projects).set({
|
|
1313
1375
|
displayName: body.displayName,
|
|
@@ -1318,8 +1380,8 @@ async function projectRoutes(app, opts) {
|
|
|
1318
1380
|
tags: JSON.stringify(body.tags ?? []),
|
|
1319
1381
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1320
1382
|
providers: JSON.stringify(body.providers ?? []),
|
|
1321
|
-
locations: JSON.stringify(
|
|
1322
|
-
defaultLocation:
|
|
1383
|
+
locations: JSON.stringify(nextLocations),
|
|
1384
|
+
defaultLocation: nextDefaultLocation,
|
|
1323
1385
|
configSource: body.configSource ?? "api",
|
|
1324
1386
|
configRevision: existing.configRevision + 1,
|
|
1325
1387
|
updatedAt: now
|
|
@@ -1346,8 +1408,8 @@ async function projectRoutes(app, opts) {
|
|
|
1346
1408
|
tags: JSON.stringify(body.tags ?? []),
|
|
1347
1409
|
labels: JSON.stringify(body.labels ?? {}),
|
|
1348
1410
|
providers: JSON.stringify(body.providers ?? []),
|
|
1349
|
-
locations: JSON.stringify(
|
|
1350
|
-
defaultLocation:
|
|
1411
|
+
locations: JSON.stringify(nextLocations),
|
|
1412
|
+
defaultLocation: nextDefaultLocation,
|
|
1351
1413
|
configSource: body.configSource ?? "api",
|
|
1352
1414
|
configRevision: 1,
|
|
1353
1415
|
createdAt: now,
|
|
@@ -1712,9 +1774,13 @@ async function keywordRoutes(app, opts) {
|
|
|
1712
1774
|
const err = validationError('Body must contain a "provider" string');
|
|
1713
1775
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1714
1776
|
}
|
|
1715
|
-
const provider =
|
|
1716
|
-
|
|
1717
|
-
|
|
1777
|
+
const provider = body.provider.trim().toLowerCase();
|
|
1778
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1779
|
+
if (validNames.length && !validNames.includes(provider)) {
|
|
1780
|
+
const err = validationError(`Unknown provider "${body.provider}". Valid providers: ${validNames.join(", ")}`, {
|
|
1781
|
+
provider: body.provider,
|
|
1782
|
+
validProviders: validNames
|
|
1783
|
+
});
|
|
1718
1784
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
1719
1785
|
}
|
|
1720
1786
|
if (body.count !== void 0 && (typeof body.count !== "number" || !Number.isFinite(body.count) || !Number.isInteger(body.count))) {
|
|
@@ -1723,7 +1789,8 @@ async function keywordRoutes(app, opts) {
|
|
|
1723
1789
|
}
|
|
1724
1790
|
const count = Math.min(Math.max(body.count ?? 5, 1), 20);
|
|
1725
1791
|
if (!opts.onGenerateKeywords) {
|
|
1726
|
-
|
|
1792
|
+
const err = notImplemented("Key phrase generation is not supported in this deployment");
|
|
1793
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1727
1794
|
}
|
|
1728
1795
|
const existingRows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
|
|
1729
1796
|
const existingKeywords = existingRows.map((r) => r.keyword);
|
|
@@ -1739,7 +1806,10 @@ async function keywordRoutes(app, opts) {
|
|
|
1739
1806
|
} catch (err) {
|
|
1740
1807
|
request.log.error({ err }, "Key phrase generation failed");
|
|
1741
1808
|
return reply.status(500).send({
|
|
1742
|
-
error:
|
|
1809
|
+
error: {
|
|
1810
|
+
code: "INTERNAL_ERROR",
|
|
1811
|
+
message: err instanceof Error ? err.message : "Failed to generate key phrases"
|
|
1812
|
+
}
|
|
1743
1813
|
});
|
|
1744
1814
|
}
|
|
1745
1815
|
});
|
|
@@ -1813,7 +1883,7 @@ function resolveProjectSafe2(app, name, reply) {
|
|
|
1813
1883
|
|
|
1814
1884
|
// ../api-routes/src/runs.ts
|
|
1815
1885
|
import crypto8 from "crypto";
|
|
1816
|
-
import { eq as eq7, asc } from "drizzle-orm";
|
|
1886
|
+
import { eq as eq7, asc, desc } from "drizzle-orm";
|
|
1817
1887
|
|
|
1818
1888
|
// ../api-routes/src/run-queue.ts
|
|
1819
1889
|
import crypto7 from "crypto";
|
|
@@ -1860,12 +1930,19 @@ async function runRoutes(app, opts) {
|
|
|
1860
1930
|
const trigger = request.body?.trigger ?? "manual";
|
|
1861
1931
|
const rawProviders = request.body?.providers;
|
|
1862
1932
|
if (rawProviders?.length) {
|
|
1863
|
-
const
|
|
1864
|
-
const
|
|
1865
|
-
if (
|
|
1866
|
-
|
|
1933
|
+
const normalized = rawProviders.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
1934
|
+
const validNames = opts.validProviderNames ?? [];
|
|
1935
|
+
if (validNames.length) {
|
|
1936
|
+
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
1937
|
+
if (invalid.length) {
|
|
1938
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
1939
|
+
invalidProviders: invalid,
|
|
1940
|
+
validProviders: validNames
|
|
1941
|
+
});
|
|
1942
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
1943
|
+
}
|
|
1867
1944
|
}
|
|
1868
|
-
rawProviders.splice(0, rawProviders.length, ...
|
|
1945
|
+
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
1869
1946
|
}
|
|
1870
1947
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
1871
1948
|
let resolvedLocation;
|
|
@@ -1944,7 +2021,9 @@ async function runRoutes(app, opts) {
|
|
|
1944
2021
|
app.get("/projects/:name/runs", async (request, reply) => {
|
|
1945
2022
|
const project = resolveProjectSafe3(app, request.params.name, reply);
|
|
1946
2023
|
if (!project) return;
|
|
1947
|
-
const
|
|
2024
|
+
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
2025
|
+
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
|
|
2026
|
+
const rows = limit == null ? app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
|
|
1948
2027
|
return reply.send(rows.map(formatRun));
|
|
1949
2028
|
});
|
|
1950
2029
|
app.get("/runs", async (_request, reply) => {
|
|
@@ -1963,12 +2042,19 @@ async function runRoutes(app, opts) {
|
|
|
1963
2042
|
}
|
|
1964
2043
|
const rawProviders = request.body?.providers;
|
|
1965
2044
|
if (rawProviders?.length) {
|
|
1966
|
-
const
|
|
1967
|
-
const
|
|
1968
|
-
if (
|
|
1969
|
-
|
|
2045
|
+
const normalized = rawProviders.map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
2046
|
+
const validNames = opts.validProviderNames ?? [];
|
|
2047
|
+
if (validNames.length) {
|
|
2048
|
+
const invalid = normalized.filter((p) => !validNames.includes(p));
|
|
2049
|
+
if (invalid.length) {
|
|
2050
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2051
|
+
invalidProviders: invalid,
|
|
2052
|
+
validProviders: validNames
|
|
2053
|
+
});
|
|
2054
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2055
|
+
}
|
|
1970
2056
|
}
|
|
1971
|
-
rawProviders.splice(0, rawProviders.length, ...
|
|
2057
|
+
rawProviders.splice(0, rawProviders.length, ...normalized);
|
|
1972
2058
|
}
|
|
1973
2059
|
const providers = rawProviders?.length ? rawProviders : void 0;
|
|
1974
2060
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2371,6 +2457,23 @@ async function applyRoutes(app, opts) {
|
|
|
2371
2457
|
return reply.status(err.statusCode).send(err.toJSON());
|
|
2372
2458
|
}
|
|
2373
2459
|
const config = parsed.data;
|
|
2460
|
+
const validNames = opts?.validProviderNames ?? [];
|
|
2461
|
+
if (validNames.length) {
|
|
2462
|
+
const allProviders = [
|
|
2463
|
+
...config.spec.providers ?? [],
|
|
2464
|
+
...config.spec.schedule?.providers ?? []
|
|
2465
|
+
];
|
|
2466
|
+
if (allProviders.length) {
|
|
2467
|
+
const invalid = allProviders.filter((p) => !validNames.includes(p));
|
|
2468
|
+
if (invalid.length) {
|
|
2469
|
+
const err = validationError(`Invalid provider(s): ${[...new Set(invalid)].join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
2470
|
+
invalidProviders: [...new Set(invalid)],
|
|
2471
|
+
validProviders: validNames
|
|
2472
|
+
});
|
|
2473
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2374
2477
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2375
2478
|
const name = config.metadata.name;
|
|
2376
2479
|
const existing = app.db.select().from(projects).where(eq8(projects.name, name)).get();
|
|
@@ -2578,16 +2681,16 @@ async function applyRoutes(app, opts) {
|
|
|
2578
2681
|
}
|
|
2579
2682
|
|
|
2580
2683
|
// ../api-routes/src/history.ts
|
|
2581
|
-
import { eq as eq9, desc, inArray } from "drizzle-orm";
|
|
2684
|
+
import { eq as eq9, desc as desc2, inArray } from "drizzle-orm";
|
|
2582
2685
|
async function historyRoutes(app) {
|
|
2583
2686
|
app.get("/projects/:name/history", async (request, reply) => {
|
|
2584
2687
|
const project = resolveProjectSafe4(app, request.params.name, reply);
|
|
2585
2688
|
if (!project) return;
|
|
2586
|
-
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(
|
|
2689
|
+
const rows = app.db.select().from(auditLog).where(eq9(auditLog.projectId, project.id)).orderBy(desc2(auditLog.createdAt)).all();
|
|
2587
2690
|
return reply.send(rows.map(formatAuditEntry));
|
|
2588
2691
|
});
|
|
2589
2692
|
app.get("/history", async (_request, reply) => {
|
|
2590
|
-
const rows = app.db.select().from(auditLog).orderBy(
|
|
2693
|
+
const rows = app.db.select().from(auditLog).orderBy(desc2(auditLog.createdAt)).all();
|
|
2591
2694
|
return reply.send(rows.map(formatAuditEntry));
|
|
2592
2695
|
});
|
|
2593
2696
|
app.get("/projects/:name/snapshots", async (request, reply) => {
|
|
@@ -2612,7 +2715,7 @@ async function historyRoutes(app) {
|
|
|
2612
2715
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
2613
2716
|
location: querySnapshots.location,
|
|
2614
2717
|
createdAt: querySnapshots.createdAt
|
|
2615
|
-
}).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(
|
|
2718
|
+
}).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
|
|
2616
2719
|
const locationFilter = request.query.location;
|
|
2617
2720
|
const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
|
|
2618
2721
|
const total = filtered.length;
|
|
@@ -2793,7 +2896,7 @@ function resolveProjectSafe4(app, name, reply) {
|
|
|
2793
2896
|
}
|
|
2794
2897
|
|
|
2795
2898
|
// ../api-routes/src/analytics.ts
|
|
2796
|
-
import { eq as eq10, desc as
|
|
2899
|
+
import { eq as eq10, desc as desc3, inArray as inArray2 } from "drizzle-orm";
|
|
2797
2900
|
async function analyticsRoutes(app) {
|
|
2798
2901
|
app.get("/projects/:name/analytics/metrics", async (request, reply) => {
|
|
2799
2902
|
const project = resolveProjectSafe5(app, request.params.name, reply);
|
|
@@ -2837,7 +2940,7 @@ async function analyticsRoutes(app) {
|
|
|
2837
2940
|
if (!project) return;
|
|
2838
2941
|
const window = parseWindow(request.query.window);
|
|
2839
2942
|
const cutoff = windowCutoff(window);
|
|
2840
|
-
const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(
|
|
2943
|
+
const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().find((r) => r.status === "completed" || r.status === "partial");
|
|
2841
2944
|
if (!latestRun) {
|
|
2842
2945
|
return reply.send({ cited: [], gap: [], uncited: [], runId: "", window });
|
|
2843
2946
|
}
|
|
@@ -2919,7 +3022,7 @@ async function analyticsRoutes(app) {
|
|
|
2919
3022
|
if (!project) return;
|
|
2920
3023
|
const window = parseWindow(request.query.window);
|
|
2921
3024
|
const cutoff = windowCutoff(window);
|
|
2922
|
-
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(
|
|
3025
|
+
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
2923
3026
|
if (windowRuns.length === 0) {
|
|
2924
3027
|
return reply.send({ overall: [], byKeyword: {}, runId: "", window });
|
|
2925
3028
|
}
|
|
@@ -3130,7 +3233,7 @@ var providerNameParameter = {
|
|
|
3130
3233
|
in: "path",
|
|
3131
3234
|
required: true,
|
|
3132
3235
|
description: "Provider name.",
|
|
3133
|
-
schema: { type: "string", enum: ["gemini", "openai", "claude", "local"] }
|
|
3236
|
+
schema: { type: "string", enum: ["gemini", "openai", "claude", "perplexity", "local"] }
|
|
3134
3237
|
};
|
|
3135
3238
|
var locationLabelParameter = {
|
|
3136
3239
|
name: "label",
|
|
@@ -3439,7 +3542,7 @@ var routeCatalog = [
|
|
|
3439
3542
|
type: "object",
|
|
3440
3543
|
required: ["provider"],
|
|
3441
3544
|
properties: {
|
|
3442
|
-
provider: { type: "string", enum: ["gemini", "openai", "claude", "local"] },
|
|
3545
|
+
provider: { type: "string", enum: ["gemini", "openai", "claude", "perplexity", "local"] },
|
|
3443
3546
|
count: integerSchema
|
|
3444
3547
|
}
|
|
3445
3548
|
}
|
|
@@ -3518,7 +3621,7 @@ var routeCatalog = [
|
|
|
3518
3621
|
path: "/api/v1/projects/{name}/runs",
|
|
3519
3622
|
summary: "List project runs",
|
|
3520
3623
|
tags: ["runs"],
|
|
3521
|
-
parameters: [nameParameter],
|
|
3624
|
+
parameters: [nameParameter, limitQueryParameter],
|
|
3522
3625
|
responses: {
|
|
3523
3626
|
200: { description: "Runs returned." }
|
|
3524
3627
|
}
|
|
@@ -4601,31 +4704,40 @@ async function settingsRoutes(app, opts) {
|
|
|
4601
4704
|
bing: opts.bing ?? { configured: false }
|
|
4602
4705
|
}));
|
|
4603
4706
|
app.put("/settings/providers/:name", async (request, reply) => {
|
|
4604
|
-
const providerName = parseProviderName(request.params.name);
|
|
4605
4707
|
const { apiKey, baseUrl, model, quota } = request.body ?? {};
|
|
4606
|
-
|
|
4607
|
-
|
|
4708
|
+
const name = request.params.name;
|
|
4709
|
+
const adapters = opts.providerAdapters ?? [];
|
|
4710
|
+
const apiAdapters = adapters.filter((a) => a.mode === "api");
|
|
4711
|
+
const adapterInfo = apiAdapters.find((a) => a.name === name);
|
|
4712
|
+
if (!adapterInfo) {
|
|
4713
|
+
const validNames = apiAdapters.map((a) => a.name);
|
|
4714
|
+
const err = validationError(`Invalid provider: ${name}. Must be one of: ${validNames.join(", ")}`, {
|
|
4715
|
+
provider: name,
|
|
4716
|
+
validProviders: validNames
|
|
4717
|
+
});
|
|
4718
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4608
4719
|
}
|
|
4609
|
-
const name = providerName;
|
|
4610
4720
|
if (name === "local") {
|
|
4611
4721
|
if (!baseUrl || typeof baseUrl !== "string") {
|
|
4612
|
-
|
|
4722
|
+
const err = validationError("baseUrl is required for local provider");
|
|
4723
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4613
4724
|
}
|
|
4614
4725
|
} else {
|
|
4615
4726
|
if (!apiKey || typeof apiKey !== "string") {
|
|
4616
|
-
|
|
4727
|
+
const err = validationError("apiKey is required");
|
|
4728
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4617
4729
|
}
|
|
4618
4730
|
}
|
|
4619
4731
|
if (model !== void 0) {
|
|
4620
|
-
|
|
4621
|
-
if (!registry.validationPattern.test(model)) {
|
|
4732
|
+
if (!adapterInfo.modelValidationPattern.test(model)) {
|
|
4622
4733
|
return reply.status(400).send({
|
|
4623
|
-
error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${
|
|
4734
|
+
error: { code: "VALIDATION_ERROR", message: `Invalid model "${model}" for provider "${name}" \u2014 ${adapterInfo.modelValidationHint}` }
|
|
4624
4735
|
});
|
|
4625
4736
|
}
|
|
4626
4737
|
}
|
|
4627
4738
|
if (!opts.onProviderUpdate) {
|
|
4628
|
-
|
|
4739
|
+
const err = notImplemented("Provider configuration updates are not supported in this deployment");
|
|
4740
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4629
4741
|
}
|
|
4630
4742
|
if (quota !== void 0) {
|
|
4631
4743
|
if (typeof quota !== "object" || quota === null) {
|
|
@@ -4642,7 +4754,12 @@ async function settingsRoutes(app, opts) {
|
|
|
4642
4754
|
}
|
|
4643
4755
|
const result = opts.onProviderUpdate(name, apiKey ?? "", model, baseUrl, quota);
|
|
4644
4756
|
if (!result) {
|
|
4645
|
-
return reply.status(500).send({
|
|
4757
|
+
return reply.status(500).send({
|
|
4758
|
+
error: {
|
|
4759
|
+
code: "INTERNAL_ERROR",
|
|
4760
|
+
message: "Failed to update provider configuration"
|
|
4761
|
+
}
|
|
4762
|
+
});
|
|
4646
4763
|
}
|
|
4647
4764
|
return result;
|
|
4648
4765
|
});
|
|
@@ -4654,11 +4771,17 @@ async function settingsRoutes(app, opts) {
|
|
|
4654
4771
|
});
|
|
4655
4772
|
}
|
|
4656
4773
|
if (!opts.onGoogleUpdate) {
|
|
4657
|
-
|
|
4774
|
+
const err = notImplemented("Google OAuth configuration updates are not supported in this deployment");
|
|
4775
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4658
4776
|
}
|
|
4659
4777
|
const result = opts.onGoogleUpdate(clientId, clientSecret);
|
|
4660
4778
|
if (!result) {
|
|
4661
|
-
return reply.status(500).send({
|
|
4779
|
+
return reply.status(500).send({
|
|
4780
|
+
error: {
|
|
4781
|
+
code: "INTERNAL_ERROR",
|
|
4782
|
+
message: "Failed to update Google OAuth configuration"
|
|
4783
|
+
}
|
|
4784
|
+
});
|
|
4662
4785
|
}
|
|
4663
4786
|
return result;
|
|
4664
4787
|
});
|
|
@@ -4670,11 +4793,17 @@ async function settingsRoutes(app, opts) {
|
|
|
4670
4793
|
});
|
|
4671
4794
|
}
|
|
4672
4795
|
if (!opts.onBingUpdate) {
|
|
4673
|
-
|
|
4796
|
+
const err = notImplemented("Bing configuration updates are not supported in this deployment");
|
|
4797
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4674
4798
|
}
|
|
4675
4799
|
const result = opts.onBingUpdate(apiKey);
|
|
4676
4800
|
if (!result) {
|
|
4677
|
-
return reply.status(500).send({
|
|
4801
|
+
return reply.status(500).send({
|
|
4802
|
+
error: {
|
|
4803
|
+
code: "INTERNAL_ERROR",
|
|
4804
|
+
message: "Failed to update Bing configuration"
|
|
4805
|
+
}
|
|
4806
|
+
});
|
|
4678
4807
|
}
|
|
4679
4808
|
return result;
|
|
4680
4809
|
});
|
|
@@ -4684,7 +4813,8 @@ async function settingsRoutes(app, opts) {
|
|
|
4684
4813
|
async function telemetryRoutes(app, opts) {
|
|
4685
4814
|
app.get("/telemetry", async (_request, reply) => {
|
|
4686
4815
|
if (!opts.getTelemetryStatus) {
|
|
4687
|
-
|
|
4816
|
+
const err = notImplemented("Telemetry status is not available in this deployment");
|
|
4817
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4688
4818
|
}
|
|
4689
4819
|
const status = opts.getTelemetryStatus();
|
|
4690
4820
|
return {
|
|
@@ -4694,11 +4824,13 @@ async function telemetryRoutes(app, opts) {
|
|
|
4694
4824
|
});
|
|
4695
4825
|
app.put("/telemetry", async (request, reply) => {
|
|
4696
4826
|
if (!opts.setTelemetryEnabled) {
|
|
4697
|
-
|
|
4827
|
+
const err = notImplemented("Telemetry configuration is not available in this deployment");
|
|
4828
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4698
4829
|
}
|
|
4699
4830
|
const { enabled } = request.body ?? {};
|
|
4700
4831
|
if (typeof enabled !== "boolean") {
|
|
4701
|
-
|
|
4832
|
+
const err = validationError("enabled (boolean) is required");
|
|
4833
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4702
4834
|
}
|
|
4703
4835
|
opts.setTelemetryEnabled(enabled);
|
|
4704
4836
|
const status = opts.getTelemetryStatus?.();
|
|
@@ -4716,11 +4848,27 @@ async function scheduleRoutes(app, opts) {
|
|
|
4716
4848
|
app.put("/projects/:name/schedule", async (request, reply) => {
|
|
4717
4849
|
const project = resolveProjectSafe6(app, request.params.name, reply);
|
|
4718
4850
|
if (!project) return;
|
|
4719
|
-
const
|
|
4720
|
-
if (!
|
|
4721
|
-
|
|
4722
|
-
|
|
4851
|
+
const parsedBody = scheduleUpsertRequestSchema.safeParse(request.body);
|
|
4852
|
+
if (!parsedBody.success) {
|
|
4853
|
+
const err = validationError("Invalid schedule payload", {
|
|
4854
|
+
issues: parsedBody.error.issues.map((issue) => ({
|
|
4855
|
+
path: issue.path.join("."),
|
|
4856
|
+
message: issue.message
|
|
4857
|
+
}))
|
|
4723
4858
|
});
|
|
4859
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4860
|
+
}
|
|
4861
|
+
const { preset, cron: cron2, timezone, providers, enabled } = parsedBody.data;
|
|
4862
|
+
const validNames = opts.validProviderNames ?? [];
|
|
4863
|
+
if (validNames.length && providers?.length) {
|
|
4864
|
+
const invalid = providers.filter((p) => !validNames.includes(p));
|
|
4865
|
+
if (invalid.length) {
|
|
4866
|
+
const err = validationError(`Invalid provider(s): ${invalid.join(", ")}. Must be one of: ${validNames.join(", ")}`, {
|
|
4867
|
+
invalidProviders: invalid,
|
|
4868
|
+
validProviders: validNames
|
|
4869
|
+
});
|
|
4870
|
+
return reply.status(err.statusCode).send(err.toJSON());
|
|
4871
|
+
}
|
|
4724
4872
|
}
|
|
4725
4873
|
if (!isValidTimezone(timezone)) {
|
|
4726
4874
|
return reply.status(400).send({
|
|
@@ -4991,7 +5139,7 @@ function resolveProjectSafe7(app, name, reply) {
|
|
|
4991
5139
|
|
|
4992
5140
|
// ../api-routes/src/google.ts
|
|
4993
5141
|
import crypto13 from "crypto";
|
|
4994
|
-
import { eq as eq13, and as and3, desc as
|
|
5142
|
+
import { eq as eq13, and as and3, desc as desc4, sql as sql2 } from "drizzle-orm";
|
|
4995
5143
|
|
|
4996
5144
|
// ../integration-google/src/constants.ts
|
|
4997
5145
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
@@ -5432,7 +5580,7 @@ async function googleRoutes(app, opts) {
|
|
|
5432
5580
|
if (endDate) conditions.push(sql2`${gscSearchData.date} <= ${endDate}`);
|
|
5433
5581
|
if (query) conditions.push(sql2`${gscSearchData.query} LIKE ${"%" + query + "%"}`);
|
|
5434
5582
|
if (page) conditions.push(sql2`${gscSearchData.page} LIKE ${"%" + page + "%"}`);
|
|
5435
|
-
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(
|
|
5583
|
+
const rows = app.db.select().from(gscSearchData).where(and3(...conditions)).orderBy(desc4(gscSearchData.date)).limit(parseInt(limit ?? "500", 10)).all();
|
|
5436
5584
|
return rows.map((r) => ({
|
|
5437
5585
|
date: r.date,
|
|
5438
5586
|
query: r.query,
|
|
@@ -5510,7 +5658,7 @@ async function googleRoutes(app, opts) {
|
|
|
5510
5658
|
const { url, limit } = request.query;
|
|
5511
5659
|
const conditions = [eq13(gscUrlInspections.projectId, project.id)];
|
|
5512
5660
|
if (url) conditions.push(eq13(gscUrlInspections.url, url));
|
|
5513
|
-
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(
|
|
5661
|
+
const rows = app.db.select().from(gscUrlInspections).where(and3(...conditions)).orderBy(desc4(gscUrlInspections.inspectedAt)).limit(parseInt(limit ?? "100", 10)).all();
|
|
5514
5662
|
return rows.map((r) => ({
|
|
5515
5663
|
id: r.id,
|
|
5516
5664
|
url: r.url,
|
|
@@ -5529,7 +5677,7 @@ async function googleRoutes(app, opts) {
|
|
|
5529
5677
|
});
|
|
5530
5678
|
app.get("/projects/:name/google/gsc/deindexed", async (request) => {
|
|
5531
5679
|
const project = resolveProject(app.db, request.params.name);
|
|
5532
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5680
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
5533
5681
|
const byUrl = /* @__PURE__ */ new Map();
|
|
5534
5682
|
for (const row of allInspections) {
|
|
5535
5683
|
const existing = byUrl.get(row.url);
|
|
@@ -5557,7 +5705,7 @@ async function googleRoutes(app, opts) {
|
|
|
5557
5705
|
});
|
|
5558
5706
|
app.get("/projects/:name/google/gsc/coverage", async (request) => {
|
|
5559
5707
|
const project = resolveProject(app.db, request.params.name);
|
|
5560
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5708
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
5561
5709
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
5562
5710
|
const historyByUrl = /* @__PURE__ */ new Map();
|
|
5563
5711
|
for (const row of allInspections) {
|
|
@@ -5649,7 +5797,7 @@ async function googleRoutes(app, opts) {
|
|
|
5649
5797
|
const project = resolveProject(app.db, request.params.name);
|
|
5650
5798
|
const parsed = parseInt(request.query.limit ?? "90", 10);
|
|
5651
5799
|
const limit = Number.isNaN(parsed) || parsed <= 0 ? 90 : parsed;
|
|
5652
|
-
const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(
|
|
5800
|
+
const rows = app.db.select().from(gscCoverageSnapshots).where(eq13(gscCoverageSnapshots.projectId, project.id)).orderBy(desc4(gscCoverageSnapshots.date)).limit(limit).all();
|
|
5653
5801
|
return rows.map((r) => ({
|
|
5654
5802
|
date: r.date,
|
|
5655
5803
|
indexed: r.indexed,
|
|
@@ -5802,7 +5950,7 @@ async function googleRoutes(app, opts) {
|
|
|
5802
5950
|
const { accessToken } = await getValidToken(store, project.canonicalDomain, "gsc", googleClientId, googleClientSecret);
|
|
5803
5951
|
let urlsToNotify = request.body?.urls ?? [];
|
|
5804
5952
|
if (request.body?.allUnindexed) {
|
|
5805
|
-
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(
|
|
5953
|
+
const allInspections = app.db.select().from(gscUrlInspections).where(eq13(gscUrlInspections.projectId, project.id)).orderBy(desc4(gscUrlInspections.inspectedAt)).all();
|
|
5806
5954
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
5807
5955
|
for (const row of allInspections) {
|
|
5808
5956
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -5877,7 +6025,7 @@ async function googleRoutes(app, opts) {
|
|
|
5877
6025
|
|
|
5878
6026
|
// ../api-routes/src/bing.ts
|
|
5879
6027
|
import crypto14 from "crypto";
|
|
5880
|
-
import { eq as eq14, and as and4, desc as
|
|
6028
|
+
import { eq as eq14, and as and4, desc as desc5 } from "drizzle-orm";
|
|
5881
6029
|
|
|
5882
6030
|
// ../integration-bing/src/constants.ts
|
|
5883
6031
|
var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
|
|
@@ -6081,7 +6229,7 @@ async function bingRoutes(app, opts) {
|
|
|
6081
6229
|
const project = resolveProject(app.db, request.params.name);
|
|
6082
6230
|
const conn = requireConnection(store, project.canonicalDomain, reply);
|
|
6083
6231
|
if (!conn) return;
|
|
6084
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(
|
|
6232
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
|
|
6085
6233
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6086
6234
|
for (const row of allInspections) {
|
|
6087
6235
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -6131,7 +6279,7 @@ async function bingRoutes(app, opts) {
|
|
|
6131
6279
|
const project = resolveProject(app.db, request.params.name);
|
|
6132
6280
|
const { url, limit } = request.query;
|
|
6133
6281
|
const whereClause = url ? and4(eq14(bingUrlInspections.projectId, project.id), eq14(bingUrlInspections.url, url)) : eq14(bingUrlInspections.projectId, project.id);
|
|
6134
|
-
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(
|
|
6282
|
+
const filtered = app.db.select().from(bingUrlInspections).where(whereClause).orderBy(desc5(bingUrlInspections.inspectedAt)).limit(Math.max(1, Math.min(parseInt(limit ?? "100", 10) || 100, 1e3))).all();
|
|
6135
6283
|
return filtered.map((r) => ({
|
|
6136
6284
|
id: r.id,
|
|
6137
6285
|
url: r.url,
|
|
@@ -6193,7 +6341,7 @@ async function bingRoutes(app, opts) {
|
|
|
6193
6341
|
}
|
|
6194
6342
|
let urlsToSubmit = request.body?.urls ?? [];
|
|
6195
6343
|
if (request.body?.allUnindexed) {
|
|
6196
|
-
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(
|
|
6344
|
+
const allInspections = app.db.select().from(bingUrlInspections).where(eq14(bingUrlInspections.projectId, project.id)).orderBy(desc5(bingUrlInspections.inspectedAt)).all();
|
|
6197
6345
|
const latestByUrl = /* @__PURE__ */ new Map();
|
|
6198
6346
|
for (const row of allInspections) {
|
|
6199
6347
|
if (!latestByUrl.has(row.url)) {
|
|
@@ -6289,51 +6437,61 @@ async function cdpRoutes(app, opts) {
|
|
|
6289
6437
|
const { snapshotId } = request.params;
|
|
6290
6438
|
const snapshot = app.db.select({ screenshotPath: querySnapshots.screenshotPath }).from(querySnapshots).where(eq15(querySnapshots.id, snapshotId)).get();
|
|
6291
6439
|
if (!snapshot?.screenshotPath) {
|
|
6292
|
-
|
|
6440
|
+
const err = notFound("Screenshot", snapshotId);
|
|
6441
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6293
6442
|
}
|
|
6294
6443
|
const base = path2.resolve(getScreenshotDir());
|
|
6295
6444
|
const fullPath = path2.resolve(path2.join(base, snapshot.screenshotPath));
|
|
6296
6445
|
if (!fullPath.startsWith(base + path2.sep) && fullPath !== base) {
|
|
6297
|
-
|
|
6446
|
+
const err = notFound("Screenshot", snapshotId);
|
|
6447
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6298
6448
|
}
|
|
6299
6449
|
if (!fs2.existsSync(fullPath)) {
|
|
6300
|
-
|
|
6450
|
+
const err = notFound("Screenshot file", snapshotId);
|
|
6451
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6301
6452
|
}
|
|
6302
6453
|
const stream = fs2.createReadStream(fullPath);
|
|
6303
6454
|
return reply.type("image/png").send(stream);
|
|
6304
6455
|
});
|
|
6305
6456
|
app.put("/settings/cdp", async (request, reply) => {
|
|
6306
6457
|
if (!opts.onCdpConfigure) {
|
|
6307
|
-
|
|
6458
|
+
const err = notImplemented("CDP configuration not supported in this deployment");
|
|
6459
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6308
6460
|
}
|
|
6309
6461
|
const { host, port = 9222 } = request.body;
|
|
6310
6462
|
if (!host || typeof host !== "string") {
|
|
6311
|
-
|
|
6463
|
+
const err = validationError("host is required");
|
|
6464
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6312
6465
|
}
|
|
6313
6466
|
const ALLOWED_HOSTS = ["localhost", "127.0.0.1", "::1"];
|
|
6314
6467
|
if (!ALLOWED_HOSTS.includes(host)) {
|
|
6315
|
-
|
|
6468
|
+
const err = validationError("host must be localhost, 127.0.0.1, or ::1");
|
|
6469
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6316
6470
|
}
|
|
6317
6471
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
6318
|
-
|
|
6472
|
+
const err = validationError("port must be an integer between 1 and 65535");
|
|
6473
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6319
6474
|
}
|
|
6320
6475
|
await opts.onCdpConfigure(host, port);
|
|
6321
6476
|
return reply.code(200).send({ endpoint: `ws://${host}:${port}` });
|
|
6322
6477
|
});
|
|
6323
6478
|
app.get("/cdp/status", async (_request, reply) => {
|
|
6324
6479
|
if (!opts.getCdpStatus) {
|
|
6325
|
-
|
|
6480
|
+
const err = notImplemented("CDP not configured");
|
|
6481
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6326
6482
|
}
|
|
6327
6483
|
const status = await opts.getCdpStatus();
|
|
6328
6484
|
return reply.send(status);
|
|
6329
6485
|
});
|
|
6330
6486
|
app.post("/cdp/screenshot", async (request, reply) => {
|
|
6331
6487
|
if (!opts.onCdpScreenshot) {
|
|
6332
|
-
|
|
6488
|
+
const err = notImplemented("CDP not configured");
|
|
6489
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6333
6490
|
}
|
|
6334
6491
|
const { query, targets } = request.body;
|
|
6335
6492
|
if (!query || typeof query !== "string") {
|
|
6336
|
-
|
|
6493
|
+
const err = validationError("query is required");
|
|
6494
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6337
6495
|
}
|
|
6338
6496
|
const results = await opts.onCdpScreenshot(query, targets);
|
|
6339
6497
|
return reply.code(200).send({ results });
|
|
@@ -6344,7 +6502,10 @@ async function cdpRoutes(app, opts) {
|
|
|
6344
6502
|
const project = resolveProject(app.db, request.params.name);
|
|
6345
6503
|
const { runId } = request.params;
|
|
6346
6504
|
const run = app.db.select().from(runs).where(and5(eq15(runs.id, runId), eq15(runs.projectId, project.id))).get();
|
|
6347
|
-
if (!run)
|
|
6505
|
+
if (!run) {
|
|
6506
|
+
const err = notFound("Run", runId);
|
|
6507
|
+
return reply.code(err.statusCode).send(err.toJSON());
|
|
6508
|
+
}
|
|
6348
6509
|
const snapshots = app.db.select({
|
|
6349
6510
|
id: querySnapshots.id,
|
|
6350
6511
|
keywordId: querySnapshots.keywordId,
|
|
@@ -6465,15 +6626,21 @@ async function apiRoutes(app, opts) {
|
|
|
6465
6626
|
await app.register(async (api) => {
|
|
6466
6627
|
await api.register(openApiRoutes, opts.openApiInfo ?? {});
|
|
6467
6628
|
await api.register(projectRoutes, {
|
|
6468
|
-
onProjectDeleted: opts.onProjectDeleted
|
|
6629
|
+
onProjectDeleted: opts.onProjectDeleted,
|
|
6630
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
6469
6631
|
});
|
|
6470
6632
|
await api.register(keywordRoutes, {
|
|
6471
|
-
onGenerateKeywords: opts.onGenerateKeywords
|
|
6633
|
+
onGenerateKeywords: opts.onGenerateKeywords,
|
|
6634
|
+
validProviderNames: opts.providerAdapters?.filter((a) => a.mode === "api").map((a) => a.name)
|
|
6472
6635
|
});
|
|
6473
6636
|
await api.register(competitorRoutes);
|
|
6474
|
-
await api.register(runRoutes, {
|
|
6637
|
+
await api.register(runRoutes, {
|
|
6638
|
+
onRunCreated: opts.onRunCreated,
|
|
6639
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
6640
|
+
});
|
|
6475
6641
|
await api.register(applyRoutes, {
|
|
6476
6642
|
onScheduleUpdated: opts.onScheduleUpdated,
|
|
6643
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name),
|
|
6477
6644
|
onGoogleConnectionPropertyUpdated: (domain, connectionType, propertyId) => {
|
|
6478
6645
|
opts.googleConnectionStore?.updateConnection(domain, connectionType, {
|
|
6479
6646
|
propertyId,
|
|
@@ -6485,6 +6652,7 @@ async function apiRoutes(app, opts) {
|
|
|
6485
6652
|
await api.register(analyticsRoutes);
|
|
6486
6653
|
await api.register(settingsRoutes, {
|
|
6487
6654
|
providerSummary: opts.providerSummary,
|
|
6655
|
+
providerAdapters: opts.providerAdapters,
|
|
6488
6656
|
onProviderUpdate: opts.onProviderUpdate,
|
|
6489
6657
|
google: opts.googleSettingsSummary,
|
|
6490
6658
|
onGoogleUpdate: opts.onGoogleSettingsUpdate,
|
|
@@ -6492,7 +6660,8 @@ async function apiRoutes(app, opts) {
|
|
|
6492
6660
|
onBingUpdate: opts.onBingSettingsUpdate
|
|
6493
6661
|
});
|
|
6494
6662
|
await api.register(scheduleRoutes, {
|
|
6495
|
-
onScheduleUpdated: opts.onScheduleUpdated
|
|
6663
|
+
onScheduleUpdated: opts.onScheduleUpdated,
|
|
6664
|
+
validProviderNames: opts.providerAdapters?.map((a) => a.name)
|
|
6496
6665
|
});
|
|
6497
6666
|
await api.register(notificationRoutes);
|
|
6498
6667
|
await api.register(telemetryRoutes, {
|
|
@@ -6520,11 +6689,12 @@ async function apiRoutes(app, opts) {
|
|
|
6520
6689
|
|
|
6521
6690
|
// ../provider-gemini/src/normalize.ts
|
|
6522
6691
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
|
6523
|
-
var DEFAULT_MODEL =
|
|
6692
|
+
var DEFAULT_MODEL = "gemini-3-flash";
|
|
6693
|
+
var VALIDATION_PATTERN = /^gemini-/;
|
|
6524
6694
|
function resolveModel(config) {
|
|
6525
6695
|
const m = config.model;
|
|
6526
6696
|
if (!m) return DEFAULT_MODEL;
|
|
6527
|
-
if (
|
|
6697
|
+
if (VALIDATION_PATTERN.test(m)) return m;
|
|
6528
6698
|
console.warn(
|
|
6529
6699
|
`[provider-gemini] Invalid model name "${m}" \u2014 this provider uses the Gemini AI Studio API (generativelanguage.googleapis.com) which only accepts "gemini-*" model names. Falling back to ${DEFAULT_MODEL}.`
|
|
6530
6700
|
);
|
|
@@ -6535,7 +6705,7 @@ function validateConfig(config) {
|
|
|
6535
6705
|
return { ok: false, provider: "gemini", message: "missing api key" };
|
|
6536
6706
|
}
|
|
6537
6707
|
const model = resolveModel(config);
|
|
6538
|
-
const warning = config.model && !
|
|
6708
|
+
const warning = config.model && !VALIDATION_PATTERN.test(config.model) ? ` (invalid model "${config.model}" replaced with default)` : "";
|
|
6539
6709
|
return {
|
|
6540
6710
|
ok: true,
|
|
6541
6711
|
provider: "gemini",
|
|
@@ -6719,15 +6889,29 @@ function toGeminiConfig(config) {
|
|
|
6719
6889
|
}
|
|
6720
6890
|
var geminiAdapter = {
|
|
6721
6891
|
name: "gemini",
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6892
|
+
displayName: "Gemini",
|
|
6893
|
+
mode: "api",
|
|
6894
|
+
keyUrl: "https://aistudio.google.com/apikey",
|
|
6895
|
+
modelRegistry: {
|
|
6896
|
+
defaultModel: "gemini-3-flash",
|
|
6897
|
+
validationPattern: /^gemini-/,
|
|
6898
|
+
validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
|
|
6899
|
+
knownModels: [
|
|
6900
|
+
{ id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
|
|
6901
|
+
{ id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
|
|
6902
|
+
{ id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
|
|
6903
|
+
{ id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
|
|
6904
|
+
]
|
|
6905
|
+
},
|
|
6906
|
+
validateConfig(config) {
|
|
6907
|
+
const result = validateConfig(toGeminiConfig(config));
|
|
6908
|
+
return {
|
|
6909
|
+
ok: result.ok,
|
|
6910
|
+
provider: "gemini",
|
|
6911
|
+
message: result.message,
|
|
6912
|
+
model: result.model
|
|
6913
|
+
};
|
|
6914
|
+
},
|
|
6731
6915
|
async healthcheck(config) {
|
|
6732
6916
|
const result = await healthcheck(toGeminiConfig(config));
|
|
6733
6917
|
return {
|
|
@@ -6777,7 +6961,7 @@ var geminiAdapter = {
|
|
|
6777
6961
|
|
|
6778
6962
|
// ../provider-openai/src/normalize.ts
|
|
6779
6963
|
import OpenAI from "openai";
|
|
6780
|
-
var DEFAULT_MODEL2 =
|
|
6964
|
+
var DEFAULT_MODEL2 = "gpt-5.4";
|
|
6781
6965
|
function validateConfig2(config) {
|
|
6782
6966
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
6783
6967
|
return { ok: false, provider: "openai", message: "missing api key" };
|
|
@@ -6971,6 +7155,22 @@ function toOpenAIConfig(config) {
|
|
|
6971
7155
|
}
|
|
6972
7156
|
var openaiAdapter = {
|
|
6973
7157
|
name: "openai",
|
|
7158
|
+
displayName: "OpenAI",
|
|
7159
|
+
mode: "api",
|
|
7160
|
+
keyUrl: "https://platform.openai.com/api-keys",
|
|
7161
|
+
modelRegistry: {
|
|
7162
|
+
defaultModel: "gpt-5.4",
|
|
7163
|
+
validationPattern: /^(gpt-|o\d)/,
|
|
7164
|
+
validationHint: "expected a GPT or o-series model name (e.g. gpt-5.4, o3)",
|
|
7165
|
+
knownModels: [
|
|
7166
|
+
{ id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
|
|
7167
|
+
{ id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
|
|
7168
|
+
{ id: "gpt-5-mini", displayName: "GPT-5 Mini", tier: "fast" },
|
|
7169
|
+
{ id: "gpt-5-nano", displayName: "GPT-5 Nano", tier: "economy" },
|
|
7170
|
+
{ id: "gpt-5", displayName: "GPT-5", tier: "standard" },
|
|
7171
|
+
{ id: "gpt-4.1", displayName: "GPT-4.1", tier: "standard" }
|
|
7172
|
+
]
|
|
7173
|
+
},
|
|
6974
7174
|
validateConfig(config) {
|
|
6975
7175
|
const result = validateConfig2(toOpenAIConfig(config));
|
|
6976
7176
|
return {
|
|
@@ -7029,7 +7229,7 @@ var openaiAdapter = {
|
|
|
7029
7229
|
|
|
7030
7230
|
// ../provider-claude/src/normalize.ts
|
|
7031
7231
|
import Anthropic from "@anthropic-ai/sdk";
|
|
7032
|
-
var DEFAULT_MODEL3 =
|
|
7232
|
+
var DEFAULT_MODEL3 = "claude-sonnet-4-6";
|
|
7033
7233
|
function validateConfig3(config) {
|
|
7034
7234
|
if (!config.apiKey || config.apiKey.length === 0) {
|
|
7035
7235
|
return { ok: false, provider: "claude", message: "missing api key" };
|
|
@@ -7217,6 +7417,19 @@ function toClaudeConfig(config) {
|
|
|
7217
7417
|
}
|
|
7218
7418
|
var claudeAdapter = {
|
|
7219
7419
|
name: "claude",
|
|
7420
|
+
displayName: "Claude",
|
|
7421
|
+
mode: "api",
|
|
7422
|
+
keyUrl: "https://platform.claude.com/settings/keys",
|
|
7423
|
+
modelRegistry: {
|
|
7424
|
+
defaultModel: "claude-sonnet-4-6",
|
|
7425
|
+
validationPattern: /^claude-/,
|
|
7426
|
+
validationHint: 'model name must start with "claude-" (e.g. claude-sonnet-4-6)',
|
|
7427
|
+
knownModels: [
|
|
7428
|
+
{ id: "claude-opus-4-6", displayName: "Claude Opus 4.6", tier: "flagship" },
|
|
7429
|
+
{ id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tier: "standard" },
|
|
7430
|
+
{ id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5", tier: "fast" }
|
|
7431
|
+
]
|
|
7432
|
+
},
|
|
7220
7433
|
validateConfig(config) {
|
|
7221
7434
|
const result = validateConfig3(toClaudeConfig(config));
|
|
7222
7435
|
return {
|
|
@@ -7275,7 +7488,7 @@ var claudeAdapter = {
|
|
|
7275
7488
|
|
|
7276
7489
|
// ../provider-local/src/normalize.ts
|
|
7277
7490
|
import OpenAI2 from "openai";
|
|
7278
|
-
var DEFAULT_MODEL4 =
|
|
7491
|
+
var DEFAULT_MODEL4 = "llama3";
|
|
7279
7492
|
function validateConfig4(config) {
|
|
7280
7493
|
if (!config.baseUrl || config.baseUrl.length === 0) {
|
|
7281
7494
|
return { ok: false, provider: "local", message: "missing base URL" };
|
|
@@ -7404,6 +7617,16 @@ function toLocalConfig(config) {
|
|
|
7404
7617
|
}
|
|
7405
7618
|
var localAdapter = {
|
|
7406
7619
|
name: "local",
|
|
7620
|
+
displayName: "Local",
|
|
7621
|
+
mode: "api",
|
|
7622
|
+
modelRegistry: {
|
|
7623
|
+
defaultModel: "llama3",
|
|
7624
|
+
validationPattern: /./,
|
|
7625
|
+
validationHint: "any model name accepted",
|
|
7626
|
+
knownModels: [
|
|
7627
|
+
{ id: "llama3", displayName: "Llama 3", tier: "standard" }
|
|
7628
|
+
]
|
|
7629
|
+
},
|
|
7407
7630
|
validateConfig(config) {
|
|
7408
7631
|
const result = validateConfig4(toLocalConfig(config));
|
|
7409
7632
|
return {
|
|
@@ -7920,6 +8143,16 @@ function getScreenshotDir2() {
|
|
|
7920
8143
|
}
|
|
7921
8144
|
var cdpChatgptAdapter = {
|
|
7922
8145
|
name: "cdp:chatgpt",
|
|
8146
|
+
displayName: "ChatGPT (Browser)",
|
|
8147
|
+
mode: "browser",
|
|
8148
|
+
modelRegistry: {
|
|
8149
|
+
defaultModel: "chatgpt-web",
|
|
8150
|
+
validationPattern: /./,
|
|
8151
|
+
validationHint: "model is detected from the ChatGPT web UI",
|
|
8152
|
+
knownModels: [
|
|
8153
|
+
{ id: "chatgpt-web", displayName: "ChatGPT (Web UI)", tier: "standard" }
|
|
8154
|
+
]
|
|
8155
|
+
},
|
|
7923
8156
|
validateConfig(config) {
|
|
7924
8157
|
if (!config.cdpEndpoint) {
|
|
7925
8158
|
return {
|
|
@@ -8000,6 +8233,207 @@ var cdpChatgptAdapter = {
|
|
|
8000
8233
|
}
|
|
8001
8234
|
};
|
|
8002
8235
|
|
|
8236
|
+
// ../provider-perplexity/src/normalize.ts
|
|
8237
|
+
import OpenAI3 from "openai";
|
|
8238
|
+
var DEFAULT_MODEL5 = "sonar";
|
|
8239
|
+
var BASE_URL = "https://api.perplexity.ai";
|
|
8240
|
+
function validateConfig5(config) {
|
|
8241
|
+
if (!config.apiKey || config.apiKey.length === 0) {
|
|
8242
|
+
return { ok: false, provider: "perplexity", message: "missing api key" };
|
|
8243
|
+
}
|
|
8244
|
+
return {
|
|
8245
|
+
ok: true,
|
|
8246
|
+
provider: "perplexity",
|
|
8247
|
+
message: "config valid",
|
|
8248
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8249
|
+
};
|
|
8250
|
+
}
|
|
8251
|
+
async function healthcheck5(config) {
|
|
8252
|
+
const validation = validateConfig5(config);
|
|
8253
|
+
if (!validation.ok) return validation;
|
|
8254
|
+
try {
|
|
8255
|
+
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
8256
|
+
const response = await client.chat.completions.create({
|
|
8257
|
+
model: config.model ?? DEFAULT_MODEL5,
|
|
8258
|
+
messages: [{ role: "user", content: 'Say "ok"' }]
|
|
8259
|
+
});
|
|
8260
|
+
const text2 = response.choices[0]?.message?.content ?? "";
|
|
8261
|
+
return {
|
|
8262
|
+
ok: text2.length > 0,
|
|
8263
|
+
provider: "perplexity",
|
|
8264
|
+
message: text2.length > 0 ? "perplexity api key verified" : "empty response from perplexity",
|
|
8265
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8266
|
+
};
|
|
8267
|
+
} catch (err) {
|
|
8268
|
+
return {
|
|
8269
|
+
ok: false,
|
|
8270
|
+
provider: "perplexity",
|
|
8271
|
+
message: err instanceof Error ? err.message : String(err),
|
|
8272
|
+
model: config.model ?? DEFAULT_MODEL5
|
|
8273
|
+
};
|
|
8274
|
+
}
|
|
8275
|
+
}
|
|
8276
|
+
async function executeTrackedQuery5(input) {
|
|
8277
|
+
const model = input.config.model ?? DEFAULT_MODEL5;
|
|
8278
|
+
const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
|
|
8279
|
+
const response = await client.chat.completions.create({
|
|
8280
|
+
model,
|
|
8281
|
+
messages: [
|
|
8282
|
+
{ role: "user", content: input.keyword }
|
|
8283
|
+
]
|
|
8284
|
+
});
|
|
8285
|
+
const rawResponse = responseToRecord4(response);
|
|
8286
|
+
const citations = extractCitations(rawResponse);
|
|
8287
|
+
const groundingSources = citations.map((url) => ({
|
|
8288
|
+
uri: url,
|
|
8289
|
+
title: ""
|
|
8290
|
+
}));
|
|
8291
|
+
return {
|
|
8292
|
+
provider: "perplexity",
|
|
8293
|
+
rawResponse,
|
|
8294
|
+
model,
|
|
8295
|
+
groundingSources,
|
|
8296
|
+
searchQueries: [input.keyword]
|
|
8297
|
+
};
|
|
8298
|
+
}
|
|
8299
|
+
function normalizeResult6(raw) {
|
|
8300
|
+
const answerText = extractAnswerText3(raw.rawResponse);
|
|
8301
|
+
const citedDomains = extractCitedDomains5(raw.groundingSources);
|
|
8302
|
+
return {
|
|
8303
|
+
provider: "perplexity",
|
|
8304
|
+
answerText,
|
|
8305
|
+
citedDomains,
|
|
8306
|
+
groundingSources: raw.groundingSources,
|
|
8307
|
+
searchQueries: raw.searchQueries
|
|
8308
|
+
};
|
|
8309
|
+
}
|
|
8310
|
+
function extractCitations(rawResponse) {
|
|
8311
|
+
const citations = rawResponse.citations;
|
|
8312
|
+
if (!Array.isArray(citations)) return [];
|
|
8313
|
+
return citations.filter((c) => typeof c === "string");
|
|
8314
|
+
}
|
|
8315
|
+
function extractAnswerText3(rawResponse) {
|
|
8316
|
+
try {
|
|
8317
|
+
const choices = rawResponse.choices;
|
|
8318
|
+
if (!choices?.length) return "";
|
|
8319
|
+
return choices[0].message?.content ?? "";
|
|
8320
|
+
} catch {
|
|
8321
|
+
return "";
|
|
8322
|
+
}
|
|
8323
|
+
}
|
|
8324
|
+
function extractCitedDomains5(groundingSources) {
|
|
8325
|
+
const domains = /* @__PURE__ */ new Set();
|
|
8326
|
+
for (const source of groundingSources) {
|
|
8327
|
+
const domain = extractDomainFromUri4(source.uri);
|
|
8328
|
+
if (domain) domains.add(domain);
|
|
8329
|
+
}
|
|
8330
|
+
return [...domains];
|
|
8331
|
+
}
|
|
8332
|
+
function extractDomainFromUri4(uri) {
|
|
8333
|
+
try {
|
|
8334
|
+
const url = new URL(uri);
|
|
8335
|
+
return url.hostname.replace(/^www\./, "");
|
|
8336
|
+
} catch {
|
|
8337
|
+
return null;
|
|
8338
|
+
}
|
|
8339
|
+
}
|
|
8340
|
+
async function generateText5(prompt, config) {
|
|
8341
|
+
const model = config.model ?? DEFAULT_MODEL5;
|
|
8342
|
+
const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
|
|
8343
|
+
const response = await client.chat.completions.create({
|
|
8344
|
+
model,
|
|
8345
|
+
messages: [{ role: "user", content: prompt }]
|
|
8346
|
+
});
|
|
8347
|
+
return response.choices[0]?.message?.content ?? "";
|
|
8348
|
+
}
|
|
8349
|
+
function responseToRecord4(response) {
|
|
8350
|
+
try {
|
|
8351
|
+
return JSON.parse(JSON.stringify(response));
|
|
8352
|
+
} catch {
|
|
8353
|
+
return { error: "failed to serialize response" };
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8357
|
+
// ../provider-perplexity/src/adapter.ts
|
|
8358
|
+
function toPerplexityConfig(config) {
|
|
8359
|
+
return {
|
|
8360
|
+
apiKey: config.apiKey ?? "",
|
|
8361
|
+
model: config.model,
|
|
8362
|
+
quotaPolicy: config.quotaPolicy
|
|
8363
|
+
};
|
|
8364
|
+
}
|
|
8365
|
+
var perplexityAdapter = {
|
|
8366
|
+
name: "perplexity",
|
|
8367
|
+
displayName: "Perplexity",
|
|
8368
|
+
mode: "api",
|
|
8369
|
+
keyUrl: "https://www.perplexity.ai/settings/api",
|
|
8370
|
+
modelRegistry: {
|
|
8371
|
+
defaultModel: "sonar",
|
|
8372
|
+
validationPattern: /^sonar/,
|
|
8373
|
+
validationHint: "expected a sonar model (e.g. sonar, sonar-pro, sonar-reasoning)",
|
|
8374
|
+
knownModels: [
|
|
8375
|
+
{ id: "sonar", displayName: "Sonar", tier: "standard" },
|
|
8376
|
+
{ id: "sonar-pro", displayName: "Sonar Pro", tier: "flagship" },
|
|
8377
|
+
{ id: "sonar-reasoning", displayName: "Sonar Reasoning", tier: "flagship" },
|
|
8378
|
+
{ id: "sonar-reasoning-pro", displayName: "Sonar Reasoning Pro", tier: "flagship" }
|
|
8379
|
+
]
|
|
8380
|
+
},
|
|
8381
|
+
validateConfig(config) {
|
|
8382
|
+
const result = validateConfig5(toPerplexityConfig(config));
|
|
8383
|
+
return {
|
|
8384
|
+
ok: result.ok,
|
|
8385
|
+
provider: "perplexity",
|
|
8386
|
+
message: result.message,
|
|
8387
|
+
model: result.model
|
|
8388
|
+
};
|
|
8389
|
+
},
|
|
8390
|
+
async healthcheck(config) {
|
|
8391
|
+
const result = await healthcheck5(toPerplexityConfig(config));
|
|
8392
|
+
return {
|
|
8393
|
+
ok: result.ok,
|
|
8394
|
+
provider: "perplexity",
|
|
8395
|
+
message: result.message,
|
|
8396
|
+
model: result.model
|
|
8397
|
+
};
|
|
8398
|
+
},
|
|
8399
|
+
async executeTrackedQuery(input, config) {
|
|
8400
|
+
const raw = await executeTrackedQuery5({
|
|
8401
|
+
keyword: input.keyword,
|
|
8402
|
+
canonicalDomains: input.canonicalDomains,
|
|
8403
|
+
competitorDomains: input.competitorDomains,
|
|
8404
|
+
config: toPerplexityConfig(config),
|
|
8405
|
+
location: input.location
|
|
8406
|
+
});
|
|
8407
|
+
return {
|
|
8408
|
+
provider: "perplexity",
|
|
8409
|
+
rawResponse: raw.rawResponse,
|
|
8410
|
+
model: raw.model,
|
|
8411
|
+
groundingSources: raw.groundingSources,
|
|
8412
|
+
searchQueries: raw.searchQueries
|
|
8413
|
+
};
|
|
8414
|
+
},
|
|
8415
|
+
normalizeResult(raw) {
|
|
8416
|
+
const perplexityRaw = {
|
|
8417
|
+
provider: "perplexity",
|
|
8418
|
+
rawResponse: raw.rawResponse,
|
|
8419
|
+
model: raw.model,
|
|
8420
|
+
groundingSources: raw.groundingSources,
|
|
8421
|
+
searchQueries: raw.searchQueries
|
|
8422
|
+
};
|
|
8423
|
+
const normalized = normalizeResult6(perplexityRaw);
|
|
8424
|
+
return {
|
|
8425
|
+
provider: "perplexity",
|
|
8426
|
+
answerText: normalized.answerText,
|
|
8427
|
+
citedDomains: normalized.citedDomains,
|
|
8428
|
+
groundingSources: normalized.groundingSources,
|
|
8429
|
+
searchQueries: normalized.searchQueries
|
|
8430
|
+
};
|
|
8431
|
+
},
|
|
8432
|
+
async generateText(prompt, config) {
|
|
8433
|
+
return generateText5(prompt, toPerplexityConfig(config));
|
|
8434
|
+
}
|
|
8435
|
+
};
|
|
8436
|
+
|
|
8003
8437
|
// src/google-config.ts
|
|
8004
8438
|
function ensureConnections(config) {
|
|
8005
8439
|
if (!config.google) config.google = {};
|
|
@@ -8078,7 +8512,75 @@ import crypto15 from "crypto";
|
|
|
8078
8512
|
import fs4 from "fs";
|
|
8079
8513
|
import path5 from "path";
|
|
8080
8514
|
import os4 from "os";
|
|
8081
|
-
import { eq as eq16, inArray as inArray3 } from "drizzle-orm";
|
|
8515
|
+
import { and as and6, eq as eq16, inArray as inArray3 } from "drizzle-orm";
|
|
8516
|
+
var RunCancelledError = class extends Error {
|
|
8517
|
+
constructor(runId) {
|
|
8518
|
+
super(`Run ${runId} was cancelled`);
|
|
8519
|
+
this.name = "RunCancelledError";
|
|
8520
|
+
}
|
|
8521
|
+
};
|
|
8522
|
+
var ProviderExecutionGate = class {
|
|
8523
|
+
constructor(maxConcurrency, maxPerMinute) {
|
|
8524
|
+
this.maxConcurrency = maxConcurrency;
|
|
8525
|
+
this.maxPerMinute = maxPerMinute;
|
|
8526
|
+
}
|
|
8527
|
+
window = [];
|
|
8528
|
+
waiters = [];
|
|
8529
|
+
rateLimitChain = Promise.resolve();
|
|
8530
|
+
inFlight = 0;
|
|
8531
|
+
async run(task) {
|
|
8532
|
+
await this.acquire();
|
|
8533
|
+
try {
|
|
8534
|
+
await this.waitForRateLimit();
|
|
8535
|
+
return await task();
|
|
8536
|
+
} finally {
|
|
8537
|
+
this.release();
|
|
8538
|
+
}
|
|
8539
|
+
}
|
|
8540
|
+
async acquire() {
|
|
8541
|
+
if (this.inFlight < Math.max(1, this.maxConcurrency)) {
|
|
8542
|
+
this.inFlight++;
|
|
8543
|
+
return;
|
|
8544
|
+
}
|
|
8545
|
+
await new Promise((resolve) => {
|
|
8546
|
+
this.waiters.push(resolve);
|
|
8547
|
+
});
|
|
8548
|
+
this.inFlight++;
|
|
8549
|
+
}
|
|
8550
|
+
release() {
|
|
8551
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
8552
|
+
const next = this.waiters.shift();
|
|
8553
|
+
next?.();
|
|
8554
|
+
}
|
|
8555
|
+
async waitForRateLimit() {
|
|
8556
|
+
let releaseChain;
|
|
8557
|
+
const previousChain = this.rateLimitChain;
|
|
8558
|
+
this.rateLimitChain = new Promise((resolve) => {
|
|
8559
|
+
releaseChain = resolve;
|
|
8560
|
+
});
|
|
8561
|
+
await previousChain;
|
|
8562
|
+
try {
|
|
8563
|
+
const now = Date.now();
|
|
8564
|
+
const windowStart = now - 6e4;
|
|
8565
|
+
while (this.window.length > 0 && this.window[0] < windowStart) {
|
|
8566
|
+
this.window.shift();
|
|
8567
|
+
}
|
|
8568
|
+
if (this.window.length >= this.maxPerMinute) {
|
|
8569
|
+
const oldestInWindow = this.window[0];
|
|
8570
|
+
const waitMs = oldestInWindow + 6e4 - now + 50;
|
|
8571
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
8572
|
+
const nowAfterWait = Date.now();
|
|
8573
|
+
const newWindowStart = nowAfterWait - 6e4;
|
|
8574
|
+
while (this.window.length > 0 && this.window[0] < newWindowStart) {
|
|
8575
|
+
this.window.shift();
|
|
8576
|
+
}
|
|
8577
|
+
}
|
|
8578
|
+
this.window.push(Date.now());
|
|
8579
|
+
} finally {
|
|
8580
|
+
releaseChain?.();
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
8583
|
+
};
|
|
8082
8584
|
var JobRunner = class {
|
|
8083
8585
|
db;
|
|
8084
8586
|
registry;
|
|
@@ -8099,13 +8601,34 @@ var JobRunner = class {
|
|
|
8099
8601
|
async executeRun(runId, projectId, providerOverride, locationOverride) {
|
|
8100
8602
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8101
8603
|
const startTime = Date.now();
|
|
8604
|
+
let runLocation;
|
|
8605
|
+
let activeProviders = [];
|
|
8606
|
+
let projectKeywords = [];
|
|
8607
|
+
const providerDispatchCounts = /* @__PURE__ */ new Map();
|
|
8102
8608
|
try {
|
|
8103
|
-
|
|
8609
|
+
const existingRun = this.getRunState(runId);
|
|
8610
|
+
if (!existingRun) {
|
|
8611
|
+
throw new Error(`Run ${runId} not found`);
|
|
8612
|
+
}
|
|
8613
|
+
if (existingRun.status === "cancelled") {
|
|
8614
|
+
this.handleCancelledRun(runId, projectId, startTime, {
|
|
8615
|
+
providerCount: 0,
|
|
8616
|
+
providers: [],
|
|
8617
|
+
keywordCount: 0
|
|
8618
|
+
});
|
|
8619
|
+
return;
|
|
8620
|
+
}
|
|
8621
|
+
if (existingRun.status !== "queued" && existingRun.status !== "running") {
|
|
8622
|
+
throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
|
|
8623
|
+
}
|
|
8624
|
+
if (existingRun.status === "queued") {
|
|
8625
|
+
this.db.update(runs).set({ status: "running", startedAt: now }).where(and6(eq16(runs.id, runId), eq16(runs.status, "queued"))).run();
|
|
8626
|
+
}
|
|
8627
|
+
this.throwIfRunCancelled(runId);
|
|
8104
8628
|
const project = this.db.select().from(projects).where(eq16(projects.id, projectId)).get();
|
|
8105
8629
|
if (!project) {
|
|
8106
8630
|
throw new Error(`Project ${projectId} not found`);
|
|
8107
8631
|
}
|
|
8108
|
-
let runLocation;
|
|
8109
8632
|
if (locationOverride === null) {
|
|
8110
8633
|
runLocation = void 0;
|
|
8111
8634
|
} else if (locationOverride) {
|
|
@@ -8117,16 +8640,26 @@ var JobRunner = class {
|
|
|
8117
8640
|
}
|
|
8118
8641
|
}
|
|
8119
8642
|
const projectProviders = providerOverride ?? JSON.parse(project.providers || "[]");
|
|
8120
|
-
|
|
8643
|
+
activeProviders = this.registry.getForProject(projectProviders);
|
|
8121
8644
|
if (activeProviders.length === 0) {
|
|
8122
8645
|
throw new Error("No providers configured. Add at least one provider API key.");
|
|
8123
8646
|
}
|
|
8124
8647
|
console.log(`[JobRunner] Run ${runId}: dispatching to ${activeProviders.length} providers: ${activeProviders.map((p) => p.adapter.name).join(", ")}`);
|
|
8125
|
-
|
|
8648
|
+
projectKeywords = this.db.select().from(keywords).where(eq16(keywords.projectId, projectId)).all();
|
|
8126
8649
|
const projectCompetitors = this.db.select().from(competitors).where(eq16(competitors.projectId, projectId)).all();
|
|
8127
8650
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
8651
|
+
const allDomains = effectiveDomains({
|
|
8652
|
+
canonicalDomain: project.canonicalDomain,
|
|
8653
|
+
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
8654
|
+
});
|
|
8655
|
+
const executionContext = {
|
|
8656
|
+
providerCount: activeProviders.length,
|
|
8657
|
+
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
8658
|
+
keywordCount: projectKeywords.length,
|
|
8659
|
+
...runLocation ? { location: runLocation.label } : {}
|
|
8660
|
+
};
|
|
8128
8661
|
const queriesPerProvider = projectKeywords.length;
|
|
8129
|
-
const todayPeriod =
|
|
8662
|
+
const todayPeriod = getCurrentUsageDay();
|
|
8130
8663
|
for (const p of activeProviders) {
|
|
8131
8664
|
const providerScope = `${projectId}:${p.adapter.name}`;
|
|
8132
8665
|
const providerUsage = this.db.select().from(usageCounters).where(eq16(usageCounters.scope, providerScope)).all().filter((r) => r.period === todayPeriod && r.metric === "queries").reduce((sum, r) => sum + r.count, 0);
|
|
@@ -8137,27 +8670,31 @@ var JobRunner = class {
|
|
|
8137
8670
|
);
|
|
8138
8671
|
}
|
|
8139
8672
|
}
|
|
8140
|
-
const
|
|
8141
|
-
for (const
|
|
8142
|
-
|
|
8673
|
+
const executionGates = /* @__PURE__ */ new Map();
|
|
8674
|
+
for (const provider of activeProviders) {
|
|
8675
|
+
executionGates.set(
|
|
8676
|
+
provider.adapter.name,
|
|
8677
|
+
new ProviderExecutionGate(
|
|
8678
|
+
provider.config.quotaPolicy.maxConcurrency,
|
|
8679
|
+
provider.config.quotaPolicy.maxRequestsPerMinute
|
|
8680
|
+
)
|
|
8681
|
+
);
|
|
8143
8682
|
}
|
|
8144
8683
|
const providerErrors = /* @__PURE__ */ new Map();
|
|
8145
8684
|
let totalSnapshotsInserted = 0;
|
|
8146
8685
|
const apiProviders = activeProviders.filter((p) => !isBrowserProvider(p.adapter.name));
|
|
8147
8686
|
const browserProviders = activeProviders.filter((p) => isBrowserProvider(p.adapter.name));
|
|
8148
|
-
|
|
8149
|
-
const
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
8160
|
-
});
|
|
8687
|
+
const processKeywordForProvider = async (registeredProvider, kw) => {
|
|
8688
|
+
const { adapter, config } = registeredProvider;
|
|
8689
|
+
const providerName = adapter.name;
|
|
8690
|
+
const gate = executionGates.get(providerName);
|
|
8691
|
+
if (!gate) {
|
|
8692
|
+
throw new Error(`Missing execution gate for provider ${providerName}`);
|
|
8693
|
+
}
|
|
8694
|
+
try {
|
|
8695
|
+
await gate.run(async () => {
|
|
8696
|
+
this.throwIfRunCancelled(runId);
|
|
8697
|
+
providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
|
|
8161
8698
|
const raw = await adapter.executeTrackedQuery(
|
|
8162
8699
|
{
|
|
8163
8700
|
keyword: kw.keyword,
|
|
@@ -8167,6 +8704,7 @@ var JobRunner = class {
|
|
|
8167
8704
|
},
|
|
8168
8705
|
config
|
|
8169
8706
|
);
|
|
8707
|
+
this.throwIfRunCancelled(runId);
|
|
8170
8708
|
const normalized = adapter.normalizeResult(raw);
|
|
8171
8709
|
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))}, domains=${JSON.stringify(allDomains)}`);
|
|
8172
8710
|
const citationState = determineCitationState(normalized, allDomains);
|
|
@@ -8222,96 +8760,29 @@ var JobRunner = class {
|
|
|
8222
8760
|
}
|
|
8223
8761
|
totalSnapshotsInserted++;
|
|
8224
8762
|
console.log(`[JobRunner] ${providerName}: keyword "${kw.keyword}" \u2192 ${citationState}`);
|
|
8225
|
-
}
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8763
|
+
});
|
|
8764
|
+
} catch (err) {
|
|
8765
|
+
if (err instanceof RunCancelledError) {
|
|
8766
|
+
throw err;
|
|
8229
8767
|
}
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
const { adapter, config } = registeredProvider;
|
|
8234
|
-
const providerName = adapter.name;
|
|
8235
|
-
try {
|
|
8236
|
-
await this.waitForRateLimit(
|
|
8237
|
-
minuteWindows.get(providerName),
|
|
8238
|
-
config.quotaPolicy.maxRequestsPerMinute
|
|
8239
|
-
);
|
|
8240
|
-
const allDomains = effectiveDomains({
|
|
8241
|
-
canonicalDomain: project.canonicalDomain,
|
|
8242
|
-
ownedDomains: JSON.parse(project.ownedDomains || "[]")
|
|
8243
|
-
});
|
|
8244
|
-
const raw = await adapter.executeTrackedQuery(
|
|
8245
|
-
{
|
|
8246
|
-
keyword: kw.keyword,
|
|
8247
|
-
canonicalDomains: allDomains,
|
|
8248
|
-
competitorDomains,
|
|
8249
|
-
location: runLocation
|
|
8250
|
-
},
|
|
8251
|
-
config
|
|
8252
|
-
);
|
|
8253
|
-
const normalized = adapter.normalizeResult(raw);
|
|
8254
|
-
console.log(`[JobRunner] ${providerName}: "${kw.keyword}" citedDomains=${JSON.stringify(normalized.citedDomains)}, groundingSources=${JSON.stringify(normalized.groundingSources.map((s) => s.uri))}, domains=${JSON.stringify(allDomains)}`);
|
|
8255
|
-
const citationState = determineCitationState(normalized, allDomains);
|
|
8256
|
-
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
8257
|
-
let screenshotRelPath = null;
|
|
8258
|
-
if (raw.screenshotPath && fs4.existsSync(raw.screenshotPath)) {
|
|
8259
|
-
const snapshotId = crypto15.randomUUID();
|
|
8260
|
-
const screenshotDir = path5.join(os4.homedir(), ".canonry", "screenshots", runId);
|
|
8261
|
-
if (!fs4.existsSync(screenshotDir)) fs4.mkdirSync(screenshotDir, { recursive: true });
|
|
8262
|
-
const destPath = path5.join(screenshotDir, `${snapshotId}.png`);
|
|
8263
|
-
fs4.renameSync(raw.screenshotPath, destPath);
|
|
8264
|
-
screenshotRelPath = `${runId}/${snapshotId}.png`;
|
|
8265
|
-
this.db.insert(querySnapshots).values({
|
|
8266
|
-
id: snapshotId,
|
|
8267
|
-
runId,
|
|
8268
|
-
keywordId: kw.id,
|
|
8269
|
-
provider: providerName,
|
|
8270
|
-
model: raw.model,
|
|
8271
|
-
citationState,
|
|
8272
|
-
answerText: normalized.answerText,
|
|
8273
|
-
citedDomains: JSON.stringify(normalized.citedDomains),
|
|
8274
|
-
competitorOverlap: JSON.stringify(overlap),
|
|
8275
|
-
location: runLocation?.label ?? null,
|
|
8276
|
-
screenshotPath: screenshotRelPath,
|
|
8277
|
-
rawResponse: JSON.stringify({
|
|
8278
|
-
model: raw.model,
|
|
8279
|
-
groundingSources: normalized.groundingSources,
|
|
8280
|
-
searchQueries: normalized.searchQueries,
|
|
8281
|
-
apiResponse: raw.rawResponse
|
|
8282
|
-
}),
|
|
8283
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8284
|
-
}).run();
|
|
8285
|
-
} else {
|
|
8286
|
-
this.db.insert(querySnapshots).values({
|
|
8287
|
-
id: crypto15.randomUUID(),
|
|
8288
|
-
runId,
|
|
8289
|
-
keywordId: kw.id,
|
|
8290
|
-
provider: providerName,
|
|
8291
|
-
model: raw.model,
|
|
8292
|
-
citationState,
|
|
8293
|
-
answerText: normalized.answerText,
|
|
8294
|
-
citedDomains: JSON.stringify(normalized.citedDomains),
|
|
8295
|
-
competitorOverlap: JSON.stringify(overlap),
|
|
8296
|
-
location: runLocation?.label ?? null,
|
|
8297
|
-
rawResponse: JSON.stringify({
|
|
8298
|
-
model: raw.model,
|
|
8299
|
-
groundingSources: normalized.groundingSources,
|
|
8300
|
-
searchQueries: normalized.searchQueries,
|
|
8301
|
-
apiResponse: raw.rawResponse
|
|
8302
|
-
}),
|
|
8303
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8304
|
-
}).run();
|
|
8305
|
-
}
|
|
8306
|
-
totalSnapshotsInserted++;
|
|
8307
|
-
console.log(`[JobRunner] ${providerName}: keyword "${kw.keyword}" \u2192 ${citationState}`);
|
|
8308
|
-
} catch (err) {
|
|
8309
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8310
|
-
console.error(`[JobRunner] ${providerName}: keyword "${kw.keyword}" FAILED: ${msg}`);
|
|
8768
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8769
|
+
console.error(`[JobRunner] ${providerName}: keyword "${kw.keyword}" FAILED: ${msg}`);
|
|
8770
|
+
if (!providerErrors.has(providerName)) {
|
|
8311
8771
|
providerErrors.set(providerName, msg);
|
|
8312
8772
|
}
|
|
8313
8773
|
}
|
|
8774
|
+
};
|
|
8775
|
+
await Promise.all(apiProviders.map(async (registeredProvider) => {
|
|
8776
|
+
await Promise.all(projectKeywords.map(async (kw) => {
|
|
8777
|
+
await processKeywordForProvider(registeredProvider, kw);
|
|
8778
|
+
}));
|
|
8779
|
+
}));
|
|
8780
|
+
for (const registeredProvider of browserProviders) {
|
|
8781
|
+
for (const kw of projectKeywords) {
|
|
8782
|
+
await processKeywordForProvider(registeredProvider, kw);
|
|
8783
|
+
}
|
|
8314
8784
|
}
|
|
8785
|
+
this.throwIfRunCancelled(runId);
|
|
8315
8786
|
const allFailed = totalSnapshotsInserted === 0 && providerErrors.size > 0;
|
|
8316
8787
|
const someFailed = providerErrors.size > 0;
|
|
8317
8788
|
if (allFailed) {
|
|
@@ -8323,18 +8794,16 @@ var JobRunner = class {
|
|
|
8323
8794
|
} else {
|
|
8324
8795
|
this.db.update(runs).set({ status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq16(runs.id, runId)).run();
|
|
8325
8796
|
}
|
|
8797
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
8326
8798
|
const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
|
|
8327
8799
|
trackEvent("run.completed", {
|
|
8328
8800
|
status: finalStatus,
|
|
8329
|
-
providerCount:
|
|
8330
|
-
providers:
|
|
8331
|
-
keywordCount:
|
|
8801
|
+
providerCount: executionContext.providerCount,
|
|
8802
|
+
providers: executionContext.providers,
|
|
8803
|
+
keywordCount: executionContext.keywordCount,
|
|
8332
8804
|
durationMs: Date.now() - startTime,
|
|
8333
|
-
...
|
|
8805
|
+
...executionContext.location ? { location: executionContext.location } : {}
|
|
8334
8806
|
});
|
|
8335
|
-
for (const p of activeProviders) {
|
|
8336
|
-
this.incrementUsage(`${projectId}:${p.adapter.name}`, "queries", queriesPerProvider);
|
|
8337
|
-
}
|
|
8338
8807
|
this.incrementUsage(projectId, "runs", 1);
|
|
8339
8808
|
if (this.onRunCompleted) {
|
|
8340
8809
|
this.onRunCompleted(runId, projectId).catch((err) => {
|
|
@@ -8342,18 +8811,31 @@ var JobRunner = class {
|
|
|
8342
8811
|
});
|
|
8343
8812
|
}
|
|
8344
8813
|
} catch (err) {
|
|
8814
|
+
const executionContext = {
|
|
8815
|
+
providerCount: activeProviders.length,
|
|
8816
|
+
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
8817
|
+
keywordCount: projectKeywords.length,
|
|
8818
|
+
...runLocation ? { location: runLocation.label } : {}
|
|
8819
|
+
};
|
|
8820
|
+
if (err instanceof RunCancelledError || this.isRunCancelled(runId)) {
|
|
8821
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
8822
|
+
this.handleCancelledRun(runId, projectId, startTime, executionContext);
|
|
8823
|
+
return;
|
|
8824
|
+
}
|
|
8345
8825
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
8346
8826
|
this.db.update(runs).set({
|
|
8347
8827
|
status: "failed",
|
|
8348
8828
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8349
8829
|
error: errorMessage
|
|
8350
8830
|
}).where(eq16(runs.id, runId)).run();
|
|
8831
|
+
this.flushProviderUsage(projectId, providerDispatchCounts);
|
|
8351
8832
|
trackEvent("run.completed", {
|
|
8352
8833
|
status: "failed",
|
|
8353
|
-
providerCount:
|
|
8354
|
-
providers:
|
|
8355
|
-
keywordCount:
|
|
8356
|
-
durationMs: Date.now() - startTime
|
|
8834
|
+
providerCount: executionContext.providerCount,
|
|
8835
|
+
providers: executionContext.providers,
|
|
8836
|
+
keywordCount: executionContext.keywordCount,
|
|
8837
|
+
durationMs: Date.now() - startTime,
|
|
8838
|
+
...executionContext.location ? { location: executionContext.location } : {}
|
|
8357
8839
|
});
|
|
8358
8840
|
if (this.onRunCompleted) {
|
|
8359
8841
|
this.onRunCompleted(runId, projectId).catch((notifErr) => {
|
|
@@ -8362,27 +8844,9 @@ var JobRunner = class {
|
|
|
8362
8844
|
}
|
|
8363
8845
|
}
|
|
8364
8846
|
}
|
|
8365
|
-
async waitForRateLimit(window, maxPerMinute) {
|
|
8366
|
-
const now = Date.now();
|
|
8367
|
-
const windowStart = now - 6e4;
|
|
8368
|
-
while (window.length > 0 && window[0] < windowStart) {
|
|
8369
|
-
window.shift();
|
|
8370
|
-
}
|
|
8371
|
-
if (window.length >= maxPerMinute) {
|
|
8372
|
-
const oldestInWindow = window[0];
|
|
8373
|
-
const waitMs = oldestInWindow + 6e4 - now + 50;
|
|
8374
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
8375
|
-
const nowAfterWait = Date.now();
|
|
8376
|
-
const newWindowStart = nowAfterWait - 6e4;
|
|
8377
|
-
while (window.length > 0 && window[0] < newWindowStart) {
|
|
8378
|
-
window.shift();
|
|
8379
|
-
}
|
|
8380
|
-
}
|
|
8381
|
-
window.push(Date.now());
|
|
8382
|
-
}
|
|
8383
8847
|
incrementUsage(scope, metric, count) {
|
|
8384
8848
|
const now = /* @__PURE__ */ new Date();
|
|
8385
|
-
const period =
|
|
8849
|
+
const period = now.toISOString().slice(0, 10);
|
|
8386
8850
|
const id = crypto15.randomUUID();
|
|
8387
8851
|
const existing = this.db.select().from(usageCounters).where(eq16(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
|
|
8388
8852
|
if (existing) {
|
|
@@ -8398,10 +8862,52 @@ var JobRunner = class {
|
|
|
8398
8862
|
}).run();
|
|
8399
8863
|
}
|
|
8400
8864
|
}
|
|
8865
|
+
flushProviderUsage(projectId, providerDispatchCounts) {
|
|
8866
|
+
for (const [providerName, count] of providerDispatchCounts.entries()) {
|
|
8867
|
+
if (count <= 0) continue;
|
|
8868
|
+
this.incrementUsage(`${projectId}:${providerName}`, "queries", count);
|
|
8869
|
+
}
|
|
8870
|
+
}
|
|
8871
|
+
getRunState(runId) {
|
|
8872
|
+
return this.db.select({
|
|
8873
|
+
status: runs.status,
|
|
8874
|
+
finishedAt: runs.finishedAt,
|
|
8875
|
+
error: runs.error
|
|
8876
|
+
}).from(runs).where(eq16(runs.id, runId)).get();
|
|
8877
|
+
}
|
|
8878
|
+
isRunCancelled(runId) {
|
|
8879
|
+
return this.getRunState(runId)?.status === "cancelled";
|
|
8880
|
+
}
|
|
8881
|
+
throwIfRunCancelled(runId) {
|
|
8882
|
+
if (this.isRunCancelled(runId)) {
|
|
8883
|
+
throw new RunCancelledError(runId);
|
|
8884
|
+
}
|
|
8885
|
+
}
|
|
8886
|
+
handleCancelledRun(runId, projectId, startTime, context) {
|
|
8887
|
+
const currentRun = this.getRunState(runId);
|
|
8888
|
+
if (currentRun && !currentRun.finishedAt) {
|
|
8889
|
+
this.db.update(runs).set({
|
|
8890
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8891
|
+
error: currentRun.error ?? "Cancelled by user"
|
|
8892
|
+
}).where(eq16(runs.id, runId)).run();
|
|
8893
|
+
}
|
|
8894
|
+
trackEvent("run.completed", {
|
|
8895
|
+
status: "cancelled",
|
|
8896
|
+
providerCount: context.providerCount,
|
|
8897
|
+
providers: context.providers,
|
|
8898
|
+
keywordCount: context.keywordCount,
|
|
8899
|
+
durationMs: Date.now() - startTime,
|
|
8900
|
+
...context.location ? { location: context.location } : {}
|
|
8901
|
+
});
|
|
8902
|
+
if (this.onRunCompleted) {
|
|
8903
|
+
this.onRunCompleted(runId, projectId).catch((err) => {
|
|
8904
|
+
console.error("[JobRunner] Notification callback failed:", err);
|
|
8905
|
+
});
|
|
8906
|
+
}
|
|
8907
|
+
}
|
|
8401
8908
|
};
|
|
8402
|
-
function
|
|
8403
|
-
|
|
8404
|
-
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
8909
|
+
function getCurrentUsageDay() {
|
|
8910
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8405
8911
|
}
|
|
8406
8912
|
function domainMatches(domain, canonicalDomain) {
|
|
8407
8913
|
const normalized = normalizeProjectDomain(canonicalDomain);
|
|
@@ -8467,7 +8973,7 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
|
8467
8973
|
|
|
8468
8974
|
// src/gsc-sync.ts
|
|
8469
8975
|
import crypto16 from "crypto";
|
|
8470
|
-
import { eq as eq17, and as
|
|
8976
|
+
import { eq as eq17, and as and7, sql as sql3 } from "drizzle-orm";
|
|
8471
8977
|
function formatDate(d) {
|
|
8472
8978
|
return d.toISOString().split("T")[0];
|
|
8473
8979
|
}
|
|
@@ -8518,7 +9024,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
8518
9024
|
});
|
|
8519
9025
|
console.log(`[GSC Sync] Received ${rows.length} rows`);
|
|
8520
9026
|
db.delete(gscSearchData).where(
|
|
8521
|
-
|
|
9027
|
+
and7(
|
|
8522
9028
|
eq17(gscSearchData.projectId, projectId),
|
|
8523
9029
|
sql3`${gscSearchData.date} >= ${startDate}`,
|
|
8524
9030
|
sql3`${gscSearchData.date} <= ${endDate}`
|
|
@@ -8607,7 +9113,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
8607
9113
|
}
|
|
8608
9114
|
}
|
|
8609
9115
|
const snapshotDate = formatDate(/* @__PURE__ */ new Date());
|
|
8610
|
-
db.delete(gscCoverageSnapshots).where(
|
|
9116
|
+
db.delete(gscCoverageSnapshots).where(and7(eq17(gscCoverageSnapshots.projectId, projectId), eq17(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
8611
9117
|
db.insert(gscCoverageSnapshots).values({
|
|
8612
9118
|
id: crypto16.randomUUID(),
|
|
8613
9119
|
projectId,
|
|
@@ -8630,7 +9136,7 @@ async function executeGscSync(db, runId, projectId, opts) {
|
|
|
8630
9136
|
|
|
8631
9137
|
// src/gsc-inspect-sitemap.ts
|
|
8632
9138
|
import crypto17 from "crypto";
|
|
8633
|
-
import { eq as eq18, and as
|
|
9139
|
+
import { eq as eq18, and as and8 } from "drizzle-orm";
|
|
8634
9140
|
|
|
8635
9141
|
// src/sitemap-parser.ts
|
|
8636
9142
|
var LOC_REGEX = /<loc>\s*([^<]+?)\s*<\/loc>/gi;
|
|
@@ -8793,7 +9299,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
|
|
|
8793
9299
|
}
|
|
8794
9300
|
}
|
|
8795
9301
|
const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
8796
|
-
db.delete(gscCoverageSnapshots).where(
|
|
9302
|
+
db.delete(gscCoverageSnapshots).where(and8(eq18(gscCoverageSnapshots.projectId, projectId), eq18(gscCoverageSnapshots.date, snapshotDate))).run();
|
|
8797
9303
|
db.insert(gscCoverageSnapshots).values({
|
|
8798
9304
|
id: crypto17.randomUUID(),
|
|
8799
9305
|
projectId,
|
|
@@ -8985,7 +9491,7 @@ var Scheduler = class {
|
|
|
8985
9491
|
};
|
|
8986
9492
|
|
|
8987
9493
|
// src/notifier.ts
|
|
8988
|
-
import { eq as eq20, desc as
|
|
9494
|
+
import { eq as eq20, desc as desc6, and as and9, or as or2 } from "drizzle-orm";
|
|
8989
9495
|
import crypto18 from "crypto";
|
|
8990
9496
|
var Notifier = class {
|
|
8991
9497
|
db;
|
|
@@ -9048,11 +9554,11 @@ var Notifier = class {
|
|
|
9048
9554
|
}
|
|
9049
9555
|
computeTransitions(runId, projectId) {
|
|
9050
9556
|
const recentRuns = this.db.select().from(runs).where(
|
|
9051
|
-
|
|
9557
|
+
and9(
|
|
9052
9558
|
eq20(runs.projectId, projectId),
|
|
9053
9559
|
or2(eq20(runs.status, "completed"), eq20(runs.status, "partial"))
|
|
9054
9560
|
)
|
|
9055
|
-
).orderBy(
|
|
9561
|
+
).orderBy(desc6(runs.createdAt)).limit(2).all();
|
|
9056
9562
|
if (recentRuns.length < 2) return [];
|
|
9057
9563
|
const currentRunId = recentRuns[0].id;
|
|
9058
9564
|
const previousRunId = recentRuns[1].id;
|
|
@@ -9255,6 +9761,20 @@ var DEFAULT_QUOTA = {
|
|
|
9255
9761
|
maxRequestsPerMinute: 10,
|
|
9256
9762
|
maxRequestsPerDay: 1e3
|
|
9257
9763
|
};
|
|
9764
|
+
var API_ADAPTERS = [
|
|
9765
|
+
geminiAdapter,
|
|
9766
|
+
openaiAdapter,
|
|
9767
|
+
claudeAdapter,
|
|
9768
|
+
localAdapter,
|
|
9769
|
+
perplexityAdapter
|
|
9770
|
+
];
|
|
9771
|
+
var BROWSER_ADAPTERS = [
|
|
9772
|
+
cdpChatgptAdapter
|
|
9773
|
+
];
|
|
9774
|
+
var ALL_ADAPTERS = [...API_ADAPTERS, ...BROWSER_ADAPTERS];
|
|
9775
|
+
var adapterMap = Object.fromEntries(
|
|
9776
|
+
API_ADAPTERS.map((a) => [a.name, a])
|
|
9777
|
+
);
|
|
9258
9778
|
function summarizeProviderConfig(provider, config) {
|
|
9259
9779
|
return {
|
|
9260
9780
|
configured: Boolean(config?.apiKey || config?.baseUrl),
|
|
@@ -9291,38 +9811,19 @@ async function createServer(opts) {
|
|
|
9291
9811
|
const p = providers[k];
|
|
9292
9812
|
return p?.apiKey || p?.baseUrl;
|
|
9293
9813
|
}));
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
quotaPolicy: providers.openai.quota ?? DEFAULT_QUOTA
|
|
9308
|
-
});
|
|
9309
|
-
}
|
|
9310
|
-
if (providers.claude?.apiKey) {
|
|
9311
|
-
registry.register(claudeAdapter, {
|
|
9312
|
-
provider: "claude",
|
|
9313
|
-
apiKey: providers.claude.apiKey,
|
|
9314
|
-
model: providers.claude.model,
|
|
9315
|
-
quotaPolicy: providers.claude.quota ?? DEFAULT_QUOTA
|
|
9316
|
-
});
|
|
9317
|
-
}
|
|
9318
|
-
if (providers.local?.baseUrl) {
|
|
9319
|
-
registry.register(localAdapter, {
|
|
9320
|
-
provider: "local",
|
|
9321
|
-
apiKey: providers.local.apiKey,
|
|
9322
|
-
baseUrl: providers.local.baseUrl,
|
|
9323
|
-
model: providers.local.model,
|
|
9324
|
-
quotaPolicy: providers.local.quota ?? DEFAULT_QUOTA
|
|
9325
|
-
});
|
|
9814
|
+
for (const adapter of API_ADAPTERS) {
|
|
9815
|
+
const entry = providers[adapter.name];
|
|
9816
|
+
if (!entry) continue;
|
|
9817
|
+
const isConfigured = adapter.name === "local" ? !!entry.baseUrl : !!entry.apiKey;
|
|
9818
|
+
if (isConfigured) {
|
|
9819
|
+
registry.register(adapter, {
|
|
9820
|
+
provider: adapter.name,
|
|
9821
|
+
apiKey: entry.apiKey,
|
|
9822
|
+
baseUrl: entry.baseUrl,
|
|
9823
|
+
model: entry.model,
|
|
9824
|
+
quotaPolicy: entry.quota ?? DEFAULT_QUOTA
|
|
9825
|
+
});
|
|
9826
|
+
}
|
|
9326
9827
|
}
|
|
9327
9828
|
const cdpConfig = opts.config.cdp;
|
|
9328
9829
|
if (cdpConfig?.host || cdpConfig?.port) {
|
|
@@ -9347,11 +9848,14 @@ async function createServer(opts) {
|
|
|
9347
9848
|
});
|
|
9348
9849
|
}
|
|
9349
9850
|
});
|
|
9350
|
-
const providerSummary =
|
|
9351
|
-
name,
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9851
|
+
const providerSummary = API_ADAPTERS.map((adapter) => ({
|
|
9852
|
+
name: adapter.name,
|
|
9853
|
+
displayName: adapter.displayName,
|
|
9854
|
+
keyUrl: adapter.keyUrl,
|
|
9855
|
+
modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
|
|
9856
|
+
model: registry.get(adapter.name)?.config.model,
|
|
9857
|
+
configured: !!registry.get(adapter.name),
|
|
9858
|
+
quota: registry.get(adapter.name)?.config.quotaPolicy
|
|
9355
9859
|
}));
|
|
9356
9860
|
const googleSettingsSummary = {
|
|
9357
9861
|
configured: Boolean(opts.config.google?.clientId && opts.config.google?.clientSecret)
|
|
@@ -9391,7 +9895,6 @@ async function createServer(opts) {
|
|
|
9391
9895
|
return true;
|
|
9392
9896
|
}
|
|
9393
9897
|
};
|
|
9394
|
-
const adapterMap = { gemini: geminiAdapter, openai: openaiAdapter, claude: claudeAdapter, local: localAdapter };
|
|
9395
9898
|
const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto19.randomBytes(32).toString("hex");
|
|
9396
9899
|
const googleConnectionStore = {
|
|
9397
9900
|
listConnections: (domain) => listGoogleConnections(opts.config, domain),
|
|
@@ -9455,6 +9958,13 @@ async function createServer(opts) {
|
|
|
9455
9958
|
version: PKG_VERSION
|
|
9456
9959
|
},
|
|
9457
9960
|
providerSummary,
|
|
9961
|
+
providerAdapters: [...API_ADAPTERS, ...BROWSER_ADAPTERS].map((a) => ({
|
|
9962
|
+
name: a.name,
|
|
9963
|
+
displayName: a.displayName,
|
|
9964
|
+
mode: a.mode,
|
|
9965
|
+
modelValidationPattern: a.modelRegistry.validationPattern,
|
|
9966
|
+
modelValidationHint: a.modelRegistry.validationHint
|
|
9967
|
+
})),
|
|
9458
9968
|
googleSettingsSummary,
|
|
9459
9969
|
bingSettingsSummary,
|
|
9460
9970
|
bingConnectionStore,
|
|
@@ -9465,7 +9975,7 @@ async function createServer(opts) {
|
|
|
9465
9975
|
},
|
|
9466
9976
|
onProviderUpdate: (providerName, apiKey, model, baseUrl, incomingQuota) => {
|
|
9467
9977
|
const name = providerName;
|
|
9468
|
-
if (!
|
|
9978
|
+
if (!adapterMap[name]) return null;
|
|
9469
9979
|
if (!opts.config.providers) opts.config.providers = {};
|
|
9470
9980
|
const existing = opts.config.providers[name];
|
|
9471
9981
|
const beforeConfig = summarizeProviderConfig(name, existing);
|
|
@@ -9748,14 +10258,6 @@ function parseKeywordResponse(raw, count) {
|
|
|
9748
10258
|
}
|
|
9749
10259
|
|
|
9750
10260
|
export {
|
|
9751
|
-
providerQuotaPolicySchema,
|
|
9752
|
-
resolveProviderInput,
|
|
9753
|
-
notificationEventSchema,
|
|
9754
|
-
getDefaultModel,
|
|
9755
|
-
effectiveDomains,
|
|
9756
|
-
apiKeys,
|
|
9757
|
-
createClient,
|
|
9758
|
-
migrate,
|
|
9759
10261
|
getConfigDir,
|
|
9760
10262
|
getConfigPath,
|
|
9761
10263
|
loadConfig,
|
|
@@ -9766,6 +10268,13 @@ export {
|
|
|
9766
10268
|
isFirstRun,
|
|
9767
10269
|
showFirstRunNotice,
|
|
9768
10270
|
trackEvent,
|
|
10271
|
+
providerQuotaPolicySchema,
|
|
10272
|
+
resolveProviderInput,
|
|
10273
|
+
notificationEventSchema,
|
|
10274
|
+
effectiveDomains,
|
|
9769
10275
|
setGoogleAuthConfig,
|
|
10276
|
+
apiKeys,
|
|
10277
|
+
createClient,
|
|
10278
|
+
migrate,
|
|
9770
10279
|
createServer
|
|
9771
10280
|
};
|