@paroicms/site-generator-plugin 0.7.0 → 0.8.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/gen-backend/ddl/site-generator.ddl.sql +22 -0
- package/gen-backend/dist/commands/execute-command.js +60 -0
- package/gen-backend/dist/{generator/session → commands}/generator-session.js +16 -8
- package/gen-backend/dist/context.js +6 -0
- package/gen-backend/dist/db/db-init.js +35 -0
- package/gen-backend/dist/db/db.queries.js +60 -0
- package/gen-backend/dist/db/ddl-migration.js +15 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/content-report.js +11 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +20 -14
- package/gen-backend/dist/generator/llm-queries/invoke-message-guard.js +2 -0
- package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +4 -1
- package/gen-backend/dist/generator/llm-queries/invoke-update-site-schema.js +5 -1
- package/gen-backend/dist/generator/site-generator/site-generator.js +14 -11
- package/gen-backend/dist/lib/site-remover.js +39 -0
- package/gen-backend/dist/plugin.js +41 -23
- package/gen-front/dist/gen-front.css +1 -1
- package/package.json +7 -5
- package/gen-backend/dist/generator/actions.js +0 -45
- /package/gen-backend/dist/{generator/generator-types.js → lib/internal-types.js} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
PRAGMA journal_mode = TRUNCATE;
|
|
2
|
+
PRAGMA foreign_keys = 1;
|
|
3
|
+
|
|
4
|
+
create table PaMetadata (
|
|
5
|
+
dbSchema varchar(100) not null,
|
|
6
|
+
k varchar(100) not null,
|
|
7
|
+
val varchar(250) not null,
|
|
8
|
+
primary key (dbSchema, k)
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
insert into PaMetadata (dbSchema, k, val) values ('site-generator', 'dbSchemaVersion', '1');
|
|
12
|
+
|
|
13
|
+
create table PaGenSession (
|
|
14
|
+
id char(36) not null primary key,
|
|
15
|
+
createdAt timestamp not null default current_timestamp,
|
|
16
|
+
status varchar(100) not null,
|
|
17
|
+
guardCount integer not null default 0,
|
|
18
|
+
promptCount integer not null default 0,
|
|
19
|
+
nodeTypeCount integer not null default 0,
|
|
20
|
+
contentCount integer not null default 0,
|
|
21
|
+
errorMessage varchar(500)
|
|
22
|
+
);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { messageOf } from "@paroi/data-formatters-lib";
|
|
2
|
+
import { updateSession } from "../db/db.queries.js";
|
|
3
|
+
import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
|
|
4
|
+
import { invokeNewSiteAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
|
|
5
|
+
import { invokeUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
|
|
6
|
+
import { generateSite } from "../generator/site-generator/site-generator.js";
|
|
7
|
+
import { newSession, verifySessionToken } from "./generator-session.js";
|
|
8
|
+
export async function executeCommand(commandCtx, input) {
|
|
9
|
+
if (input.command === "newSession") {
|
|
10
|
+
return {
|
|
11
|
+
success: true,
|
|
12
|
+
result: await newSession(commandCtx, input),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const { sessionId } = await verifySessionToken(commandCtx, input.sessionToken);
|
|
16
|
+
const ctx = {
|
|
17
|
+
...commandCtx,
|
|
18
|
+
sessionId,
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
if (input.command === "createSiteSchema") {
|
|
22
|
+
const errorResponse = await invokeMessageGuard(ctx, input);
|
|
23
|
+
if (errorResponse)
|
|
24
|
+
return errorResponse;
|
|
25
|
+
return {
|
|
26
|
+
success: true,
|
|
27
|
+
result: await invokeNewSiteAnalysis(ctx, input),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (input.command === "updateSiteSchema") {
|
|
31
|
+
const errorResponse = await invokeMessageGuard(ctx, input, { skipOutOfScopeCheck: true });
|
|
32
|
+
if (errorResponse)
|
|
33
|
+
return errorResponse;
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
result: await invokeUpdateSiteSchema(ctx, input),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (input.command === "generateSite") {
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
result: await generateSite(ctx, input),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
ctx.logger.error(`Invalid command: ${input.command}`);
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
userMessage: "Invalid command",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
try {
|
|
53
|
+
await updateSession(ctx, { status: "error", errorMessage: messageOf(error) });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
ctx.logger.error("Failed to update session error message", error);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { ApiError } from "@paroicms/public-server-lib";
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { insertSession, readSession } from "../db/db.queries.js";
|
|
3
4
|
const { sign, verify } = (await import("jsonwebtoken")).default;
|
|
4
|
-
const JWT_SECRET = "init"; // FIXME: Hardcoded JWT secret as specified
|
|
5
5
|
export async function newSession(ctx, command) {
|
|
6
6
|
const { service } = ctx;
|
|
7
7
|
const { recaptchaToken } = command;
|
|
8
8
|
const isValid = await service.validateRecaptchaResponse(recaptchaToken);
|
|
9
9
|
if (!isValid)
|
|
10
10
|
throw new ApiError("Invalid reCAPTCHA token", 400);
|
|
11
|
-
const sessionId =
|
|
12
|
-
const token = sign({ sessionId },
|
|
11
|
+
const sessionId = randomUUID();
|
|
12
|
+
const token = sign({ sessionId }, ctx.jwtSecret, { expiresIn: "48h" });
|
|
13
|
+
await insertSession(ctx, { sessionId });
|
|
13
14
|
return { token };
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Verifies a session token and returns the session ID if valid
|
|
17
18
|
*/
|
|
18
|
-
export function verifySessionToken(token) {
|
|
19
|
+
export async function verifySessionToken(ctx, token) {
|
|
19
20
|
let payload;
|
|
20
21
|
try {
|
|
21
|
-
payload = verify(token,
|
|
22
|
+
payload = verify(token, ctx.jwtSecret);
|
|
22
23
|
}
|
|
23
24
|
catch (error) {
|
|
24
25
|
if (error.name === "TokenExpiredError")
|
|
@@ -27,7 +28,14 @@ export function verifySessionToken(token) {
|
|
|
27
28
|
throw new ApiError("Invalid session token", 401);
|
|
28
29
|
throw error;
|
|
29
30
|
}
|
|
30
|
-
if (!payload || !payload.sessionId)
|
|
31
|
+
if (!payload || !("sessionId" in payload) || typeof payload.sessionId !== "string") {
|
|
31
32
|
throw new ApiError("Invalid session token", 401);
|
|
32
|
-
|
|
33
|
+
}
|
|
34
|
+
const sessionId = payload.sessionId;
|
|
35
|
+
const session = await readSession(ctx, sessionId);
|
|
36
|
+
if (!session)
|
|
37
|
+
throw new ApiError(`Missing session "${sessionId}"`, 500);
|
|
38
|
+
if (session.promptCount >= 20)
|
|
39
|
+
throw new ApiError("Session consumed", 401);
|
|
40
|
+
return { sessionId };
|
|
33
41
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
import { strVal } from "@paroi/data-formatters-lib";
|
|
1
2
|
import { resolveModuleDirectory } from "@paroicms/public-server-lib";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
2
5
|
export const projectDir = resolveModuleDirectory(import.meta.url, { parent: true });
|
|
6
|
+
export const packageDir = dirname(projectDir);
|
|
7
|
+
export const pluginVersion = strVal(JSON.parse(readFileSync(join(packageDir, "package.json"), "utf-8")).version);
|
|
8
|
+
export const SLUG = "site-generator";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createOrOpenSqliteConnection, createSqlLogger, getMetadataDbSchemaVersion, } from "@paroicms/internal-server-lib";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { projectDir } from "../context.js";
|
|
4
|
+
import { currentDbSchemaVersion, dbSchemaName, migrateSiteGeneratorDb } from "./ddl-migration.js";
|
|
5
|
+
export async function createOrOpenSiteGeneratorConnection({ sqliteFile, canCreate, logger, }) {
|
|
6
|
+
const { logNextQuery, knexLogger } = createSqlLogger({ logger, dbSchemaName });
|
|
7
|
+
const { cn } = await createOrOpenSqliteConnection({
|
|
8
|
+
canCreate,
|
|
9
|
+
dbSchemaName,
|
|
10
|
+
sqliteFile,
|
|
11
|
+
ddlFile: join(projectDir, "ddl", "site-generator.ddl.sql"),
|
|
12
|
+
migrateDb,
|
|
13
|
+
knexLogger,
|
|
14
|
+
logger,
|
|
15
|
+
});
|
|
16
|
+
async function migrateDb(cn) {
|
|
17
|
+
const dbVersion = await getMetadataDbSchemaVersion(cn, { dbSchemaName });
|
|
18
|
+
if (dbVersion === currentDbSchemaVersion) {
|
|
19
|
+
return {
|
|
20
|
+
migrated: false,
|
|
21
|
+
schemaVersion: dbVersion,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
await migrateSiteGeneratorDb(cn, {
|
|
25
|
+
fromVersion: dbVersion,
|
|
26
|
+
logger,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
migrated: true,
|
|
30
|
+
fromVersion: dbVersion,
|
|
31
|
+
schemaVersion: currentDbSchemaVersion,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { cn, logNextQuery };
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { strVal } from "@paroi/data-formatters-lib";
|
|
2
|
+
export async function readSession(ctx, sessionId) {
|
|
3
|
+
const { cn } = ctx;
|
|
4
|
+
const row = await cn("PaGenSession")
|
|
5
|
+
.select("id", "status", "guardCount", "promptCount", "nodeTypeCount", "contentCount", "errorMessage")
|
|
6
|
+
.where({ id: sessionId })
|
|
7
|
+
.first();
|
|
8
|
+
if (!row)
|
|
9
|
+
return;
|
|
10
|
+
return {
|
|
11
|
+
id: strVal(row.id),
|
|
12
|
+
createdAt: strVal(row.createdAt),
|
|
13
|
+
status: formatSessionStatus(row.status),
|
|
14
|
+
guardCount: Number(row.guardCount),
|
|
15
|
+
promptCount: Number(row.promptCount),
|
|
16
|
+
nodeTypeCount: Number(row.nodeTypeCount),
|
|
17
|
+
contentCount: Number(row.contentCount),
|
|
18
|
+
errorMessage: strVal(row.errorMessage),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export async function insertSession(ctx, { sessionId }) {
|
|
22
|
+
const { cn } = ctx;
|
|
23
|
+
await cn("PaGenSession").insert({
|
|
24
|
+
id: sessionId,
|
|
25
|
+
status: "started",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function updateSession(ctx, values) {
|
|
29
|
+
const { sessionId, cn } = ctx;
|
|
30
|
+
const { status, guardCountInc, promptCountInc, nodeTypeCount, contentCountInc, errorMessage } = values;
|
|
31
|
+
const affected = await cn("PaGenSession")
|
|
32
|
+
.where({
|
|
33
|
+
id: sessionId,
|
|
34
|
+
})
|
|
35
|
+
.update({
|
|
36
|
+
status,
|
|
37
|
+
guardCountInc: guardCountInc ? cn.raw(`guardCount + ${guardCountInc}`) : undefined,
|
|
38
|
+
promptCount: promptCountInc ? cn.raw(`promptCount + ${promptCountInc}`) : undefined,
|
|
39
|
+
nodeTypeCount,
|
|
40
|
+
contentCount: contentCountInc ? cn.raw(`contentCount + ${contentCountInc}`) : undefined,
|
|
41
|
+
errorMessage,
|
|
42
|
+
});
|
|
43
|
+
if (affected !== 1) {
|
|
44
|
+
throw new Error(`Session "${sessionId}" not found`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function formatSessionStatus(val) {
|
|
48
|
+
const v = strVal(val);
|
|
49
|
+
if (v === "started")
|
|
50
|
+
return "started";
|
|
51
|
+
if (v === "analyzed")
|
|
52
|
+
return "analyzed";
|
|
53
|
+
if (v === "updated")
|
|
54
|
+
return "updated";
|
|
55
|
+
if (v === "generated")
|
|
56
|
+
return "generated";
|
|
57
|
+
if (v === "error")
|
|
58
|
+
return "error";
|
|
59
|
+
throw new Error(`Invalid session status: "${val}"`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const dbSchemaName = "site-generator";
|
|
2
|
+
export const currentDbSchemaVersion = 1;
|
|
3
|
+
export async function migrateSiteGeneratorDb(_cn, { fromVersion, logger }) {
|
|
4
|
+
const toVersion = currentDbSchemaVersion;
|
|
5
|
+
const currentVersion = fromVersion;
|
|
6
|
+
if (currentVersion === 1) {
|
|
7
|
+
// await cn.raw("...");
|
|
8
|
+
// await setMetadataDbSchemaVersion(cn, { dbSchemaName, value: 2 });
|
|
9
|
+
// currentVersion = 2;
|
|
10
|
+
}
|
|
11
|
+
if (currentVersion !== toVersion) {
|
|
12
|
+
throw new Error(`version of ${dbSchemaName} database should be '${toVersion}', but is '${currentVersion}'`);
|
|
13
|
+
}
|
|
14
|
+
logger.info(`${dbSchemaName} database was migrated from ${fromVersion} to ${currentVersion}`);
|
|
15
|
+
}
|
package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js
CHANGED
|
@@ -2,19 +2,21 @@ import { getPartTypeByName, getRegularDocumentTypeByName, getRoutingDocumentType
|
|
|
2
2
|
import { createSimpleTranslator, } from "@paroicms/public-server-lib";
|
|
3
3
|
import { getRandomImagePath } from "../lib/images-lib.js";
|
|
4
4
|
import { createTaskCollector } from "../lib/tasks.js";
|
|
5
|
+
import { createGeneratedContentReport } from "./content-report.js";
|
|
5
6
|
import { generateLocalizedFooterMention } from "./create-node-contents.js";
|
|
6
7
|
import { generateFieldSetContent, generateMultipleFieldSetContents, } from "./generate-fake-content.js";
|
|
7
|
-
export async function fillSiteWithFakeContent(ctx, {
|
|
8
|
+
export async function fillSiteWithFakeContent(ctx, { regSite, siteTitle }) {
|
|
8
9
|
const { service } = ctx;
|
|
9
|
-
const { fqdn } =
|
|
10
|
+
const { fqdn } = regSite;
|
|
11
|
+
const report = createGeneratedContentReport();
|
|
10
12
|
const { siteSchema, siteIds } = await service.connector.loadSiteSchemaAndIds(fqdn);
|
|
11
13
|
const schemaI18n = createSimpleTranslator({
|
|
12
14
|
labels: siteSchema.l10n,
|
|
13
15
|
logger: ctx.logger,
|
|
14
16
|
});
|
|
15
|
-
await updateSiteFields(ctx, { fqdn, siteSchema,
|
|
17
|
+
await updateSiteFields(ctx, report, { fqdn, siteSchema, siteTitle });
|
|
16
18
|
const tasks = createTaskCollector(ctx);
|
|
17
|
-
fillRoutingDocumentAndAddChildren(ctx, tasks, {
|
|
19
|
+
fillRoutingDocumentAndAddChildren(ctx, tasks, report, {
|
|
18
20
|
fqdn,
|
|
19
21
|
siteSchema,
|
|
20
22
|
schemaI18n,
|
|
@@ -28,15 +30,16 @@ export async function fillSiteWithFakeContent(ctx, { siteConf, siteId, siteTitle
|
|
|
28
30
|
ctx.logger.warn(`Failed to generate ${errorMessages.length} documents:\n - ${errorMessages.join("\n - ")}`);
|
|
29
31
|
}
|
|
30
32
|
ctx.logger.debug(`… Executed ${doneCount} generating tasks`);
|
|
33
|
+
return report;
|
|
31
34
|
}
|
|
32
|
-
function fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, nodeOptions) {
|
|
35
|
+
function fillRoutingDocumentAndAddChildren(ctx, tasks, report, siteOptions, nodeOptions) {
|
|
33
36
|
const { routingIds, nodeType } = nodeOptions;
|
|
34
37
|
const { siteSchema } = siteOptions;
|
|
35
|
-
tasks.add(() => updateRoutingDocument(ctx, siteOptions, nodeOptions));
|
|
38
|
+
tasks.add(() => updateRoutingDocument(ctx, report, siteOptions, nodeOptions));
|
|
36
39
|
for (const listType of nodeType.lists ?? []) {
|
|
37
40
|
for (const typeName of listType.parts) {
|
|
38
41
|
const partType = getPartTypeByName(siteSchema, typeName);
|
|
39
|
-
tasks.add(() => addParts(ctx, siteOptions, {
|
|
42
|
+
tasks.add(() => addParts(ctx, report, siteOptions, {
|
|
40
43
|
parentNodeId: routingIds.nodeId,
|
|
41
44
|
nodeType: partType,
|
|
42
45
|
documentType: nodeType,
|
|
@@ -48,24 +51,24 @@ function fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, nodeOptions)
|
|
|
48
51
|
const childIds = routingIds.children?.[typeName];
|
|
49
52
|
if (!childIds)
|
|
50
53
|
throw new Error(`Missing childIds for ${typeName}`);
|
|
51
|
-
fillRoutingDocumentAndAddChildren(ctx, tasks, siteOptions, {
|
|
54
|
+
fillRoutingDocumentAndAddChildren(ctx, tasks, report, siteOptions, {
|
|
52
55
|
routingIds: childIds,
|
|
53
56
|
nodeType: childType,
|
|
54
57
|
});
|
|
55
58
|
}
|
|
56
59
|
for (const typeName of nodeType.regularChildren ?? []) {
|
|
57
60
|
const childType = getRegularDocumentTypeByName(siteSchema, typeName);
|
|
58
|
-
tasks.add(() => addRegularDocuments(ctx, siteOptions, {
|
|
61
|
+
tasks.add(() => addRegularDocuments(ctx, report, siteOptions, {
|
|
59
62
|
parentNodeId: routingIds.nodeId,
|
|
60
63
|
nodeType: childType,
|
|
61
64
|
documentType: childType,
|
|
62
65
|
}));
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
|
-
async function updateSiteFields(ctx, options) {
|
|
68
|
+
async function updateSiteFields(ctx, _report, options) {
|
|
66
69
|
const { service, logger } = ctx;
|
|
67
70
|
logger.debug("Updating site fields…");
|
|
68
|
-
const { fqdn, siteSchema,
|
|
71
|
+
const { fqdn, siteSchema, siteTitle } = options;
|
|
69
72
|
const siteType = siteSchema.nodeTypes._site;
|
|
70
73
|
const logoImageFile = getRandomImagePath();
|
|
71
74
|
const fields = [
|
|
@@ -123,7 +126,7 @@ async function updateSiteFields(ctx, options) {
|
|
|
123
126
|
await service.connector.updateSiteFields(fqdn, Object.fromEntries(fields));
|
|
124
127
|
logger.debug("… Site fields updated");
|
|
125
128
|
}
|
|
126
|
-
async function updateRoutingDocument(ctx, siteOptions, nodeOptions) {
|
|
129
|
+
async function updateRoutingDocument(ctx, report, siteOptions, nodeOptions) {
|
|
127
130
|
ctx.logger.debug(`[TASK] Updating routing document "${nodeOptions.nodeType.typeName}"…`);
|
|
128
131
|
const { routingIds, nodeType } = nodeOptions;
|
|
129
132
|
const { fqdn, siteSchema, schemaI18n } = siteOptions;
|
|
@@ -139,8 +142,9 @@ async function updateRoutingDocument(ctx, siteOptions, nodeOptions) {
|
|
|
139
142
|
nodeId: routingIds.nodeId,
|
|
140
143
|
content: toRiDocumentContent(content, nodeType),
|
|
141
144
|
});
|
|
145
|
+
report.addContentCount(1);
|
|
142
146
|
}
|
|
143
|
-
async function addRegularDocuments(ctx, siteOptions, nodeOptions) {
|
|
147
|
+
async function addRegularDocuments(ctx, report, siteOptions, nodeOptions) {
|
|
144
148
|
ctx.logger.debug(`[TASK] Adding regular documents "${nodeOptions.nodeType.typeName}"…`);
|
|
145
149
|
const { parentNodeId, nodeType, documentType } = nodeOptions;
|
|
146
150
|
const { fqdn, siteSchema, schemaI18n } = siteOptions;
|
|
@@ -164,8 +168,9 @@ async function addRegularDocuments(ctx, siteOptions, nodeOptions) {
|
|
|
164
168
|
parentNodeId,
|
|
165
169
|
contents: list.map((content) => toRiDocumentContent(content, nodeType)),
|
|
166
170
|
});
|
|
171
|
+
report.addContentCount(list.length);
|
|
167
172
|
}
|
|
168
|
-
async function addParts(ctx, siteOptions, nodeOptions) {
|
|
173
|
+
async function addParts(ctx, report, siteOptions, nodeOptions) {
|
|
169
174
|
ctx.logger.debug(`[TASK] Adding parts "${nodeOptions.nodeType.typeName}"…`);
|
|
170
175
|
const { parentNodeId, nodeType, documentType } = nodeOptions;
|
|
171
176
|
const { fqdn, siteSchema, schemaI18n } = siteOptions;
|
|
@@ -189,6 +194,7 @@ async function addParts(ctx, siteOptions, nodeOptions) {
|
|
|
189
194
|
parentNodeId,
|
|
190
195
|
contents: list.map((content) => toRiPartContent(content, nodeType)),
|
|
191
196
|
});
|
|
197
|
+
report.addContentCount(list.length);
|
|
192
198
|
}
|
|
193
199
|
function toRiDocumentContent(content, nodeType) {
|
|
194
200
|
const { title, fields, featuredImage } = content;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boolVal, listValOrUndef, strVal, strValOrUndef, } from "@paroi/data-formatters-lib";
|
|
2
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
2
3
|
import { createPromptTemplate } from "../lib/create-prompt.js";
|
|
3
4
|
import { debugLlmOutput } from "../lib/debug-utils.js";
|
|
4
5
|
import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
|
|
@@ -35,6 +36,7 @@ export async function invokeMessageGuard(ctx, input, { skipOutOfScopeCheck = fal
|
|
|
35
36
|
}
|
|
36
37
|
if (report.valid)
|
|
37
38
|
return;
|
|
39
|
+
await updateSession(ctx, { guardCountInc: 1 });
|
|
38
40
|
return {
|
|
39
41
|
success: false,
|
|
40
42
|
language: report.language,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
1
2
|
import { reorderObjectKeys } from "../helpers/js-utils.js";
|
|
2
3
|
import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from "../lib/create-prompt.js";
|
|
3
4
|
import { debugLlmOutput } from "../lib/debug-utils.js";
|
|
@@ -24,6 +25,7 @@ export async function invokeNewSiteAnalysis(ctx, input) {
|
|
|
24
25
|
[analysis.siteProperties.language]: analysis.siteProperties.title,
|
|
25
26
|
};
|
|
26
27
|
if (!unusedInformation) {
|
|
28
|
+
await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
|
|
27
29
|
return {
|
|
28
30
|
siteTitle,
|
|
29
31
|
siteSchema,
|
|
@@ -40,7 +42,8 @@ export async function invokeNewSiteAnalysis(ctx, input) {
|
|
|
40
42
|
siteSchema,
|
|
41
43
|
l10n,
|
|
42
44
|
},
|
|
43
|
-
});
|
|
45
|
+
}, { asRemainingPrompt: true });
|
|
46
|
+
await updateSession(ctx, { status: "analyzed", promptCountInc: 1 });
|
|
44
47
|
return {
|
|
45
48
|
siteTitle,
|
|
46
49
|
siteSchema: updated.siteSchema,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
1
2
|
import { createPromptTemplate, getPredefinedFields, getSiteSchemaTsDefs, } from "../lib/create-prompt.js";
|
|
2
3
|
import { debugLlmOutput } from "../lib/debug-utils.js";
|
|
3
4
|
import { parseLlmResponseAsProperties } from "../lib/parse-llm-response.js";
|
|
@@ -7,7 +8,7 @@ const prompt1Tpl = await createPromptTemplate({
|
|
|
7
8
|
const prompt2Tpl = await createPromptTemplate({
|
|
8
9
|
fileName: "update-site-schema-2-execute.md",
|
|
9
10
|
});
|
|
10
|
-
export async function invokeUpdateSiteSchema(ctx, input) {
|
|
11
|
+
export async function invokeUpdateSiteSchema(ctx, input, { asRemainingPrompt = false } = {}) {
|
|
11
12
|
const task = await invokeUpdateSiteSchemaStep1(ctx, input);
|
|
12
13
|
if (!task.taskDetailsMd) {
|
|
13
14
|
// no changes
|
|
@@ -21,6 +22,9 @@ export async function invokeUpdateSiteSchema(ctx, input) {
|
|
|
21
22
|
taskDetailsMd: task.taskDetailsMd,
|
|
22
23
|
generatedSchema: input.generatedSchema,
|
|
23
24
|
});
|
|
25
|
+
if (!asRemainingPrompt) {
|
|
26
|
+
await updateSession(ctx, { status: "updated", promptCountInc: 1 });
|
|
27
|
+
}
|
|
24
28
|
return {
|
|
25
29
|
...genSchema,
|
|
26
30
|
changed: true,
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { generateSlug } from "@paroicms/public-anywhere-lib";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { updateSession } from "../../db/db.queries.js";
|
|
5
5
|
import { fillSiteWithFakeContent } from "../fake-content-generator.ts/create-database-with-fake-content.js";
|
|
6
6
|
import { createTheme } from "./theme-creator.js";
|
|
7
7
|
export async function generateSite(ctx, input) {
|
|
8
|
-
const { service, logger, packConf } = ctx;
|
|
9
|
-
const { sitesDir, packName } = packConf;
|
|
8
|
+
const { service, logger, sitesDir, packConf: { packName }, } = ctx;
|
|
10
9
|
const { generatedSchema: { l10n, siteSchema, siteTitle }, withFakeContent, } = input;
|
|
11
|
-
|
|
12
|
-
throw new Error(`Site-generator plugin can generate sites only for pack with "sitesDir", but pack "${packName}" doesn't have it`);
|
|
13
|
-
}
|
|
14
|
-
const siteId = randomUUID();
|
|
10
|
+
const siteId = ctx.sessionId;
|
|
15
11
|
logger.info(`Generating site: ${siteId}…`);
|
|
16
12
|
const siteDir = join(sitesDir, siteId);
|
|
17
13
|
await mkdir(siteDir);
|
|
@@ -23,14 +19,17 @@ export async function generateSite(ctx, input) {
|
|
|
23
19
|
siteTitle: siteTitle.en ?? Object.values(siteTitle)[0] ?? "new-website",
|
|
24
20
|
}), null, 2), "utf-8");
|
|
25
21
|
await createTheme(ctx, siteDir, siteSchema);
|
|
26
|
-
const
|
|
22
|
+
const regSite = await service.connector.registerNewSite({
|
|
27
23
|
packName,
|
|
28
24
|
siteDir,
|
|
29
25
|
domain: siteId,
|
|
30
26
|
version: "0.0.0",
|
|
31
27
|
});
|
|
32
28
|
if (withFakeContent) {
|
|
33
|
-
await fillSiteWithFakeContent(ctx, {
|
|
29
|
+
const report = await fillSiteWithFakeContent(ctx, { regSite, siteTitle });
|
|
30
|
+
await updateSession(ctx, {
|
|
31
|
+
contentCountInc: report.getContentCount(),
|
|
32
|
+
});
|
|
34
33
|
}
|
|
35
34
|
const account = {
|
|
36
35
|
kind: "local",
|
|
@@ -38,8 +37,12 @@ export async function generateSite(ctx, input) {
|
|
|
38
37
|
name: "Admin",
|
|
39
38
|
password: Math.random().toString(36).substring(2, 6), // 4 random lowercase characters,
|
|
40
39
|
};
|
|
41
|
-
await ctx.service.connector.createAccount(
|
|
42
|
-
const { siteUrl } =
|
|
40
|
+
await ctx.service.connector.createAccount(regSite.fqdn, account, { asContactEmail: true });
|
|
41
|
+
const { siteUrl } = regSite;
|
|
42
|
+
await updateSession(ctx, {
|
|
43
|
+
status: "generated",
|
|
44
|
+
nodeTypeCount: siteSchema.nodeTypes?.length ?? 0,
|
|
45
|
+
});
|
|
43
46
|
return {
|
|
44
47
|
siteId,
|
|
45
48
|
url: siteUrl,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
let runningId;
|
|
4
|
+
export function startSiteRemover(ctx) {
|
|
5
|
+
clearInterval(runningId);
|
|
6
|
+
let sitesDir;
|
|
7
|
+
runningId = setInterval(async () => {
|
|
8
|
+
if (!sitesDir)
|
|
9
|
+
return;
|
|
10
|
+
try {
|
|
11
|
+
await removeExpiredSites(ctx);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
ctx.logger.error("[site-remover]", error);
|
|
15
|
+
}
|
|
16
|
+
}, 1000 * 60 * 60).unref(); // Check every hour
|
|
17
|
+
return {
|
|
18
|
+
stop() {
|
|
19
|
+
clearInterval(runningId);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export async function removeExpiredSites(ctx) {
|
|
24
|
+
const { sitesDir, packConf, service } = ctx;
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const expirationTime = new Date(now.getTime() - 1000 * 60 * 60 * 24 * 2); // 2 days
|
|
27
|
+
// List entries in sitesDir, filter directories named as uuidv4, and remove them.
|
|
28
|
+
const uuidv4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
29
|
+
const entries = await readdir(sitesDir, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
if (!entry.isDirectory() || !uuidv4Regex.test(entry.name))
|
|
32
|
+
continue;
|
|
33
|
+
const dirPath = join(sitesDir, entry.name);
|
|
34
|
+
const st = await stat(dirPath);
|
|
35
|
+
if (st.ctime < expirationTime) {
|
|
36
|
+
await service.connector.removeSite(`${entry.name}.${packConf.parentDomain}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
2
2
|
import { ChatMistralAI } from "@langchain/mistralai";
|
|
3
|
-
import {
|
|
4
|
-
import { pathExists } from "@paroicms/internal-server-lib";
|
|
3
|
+
import { getJwtSecretSync, pathExists } from "@paroicms/internal-server-lib";
|
|
5
4
|
import { ApiError, escapeHtml } from "@paroicms/public-server-lib";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { executeCommand } from "./commands/execute-command.js";
|
|
7
|
+
import { SLUG, packageDir, pluginVersion } from "./context.js";
|
|
9
8
|
import { formatGeneratorCommand, formatGeneratorPluginConfiguration } from "./data-format.js";
|
|
10
|
-
import {
|
|
9
|
+
import { createOrOpenSiteGeneratorConnection } from "./db/db-init.js";
|
|
11
10
|
import { initializeImageNames } from "./generator/lib/images-lib.js";
|
|
12
|
-
|
|
13
|
-
const version = strVal(JSON.parse(readFileSync(join(packageDir, "package.json"), "utf-8")).version);
|
|
14
|
-
const SLUG = "site-generator";
|
|
11
|
+
import { removeExpiredSites } from "./lib/site-remover.js";
|
|
15
12
|
await initializeImageNames();
|
|
16
13
|
const plugin = {
|
|
17
|
-
version,
|
|
14
|
+
version: pluginVersion,
|
|
18
15
|
slug: SLUG,
|
|
19
16
|
async siteInit(service) {
|
|
17
|
+
const { cn, logNextQuery } = await createOrOpenSiteGeneratorConnection({
|
|
18
|
+
sqliteFile: join(service.registeredSite.dataDir, "site-generator.sqlite"),
|
|
19
|
+
canCreate: true,
|
|
20
|
+
logger: service.logger,
|
|
21
|
+
});
|
|
20
22
|
const pluginConf = formatGeneratorPluginConfiguration(service.configuration);
|
|
21
23
|
let debugDir = pluginConf.debugDir;
|
|
22
24
|
if (debugDir) {
|
|
@@ -55,6 +57,32 @@ const plugin = {
|
|
|
55
57
|
temperature: 0.2,
|
|
56
58
|
maxTokens: 50_000,
|
|
57
59
|
});
|
|
60
|
+
let rawContext;
|
|
61
|
+
service.registerHook("initialized", (service) => {
|
|
62
|
+
const packConf = service.connector.getSitePackConf(pluginConf.packName);
|
|
63
|
+
const { sitesDir, packName } = packConf;
|
|
64
|
+
if (!sitesDir || packConf.serveOn !== "subDomain") {
|
|
65
|
+
throw new Error(`Site-generator plugin can generate sites only for sub-domain pack with "sitesDir", but pack "${packName}" doesn't have it`);
|
|
66
|
+
}
|
|
67
|
+
rawContext = {
|
|
68
|
+
cn,
|
|
69
|
+
logNextQuery,
|
|
70
|
+
jwtSecret: getJwtSecretSync(join(service.registeredSite.dataDir, "site-generator-secret.txt")),
|
|
71
|
+
pluginConf,
|
|
72
|
+
bestModel,
|
|
73
|
+
bestModelName: bestModel.model,
|
|
74
|
+
goodModel,
|
|
75
|
+
goodModelName: goodModel.model,
|
|
76
|
+
cheapModel,
|
|
77
|
+
cheapModelName: cheapModel.model,
|
|
78
|
+
debugDir,
|
|
79
|
+
sitesDir,
|
|
80
|
+
packConf,
|
|
81
|
+
service,
|
|
82
|
+
logger: service.logger,
|
|
83
|
+
};
|
|
84
|
+
removeExpiredSites(rawContext).catch(console.error);
|
|
85
|
+
});
|
|
58
86
|
service.setPublicAssetsDirectory(join(packageDir, "gen-front", "dist"));
|
|
59
87
|
const scriptAttr = [
|
|
60
88
|
["type", "module"],
|
|
@@ -64,24 +92,14 @@ const plugin = {
|
|
|
64
92
|
];
|
|
65
93
|
service.addHeadTag(`<link rel="stylesheet" href="${escapeHtml(`${service.pluginAssetsUrl}/gen-front.css`)}">`, `<script ${scriptAttr.map(([key, val]) => `${key}="${escapeHtml(val)}"`).join(" ")}></script>`);
|
|
66
94
|
service.setPublicApiHandler(async (service, httpContext, relativePath) => {
|
|
95
|
+
if (!rawContext)
|
|
96
|
+
throw new Error("should be initialized");
|
|
97
|
+
const ctx = rawContext;
|
|
67
98
|
const { req, res } = httpContext;
|
|
68
99
|
if (relativePath !== "") {
|
|
69
100
|
res.status(404).send({ status: 404 });
|
|
70
101
|
return;
|
|
71
102
|
}
|
|
72
|
-
const ctx = {
|
|
73
|
-
pluginConf,
|
|
74
|
-
packConf: service.connector.getSitePackConf(pluginConf.packName),
|
|
75
|
-
service,
|
|
76
|
-
logger: service.logger,
|
|
77
|
-
bestModel,
|
|
78
|
-
bestModelName: bestModel.model,
|
|
79
|
-
goodModel,
|
|
80
|
-
goodModelName: goodModel.model,
|
|
81
|
-
cheapModel,
|
|
82
|
-
cheapModelName: cheapModel.model,
|
|
83
|
-
debugDir,
|
|
84
|
-
};
|
|
85
103
|
let command;
|
|
86
104
|
try {
|
|
87
105
|
command = formatGeneratorCommand(req.body);
|