@paroicms/site-generator-plugin 0.9.0 → 0.10.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 +57 -9
- package/gen-backend/dist/commands/execute-command.js +35 -9
- package/gen-backend/dist/commands/generator-session.js +49 -10
- package/gen-backend/dist/data-format.js +32 -4
- package/gen-backend/dist/db/db-init.js +3 -1
- package/gen-backend/dist/db/db-read.queries.js +142 -0
- package/gen-backend/dist/db/db-write.queries.js +144 -0
- package/gen-backend/dist/db/ddl-migration.js +8 -6
- package/gen-backend/dist/db/formatters.js +46 -0
- package/gen-backend/dist/generator/fake-content-generator.ts/content-report.js +9 -5
- package/gen-backend/dist/generator/fake-content-generator.ts/create-database-with-fake-content.js +18 -13
- package/gen-backend/dist/generator/fake-content-generator.ts/generate-fake-content.js +16 -12
- package/gen-backend/dist/generator/fake-content-generator.ts/invoke-generate-fake-content.js +26 -17
- package/gen-backend/dist/generator/lib/calling-llm-anthropic.js +33 -0
- package/gen-backend/dist/generator/lib/calling-llm-mistral.js +156 -0
- package/gen-backend/dist/generator/lib/create-prompt.js +2 -2
- package/gen-backend/dist/generator/lib/debug-utils.js +74 -48
- package/gen-backend/dist/generator/lib/llm-tokens.js +7 -9
- package/gen-backend/dist/generator/lib/llm-utils.js +8 -0
- package/gen-backend/dist/generator/lib/prompt-template.js +10 -0
- package/gen-backend/dist/generator/lib/session-utils.js +31 -0
- package/gen-backend/dist/generator/llm-queries/invoke-message-guard.js +20 -9
- package/gen-backend/dist/generator/llm-queries/invoke-new-site-analysis.js +73 -47
- package/gen-backend/dist/generator/llm-queries/invoke-update-site-schema.js +106 -43
- package/gen-backend/dist/generator/site-generator/site-generator.js +26 -18
- package/gen-backend/dist/lib/create-raw-context.js +31 -0
- package/gen-backend/dist/lib/site-remover.js +1 -1
- package/gen-backend/dist/plugin.js +8 -54
- package/gen-backend/prompts/generate-fake-content-multiple-documents.md +5 -5
- package/gen-backend/prompts/generate-fake-content-multiple-parts.md +5 -5
- package/gen-backend/prompts/generate-fake-content-single.md +4 -4
- package/gen-backend/prompts/{new-site-1-analysis.md → initial-1-analysis.md} +38 -29
- package/gen-backend/prompts/{new-site-2-fields.md → initial-2-fields.md} +3 -3
- package/gen-backend/prompts/message-guard.md +1 -1
- package/gen-backend/prompts/update-site-schema-1-write-details.md +5 -5
- package/gen-backend/prompts/update-site-schema-2-execute.md +29 -29
- package/gen-front/dist/gen-front.css +1 -1
- package/gen-front/dist/gen-front.mjs +137 -1175
- package/package.json +30 -32
- package/gen-backend/dist/commands/actions.js +0 -49
- package/gen-backend/dist/db/db.queries.js +0 -60
- package/gen-backend/dist/errors.js +0 -20
- package/gen-backend/dist/generator/actions.js +0 -45
- package/gen-backend/dist/generator/fake-content-generator.ts/augment-fields.js +0 -51
- package/gen-backend/dist/generator/generator-session.js +0 -33
- package/gen-backend/dist/generator/generator-types.js +0 -1
- package/gen-backend/dist/generator/lib/token-tracking.js +0 -118
- package/gen-backend/dist/generator/session/generator-session.js +0 -33
- package/gen-backend/dist/generator/session/session-command.js +0 -17
- package/gen-backend/dist/generator/site-generator/theme-scss.js +0 -262
- package/gen-backend/dist/lib/generator-context.js +0 -14
- package/gen-backend/prompts/test-message1.txt +0 -1
- package/gen-front/dist/gen-front.eot +0 -0
- package/gen-front/dist/gen-front.svg +0 -345
- package/gen-front/dist/gen-front.ttf +0 -0
- package/gen-front/dist/gen-front.woff +0 -0
- package/gen-front/dist/gen-front.woff2 +0 -0
- package/gen-front/dist/gen-front2.woff2 +0 -0
- package/gen-front/dist/gen-front3.woff2 +0 -0
|
@@ -8,15 +8,63 @@ create table PaMetadata (
|
|
|
8
8
|
primary key (dbSchema, k)
|
|
9
9
|
);
|
|
10
10
|
|
|
11
|
-
insert into PaMetadata (dbSchema, k, val) values ('site-generator', 'dbSchemaVersion', '
|
|
11
|
+
insert into PaMetadata (dbSchema, k, val) values ('site-generator', 'dbSchemaVersion', '2');
|
|
12
12
|
|
|
13
|
-
create table
|
|
14
|
-
id
|
|
13
|
+
create table PgSession (
|
|
14
|
+
id varchar(100) not null primary key,
|
|
15
15
|
createdAt timestamp not null default current_timestamp,
|
|
16
|
+
expireAt timestamp not null,
|
|
17
|
+
language varchar(5) not null
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
create table PgStep (
|
|
21
|
+
stepNumber integer not null,
|
|
22
|
+
sessionId varchar(100) not null references PgSession(id),
|
|
23
|
+
startedAt timestamp not null default current_timestamp,
|
|
24
|
+
kind varchar(100) not null,
|
|
16
25
|
status varchar(100) not null,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
durationMs integer,
|
|
27
|
+
currentActivity varchar(100),
|
|
28
|
+
explanation text,
|
|
29
|
+
primary key (stepNumber, sessionId)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
create table PgSchemaStep (
|
|
33
|
+
stepNumber integer not null,
|
|
34
|
+
sessionId varchar(100) not null,
|
|
35
|
+
siteSchema text not null,
|
|
36
|
+
l10n text not null,
|
|
37
|
+
localizedValues text not null,
|
|
38
|
+
nodeTypeCount integer not null,
|
|
39
|
+
inputTokenCount integer not null,
|
|
40
|
+
outputTokenCount integer not null,
|
|
41
|
+
promptTitle varchar(200),
|
|
42
|
+
primary key (stepNumber, sessionId),
|
|
43
|
+
foreign key (stepNumber, sessionId) references PgStep(stepNumber, sessionId)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
create table PgGeneratedSiteStep (
|
|
47
|
+
siteId varchar(100) not null primary key,
|
|
48
|
+
stepNumber integer not null,
|
|
49
|
+
sessionId varchar(100) not null,
|
|
50
|
+
siteUrl varchar(200) not null,
|
|
51
|
+
loginEmail varchar(100) not null,
|
|
52
|
+
loginPassword varchar(100) not null,
|
|
53
|
+
localizedValues text not null,
|
|
54
|
+
contentEntryCount integer,
|
|
55
|
+
contentInputTokenCount integer,
|
|
56
|
+
contentOutputTokenCount integer,
|
|
57
|
+
contentErrors text,
|
|
58
|
+
unique (stepNumber, sessionId),
|
|
59
|
+
foreign key (stepNumber, sessionId) references PgStep(stepNumber, sessionId)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
create table PgIssueEvent(
|
|
63
|
+
id integer not null primary key autoincrement,
|
|
64
|
+
sessionId varchar(100) not null references PgSession(id),
|
|
65
|
+
createdAt timestamp not null default current_timestamp,
|
|
66
|
+
eventType varchar(100) not null,
|
|
67
|
+
issueMessage text,
|
|
68
|
+
llmTaskName varchar(100),
|
|
69
|
+
stepNumber integer
|
|
70
|
+
);
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { messageOf } from "@paroi/data-formatters-lib";
|
|
2
|
-
import {
|
|
2
|
+
import { fetchWorkSession, loadStep } from "../db/db-read.queries.js";
|
|
3
|
+
import { insertIssueEvent, updateSession } from "../db/db-write.queries.js";
|
|
3
4
|
import { invokeMessageGuard } from "../generator/llm-queries/invoke-message-guard.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { startInitialAnalysis } from "../generator/llm-queries/invoke-new-site-analysis.js";
|
|
6
|
+
import { startUpdateSiteSchema } from "../generator/llm-queries/invoke-update-site-schema.js";
|
|
6
7
|
import { generateSite } from "../generator/site-generator/site-generator.js";
|
|
7
|
-
import {
|
|
8
|
+
import { newSessionCommand, renewSessionCommand, verifySessionToken } from "./generator-session.js";
|
|
8
9
|
export async function executeCommand(commandCtx, input) {
|
|
9
10
|
if (input.command === "newSession") {
|
|
10
11
|
return {
|
|
11
12
|
success: true,
|
|
12
|
-
result: await
|
|
13
|
+
result: await newSessionCommand(commandCtx, input),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (input.command === "renewSession") {
|
|
17
|
+
return {
|
|
18
|
+
success: true,
|
|
19
|
+
result: await renewSessionCommand(commandCtx, input),
|
|
13
20
|
};
|
|
14
21
|
}
|
|
15
22
|
const { sessionId } = await verifySessionToken(commandCtx, input.sessionToken);
|
|
@@ -18,13 +25,32 @@ export async function executeCommand(commandCtx, input) {
|
|
|
18
25
|
sessionId,
|
|
19
26
|
};
|
|
20
27
|
try {
|
|
28
|
+
if (input.command === "setLanguage") {
|
|
29
|
+
await updateSession(ctx, { language: input.language });
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
result: { updated: true },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (input.command === "loadWorkSession") {
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
result: await fetchWorkSession(ctx),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (input.command === "loadStep") {
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
result: await loadStep(ctx, input.stepNumber),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
21
47
|
if (input.command === "createSiteSchema") {
|
|
22
48
|
const errorResponse = await invokeMessageGuard(ctx, input);
|
|
23
49
|
if (errorResponse)
|
|
24
50
|
return errorResponse;
|
|
25
51
|
return {
|
|
26
52
|
success: true,
|
|
27
|
-
result: await
|
|
53
|
+
result: await startInitialAnalysis(ctx, input),
|
|
28
54
|
};
|
|
29
55
|
}
|
|
30
56
|
if (input.command === "updateSiteSchema") {
|
|
@@ -33,7 +59,7 @@ export async function executeCommand(commandCtx, input) {
|
|
|
33
59
|
return errorResponse;
|
|
34
60
|
return {
|
|
35
61
|
success: true,
|
|
36
|
-
result: await
|
|
62
|
+
result: await startUpdateSiteSchema(ctx, input),
|
|
37
63
|
};
|
|
38
64
|
}
|
|
39
65
|
if (input.command === "generateSite") {
|
|
@@ -50,10 +76,10 @@ export async function executeCommand(commandCtx, input) {
|
|
|
50
76
|
}
|
|
51
77
|
catch (error) {
|
|
52
78
|
try {
|
|
53
|
-
await
|
|
79
|
+
await insertIssueEvent(ctx, { eventType: "unexpectedError", issueMessage: messageOf(error) });
|
|
54
80
|
}
|
|
55
81
|
catch {
|
|
56
|
-
ctx.logger.error("Failed to
|
|
82
|
+
ctx.logger.error("Failed to insert issue event", error);
|
|
57
83
|
}
|
|
58
84
|
throw error;
|
|
59
85
|
}
|
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
import { ApiError } from "@paroicms/public-server-lib";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { getInvalidSessionError } from "../db/db-read.queries.js";
|
|
4
|
+
import { insertSession, updateSession } from "../db/db-write.queries.js";
|
|
4
5
|
const { sign, verify } = (await import("jsonwebtoken")).default;
|
|
5
|
-
export async function
|
|
6
|
+
export async function newSessionCommand(ctx, command) {
|
|
7
|
+
const { service } = ctx;
|
|
8
|
+
const { recaptchaToken, language } = command;
|
|
9
|
+
const isValid = await service.validateRecaptchaResponse(recaptchaToken);
|
|
10
|
+
if (!isValid)
|
|
11
|
+
throw new ApiError("Invalid reCAPTCHA token", 400);
|
|
12
|
+
const date = new Date();
|
|
13
|
+
const compressedTs = `${date.toISOString().replace(/[-:]/g, "").split(".")[0]}Z`;
|
|
14
|
+
const sessionId = `${compressedTs}-${nanoid()}`;
|
|
15
|
+
const token = sign({ sessionId }, ctx.jwtSecret, { expiresIn: "48h" });
|
|
16
|
+
const expireAt = new Date(date.getTime() + 48 * 60 * 60 * 1000);
|
|
17
|
+
await insertSession(ctx, { sessionId, language, expireAt });
|
|
18
|
+
return { token };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Renews a session token and extends its expiration time
|
|
22
|
+
*/
|
|
23
|
+
export async function renewSessionCommand(ctx, command) {
|
|
6
24
|
const { service } = ctx;
|
|
7
25
|
const { recaptchaToken } = command;
|
|
8
26
|
const isValid = await service.validateRecaptchaResponse(recaptchaToken);
|
|
9
27
|
if (!isValid)
|
|
10
28
|
throw new ApiError("Invalid reCAPTCHA token", 400);
|
|
11
|
-
|
|
29
|
+
let payload;
|
|
30
|
+
try {
|
|
31
|
+
// When renewing, we verify the token but allow expired tokens
|
|
32
|
+
payload = verify(command.sessionToken, ctx.jwtSecret, { ignoreExpiration: true });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.name === "JsonWebTokenError")
|
|
36
|
+
throw new ApiError("Invalid session token", 401);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
if (!payload || !("sessionId" in payload) || typeof payload.sessionId !== "string") {
|
|
40
|
+
throw new ApiError("Invalid session token", 401);
|
|
41
|
+
}
|
|
42
|
+
const sessionId = payload.sessionId;
|
|
43
|
+
// Check if the session is valid (not deleted, not too many steps)
|
|
44
|
+
const errorMessage = await getInvalidSessionError(ctx, sessionId);
|
|
45
|
+
if (errorMessage && errorMessage !== "expired") {
|
|
46
|
+
throw new ApiError(`Invalid session "${sessionId}": ${errorMessage}`, 401);
|
|
47
|
+
}
|
|
48
|
+
// Generate new expiration time - 48 hours from now
|
|
49
|
+
const expireAt = new Date(Date.now() + 48 * 60 * 60 * 1000);
|
|
50
|
+
// Update the session's expiration time
|
|
51
|
+
await updateSession({ ...ctx, sessionId }, { expireAt });
|
|
52
|
+
// Generate a new token
|
|
12
53
|
const token = sign({ sessionId }, ctx.jwtSecret, { expiresIn: "48h" });
|
|
13
|
-
await insertSession(ctx, { sessionId });
|
|
14
54
|
return { token };
|
|
15
55
|
}
|
|
16
56
|
/**
|
|
@@ -32,10 +72,9 @@ export async function verifySessionToken(ctx, token) {
|
|
|
32
72
|
throw new ApiError("Invalid session token", 401);
|
|
33
73
|
}
|
|
34
74
|
const sessionId = payload.sessionId;
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
throw new ApiError(`
|
|
38
|
-
|
|
39
|
-
throw new ApiError("Session consumed", 401);
|
|
75
|
+
const errorMessage = await getInvalidSessionError(ctx, sessionId);
|
|
76
|
+
if (errorMessage) {
|
|
77
|
+
throw new ApiError(`Invalid session "${sessionId}": ${errorMessage}`, 401);
|
|
78
|
+
}
|
|
40
79
|
return { sessionId };
|
|
41
80
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { boolVal, isObj, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
|
|
1
|
+
import { boolVal, isObj, nbVal, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
|
|
2
2
|
import { ApiError } from "@paroicms/public-server-lib";
|
|
3
3
|
export function formatGeneratorPluginConfiguration(pluginConf) {
|
|
4
4
|
const anthropicApiKey = strValOrUndef(pluginConf.anthropicApiKey, { varName: "anthropicApiKey" });
|
|
@@ -17,9 +17,37 @@ export function formatGeneratorCommand(data) {
|
|
|
17
17
|
if (command === "newSession") {
|
|
18
18
|
return {
|
|
19
19
|
command: "newSession",
|
|
20
|
+
language: strVal(data.language, { varName: "language" }),
|
|
20
21
|
recaptchaToken: strVal(data.recaptchaToken, { varName: "recaptchaToken" }),
|
|
21
22
|
};
|
|
22
23
|
}
|
|
24
|
+
if (command === "renewSession") {
|
|
25
|
+
return {
|
|
26
|
+
command: "renewSession",
|
|
27
|
+
sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
|
|
28
|
+
recaptchaToken: strVal(data.recaptchaToken, { varName: "recaptchaToken" }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (command === "setLanguage") {
|
|
32
|
+
return {
|
|
33
|
+
command: "setLanguage",
|
|
34
|
+
sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
|
|
35
|
+
language: strVal(data.language, { varName: "language" }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (command === "loadWorkSession") {
|
|
39
|
+
return {
|
|
40
|
+
command: "loadWorkSession",
|
|
41
|
+
sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (command === "loadStep") {
|
|
45
|
+
return {
|
|
46
|
+
command: "loadStep",
|
|
47
|
+
sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
|
|
48
|
+
stepNumber: nbVal(data.stepNumber, { varName: "stepNumber" }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
23
51
|
if (command === "createSiteSchema") {
|
|
24
52
|
return {
|
|
25
53
|
command: "createSiteSchema",
|
|
@@ -32,15 +60,15 @@ export function formatGeneratorCommand(data) {
|
|
|
32
60
|
command: "updateSiteSchema",
|
|
33
61
|
sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
|
|
34
62
|
prompt: formatPrompt(data.prompt),
|
|
35
|
-
|
|
63
|
+
fromStepNumber: nbVal(data.fromStepNumber),
|
|
36
64
|
};
|
|
37
65
|
}
|
|
38
66
|
if (command === "generateSite") {
|
|
39
67
|
return {
|
|
40
68
|
command: "generateSite",
|
|
41
69
|
sessionToken: strVal(data.sessionToken, { varName: "sessionToken" }),
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
fromStepNumber: nbVal(data.fromStepNumber),
|
|
71
|
+
withSampleData: boolVal(data.withSampleData, { varName: "withSampleData" }),
|
|
44
72
|
};
|
|
45
73
|
}
|
|
46
74
|
throw new ApiError(`Invalid command: ${command}`, 400);
|
|
@@ -4,11 +4,12 @@ import { projectDir } from "../context.js";
|
|
|
4
4
|
import { currentDbSchemaVersion, dbSchemaName, migrateSiteGeneratorDb } from "./ddl-migration.js";
|
|
5
5
|
export async function createOrOpenSiteGeneratorConnection({ sqliteFile, canCreate, logger, }) {
|
|
6
6
|
const { logNextQuery, knexLogger } = createSqlLogger({ logger, dbSchemaName });
|
|
7
|
+
const ddlFile = join(projectDir, "ddl", "site-generator.ddl.sql");
|
|
7
8
|
const { cn } = await createOrOpenSqliteConnection({
|
|
8
9
|
canCreate,
|
|
9
10
|
dbSchemaName,
|
|
10
11
|
sqliteFile,
|
|
11
|
-
ddlFile
|
|
12
|
+
ddlFile,
|
|
12
13
|
migrateDb,
|
|
13
14
|
knexLogger,
|
|
14
15
|
logger,
|
|
@@ -24,6 +25,7 @@ export async function createOrOpenSiteGeneratorConnection({ sqliteFile, canCreat
|
|
|
24
25
|
await migrateSiteGeneratorDb(cn, {
|
|
25
26
|
fromVersion: dbVersion,
|
|
26
27
|
logger,
|
|
28
|
+
ddlFile,
|
|
27
29
|
});
|
|
28
30
|
return {
|
|
29
31
|
migrated: true,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { dateVal, nbVal, strVal, strValOrUndef } from "@paroi/data-formatters-lib";
|
|
2
|
+
import { ensureType, formatActivityCode, formatStepKind, formatStepStatus } from "./formatters.js";
|
|
3
|
+
export async function getInvalidSessionError(ctx, sessionId) {
|
|
4
|
+
const { cn } = ctx;
|
|
5
|
+
const row = await cn("PgSession as s")
|
|
6
|
+
.leftJoin("PgSchemaStep as u", "u.sessionId", "s.id")
|
|
7
|
+
.select("s.id", "s.expireAt", cn.raw("count(u.sessionId) as successStepCount"))
|
|
8
|
+
.where({ "s.id": sessionId })
|
|
9
|
+
.groupBy("s.id")
|
|
10
|
+
.first();
|
|
11
|
+
if (!row)
|
|
12
|
+
return "not found";
|
|
13
|
+
const successStepCount = nbVal(row.successStepCount);
|
|
14
|
+
if (successStepCount >= 20)
|
|
15
|
+
return "too many steps";
|
|
16
|
+
const expireAt = dateVal(row.expireAt).getTime();
|
|
17
|
+
if (expireAt < Date.now())
|
|
18
|
+
return "expired";
|
|
19
|
+
}
|
|
20
|
+
export async function readStepSchema(ctx, stepNumber) {
|
|
21
|
+
const { cn, sessionId } = ctx;
|
|
22
|
+
const row = await cn("PgSchemaStep")
|
|
23
|
+
.select("siteSchema", "l10n", "localizedValues")
|
|
24
|
+
.where({ sessionId, stepNumber })
|
|
25
|
+
.first();
|
|
26
|
+
if (!row)
|
|
27
|
+
throw new Error(`[${sessionId}] Step "${stepNumber}" not found`);
|
|
28
|
+
return {
|
|
29
|
+
siteSchema: JSON.parse(strVal(row.siteSchema)),
|
|
30
|
+
l10n: JSON.parse(strVal(row.l10n)),
|
|
31
|
+
localizedValues: JSON.parse(strVal(row.localizedValues)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function fetchWorkSession(ctx) {
|
|
35
|
+
const { cn, sessionId } = ctx;
|
|
36
|
+
const row = await cn("PgSession")
|
|
37
|
+
.select("id", "createdAt", "expireAt", "language")
|
|
38
|
+
.where({ id: sessionId })
|
|
39
|
+
.first();
|
|
40
|
+
if (!row) {
|
|
41
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
42
|
+
}
|
|
43
|
+
const steps = await loadSessionSteps(ctx);
|
|
44
|
+
return {
|
|
45
|
+
id: strVal(row.id),
|
|
46
|
+
createdAt: dateVal(row.createdAt).toISOString(),
|
|
47
|
+
expireAt: dateVal(row.expireAt).toISOString(),
|
|
48
|
+
language: strVal(row.language),
|
|
49
|
+
steps,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function loadSessionSteps(ctx) {
|
|
53
|
+
const { cn, sessionId } = ctx;
|
|
54
|
+
const rows = await buildStepsQuery(cn)
|
|
55
|
+
.where({ "s.sessionId": sessionId })
|
|
56
|
+
.orderBy("s.stepNumber");
|
|
57
|
+
return rows.map((row) => mapRowToStep(row, sessionId));
|
|
58
|
+
}
|
|
59
|
+
export async function loadStep(ctx, stepNumber) {
|
|
60
|
+
const { cn, sessionId } = ctx;
|
|
61
|
+
const row = await buildStepsQuery(cn)
|
|
62
|
+
.where({ "s.sessionId": sessionId, "s.stepNumber": stepNumber })
|
|
63
|
+
.first();
|
|
64
|
+
if (!row) {
|
|
65
|
+
throw new Error(`[Session "${sessionId}"] Step "${stepNumber}" not found`);
|
|
66
|
+
}
|
|
67
|
+
return mapRowToStep(row, sessionId);
|
|
68
|
+
}
|
|
69
|
+
function buildStepsQuery(cn) {
|
|
70
|
+
return cn("PgStep as s")
|
|
71
|
+
.select("s.stepNumber", "s.kind", "s.status", "s.currentActivity", "s.explanation", "c.promptTitle", "c.siteSchema", "c.l10n", "c.localizedValues as schemaLocalizedValues", "i.siteId", "i.localizedValues as siteLocalizedValues", "i.siteUrl", "i.loginEmail", "i.loginPassword")
|
|
72
|
+
.leftJoin("PgSchemaStep as c", {
|
|
73
|
+
"c.stepNumber": "s.stepNumber",
|
|
74
|
+
"c.sessionId": "s.sessionId",
|
|
75
|
+
})
|
|
76
|
+
.leftJoin("PgGeneratedSiteStep as i", {
|
|
77
|
+
"i.stepNumber": "s.stepNumber",
|
|
78
|
+
"i.sessionId": "s.sessionId",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function mapRowToStep(row, sessionId) {
|
|
82
|
+
const baseStep = {
|
|
83
|
+
stepNumber: nbVal(row.stepNumber),
|
|
84
|
+
explanation: strValOrUndef(row.explanation),
|
|
85
|
+
};
|
|
86
|
+
const status = formatStepStatus(row.status);
|
|
87
|
+
const kind = formatStepKind(row.kind);
|
|
88
|
+
if (status === "pending") {
|
|
89
|
+
return ensureType({
|
|
90
|
+
...baseStep,
|
|
91
|
+
status,
|
|
92
|
+
kind,
|
|
93
|
+
currentActivity: formatActivityCode(row.currentActivity),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (status === "completed") {
|
|
97
|
+
if (kind === "initialSchema" || kind === "updateSchema") {
|
|
98
|
+
if (!row.siteSchema) {
|
|
99
|
+
throw new Error(`[Session "${sessionId}"] PgSchemaStep row is missing for completed step "${row.stepNumber}"`);
|
|
100
|
+
}
|
|
101
|
+
return ensureType({
|
|
102
|
+
...baseStep,
|
|
103
|
+
status,
|
|
104
|
+
kind,
|
|
105
|
+
promptTitle: strValOrUndef(row.promptTitle),
|
|
106
|
+
siteSchema: JSON.parse(strVal(row.siteSchema)),
|
|
107
|
+
l10n: JSON.parse(strVal(row.l10n)),
|
|
108
|
+
localizedValues: JSON.parse(strVal(row.schemaLocalizedValues)),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (kind === "generateSite") {
|
|
112
|
+
if (!row.siteId) {
|
|
113
|
+
throw new Error(`[Session "${sessionId}"] PgGeneratedSiteStep row is missing for completed step "${row.stepNumber}"`);
|
|
114
|
+
}
|
|
115
|
+
const siteUrl = strVal(row.siteUrl);
|
|
116
|
+
return ensureType({
|
|
117
|
+
...baseStep,
|
|
118
|
+
status,
|
|
119
|
+
kind,
|
|
120
|
+
siteId: strVal(row.siteId),
|
|
121
|
+
siteUrl,
|
|
122
|
+
backOfficeUrl: `${siteUrl}/adm`,
|
|
123
|
+
loginEmail: strVal(row.loginEmail),
|
|
124
|
+
loginPassword: strVal(row.loginPassword),
|
|
125
|
+
localizedValues: JSON.parse(strVal(row.siteLocalizedValues)),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`[Session "${sessionId}"] invalid step kind "${kind}"`);
|
|
129
|
+
}
|
|
130
|
+
if (status === "failed") {
|
|
131
|
+
return ensureType({
|
|
132
|
+
...baseStep,
|
|
133
|
+
status,
|
|
134
|
+
kind,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return ensureType({
|
|
138
|
+
...baseStep,
|
|
139
|
+
status,
|
|
140
|
+
kind,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { strVal } from "@paroi/data-formatters-lib";
|
|
2
|
+
import { sqliteDateTime } from "@paroicms/internal-server-lib";
|
|
3
|
+
export async function insertSession(ctx, values) {
|
|
4
|
+
const { cn } = ctx;
|
|
5
|
+
await cn("PgSession").insert({
|
|
6
|
+
id: values.sessionId,
|
|
7
|
+
language: values.language,
|
|
8
|
+
expireAt: sqliteDateTime(values.expireAt),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export async function updateSession(ctx, values) {
|
|
12
|
+
const { cn, sessionId } = ctx;
|
|
13
|
+
if (Object.keys(values).length === 0)
|
|
14
|
+
return;
|
|
15
|
+
await cn("PgSession")
|
|
16
|
+
.where({ id: sessionId })
|
|
17
|
+
.update({
|
|
18
|
+
language: values.language,
|
|
19
|
+
expireAt: values.expireAt ? sqliteDateTime(values.expireAt) : undefined,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function insertStep(ctx, values) {
|
|
23
|
+
const { cn, sessionId } = ctx;
|
|
24
|
+
// Find the highest step number for this session
|
|
25
|
+
const result = await cn("PgStep").where({ sessionId }).max("stepNumber as maxStep").first();
|
|
26
|
+
const stepNumber = (result?.maxStep || 0) + 1;
|
|
27
|
+
const startedAt = new Date();
|
|
28
|
+
await cn("PgStep").insert({
|
|
29
|
+
stepNumber,
|
|
30
|
+
sessionId,
|
|
31
|
+
startedAt: sqliteDateTime(startedAt),
|
|
32
|
+
kind: values.kind,
|
|
33
|
+
status: values.status,
|
|
34
|
+
currentActivity: values.currentActivity,
|
|
35
|
+
explanation: values.explanation,
|
|
36
|
+
});
|
|
37
|
+
return { stepNumber, startedAt };
|
|
38
|
+
}
|
|
39
|
+
export async function updateStep(ctx, stepHandle, values) {
|
|
40
|
+
const { cn, sessionId } = ctx;
|
|
41
|
+
const { stepNumber, startedAt } = stepHandle;
|
|
42
|
+
const updateValues = {};
|
|
43
|
+
if (values.status) {
|
|
44
|
+
updateValues.status = values.status;
|
|
45
|
+
if (values.status !== "pending") {
|
|
46
|
+
updateValues.durationMs = Date.now() - startedAt.getTime();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (values.currentActivity !== undefined) {
|
|
50
|
+
updateValues.currentActivity = values.currentActivity;
|
|
51
|
+
}
|
|
52
|
+
if (values.explanation !== undefined)
|
|
53
|
+
updateValues.explanation = values.explanation;
|
|
54
|
+
if (Object.keys(updateValues).length === 0)
|
|
55
|
+
return;
|
|
56
|
+
await cn("PgStep").where({ sessionId, stepNumber }).update(updateValues);
|
|
57
|
+
}
|
|
58
|
+
export async function saveCompletedSchemaStep(ctx, stepHandle, values) {
|
|
59
|
+
const { cn, sessionId } = ctx;
|
|
60
|
+
const { stepNumber, startedAt } = stepHandle;
|
|
61
|
+
// Transaction to ensure both operations succeed or fail together
|
|
62
|
+
await cn.transaction(async (tx) => {
|
|
63
|
+
// Update the step status
|
|
64
|
+
await tx("PgStep")
|
|
65
|
+
.where({ sessionId, stepNumber })
|
|
66
|
+
.update({
|
|
67
|
+
status: "completed",
|
|
68
|
+
durationMs: Date.now() - startedAt.getTime(),
|
|
69
|
+
currentActivity: null,
|
|
70
|
+
explanation: values.explanation,
|
|
71
|
+
});
|
|
72
|
+
// Insert the completed step details
|
|
73
|
+
await tx("PgSchemaStep").insert({
|
|
74
|
+
stepNumber,
|
|
75
|
+
sessionId,
|
|
76
|
+
promptTitle: values.promptTitle,
|
|
77
|
+
siteSchema: JSON.stringify(values.siteSchema),
|
|
78
|
+
l10n: JSON.stringify(values.l10n),
|
|
79
|
+
localizedValues: JSON.stringify(values.localizedValues),
|
|
80
|
+
nodeTypeCount: values.siteSchema.nodeTypes?.length ?? 0,
|
|
81
|
+
inputTokenCount: values.inputTokenCount,
|
|
82
|
+
outputTokenCount: values.outputTokenCount,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export async function saveGeneratedSiteStep(ctx, stepHandle, values) {
|
|
87
|
+
const { cn, sessionId } = ctx;
|
|
88
|
+
const { stepNumber, startedAt } = stepHandle;
|
|
89
|
+
const completed = values.status === "completed";
|
|
90
|
+
await cn.transaction(async (tx) => {
|
|
91
|
+
await tx("PgStep")
|
|
92
|
+
.where({ sessionId, stepNumber })
|
|
93
|
+
.update({
|
|
94
|
+
status: values.status,
|
|
95
|
+
durationMs: completed ? Date.now() - startedAt.getTime() : null,
|
|
96
|
+
currentActivity: completed ? null : undefined,
|
|
97
|
+
});
|
|
98
|
+
await tx("PgGeneratedSiteStep").insert({
|
|
99
|
+
siteId: values.siteId,
|
|
100
|
+
stepNumber,
|
|
101
|
+
sessionId,
|
|
102
|
+
siteUrl: values.siteUrl,
|
|
103
|
+
loginEmail: values.loginEmail,
|
|
104
|
+
loginPassword: values.loginPassword,
|
|
105
|
+
localizedValues: JSON.stringify(values.localizedValues),
|
|
106
|
+
contentEntryCount: completed ? 0 : undefined,
|
|
107
|
+
contentInputTokenCount: completed ? 0 : undefined,
|
|
108
|
+
contentOutputTokenCount: completed ? 0 : undefined,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export async function updateGeneratedSiteStepSetAsCompleted(ctx, stepHandle, values) {
|
|
113
|
+
const { cn, sessionId } = ctx;
|
|
114
|
+
const { stepNumber, startedAt } = stepHandle;
|
|
115
|
+
await cn.transaction(async (tx) => {
|
|
116
|
+
await tx("PgStep")
|
|
117
|
+
.where({ sessionId, stepNumber })
|
|
118
|
+
.update({
|
|
119
|
+
status: values.status,
|
|
120
|
+
durationMs: Date.now() - startedAt.getTime(),
|
|
121
|
+
currentActivity: null,
|
|
122
|
+
});
|
|
123
|
+
await tx("PgGeneratedSiteStep").where({ sessionId, stepNumber }).update({
|
|
124
|
+
contentEntryCount: values.contentEntryCount,
|
|
125
|
+
contentInputTokenCount: values.contentInputTokenCount,
|
|
126
|
+
contentOutputTokenCount: values.contentOutputTokenCount,
|
|
127
|
+
contentErrors: values.contentErrors,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export async function insertIssueEvent(ctx, values) {
|
|
132
|
+
const { cn, sessionId } = ctx;
|
|
133
|
+
const [inserted] = await cn("PgIssueEvent")
|
|
134
|
+
.insert({
|
|
135
|
+
sessionId,
|
|
136
|
+
eventType: values.eventType,
|
|
137
|
+
issueMessage: values.issueMessage,
|
|
138
|
+
llmTaskName: values.llmTaskName,
|
|
139
|
+
stepNumber: values.stepNumber,
|
|
140
|
+
siteId: values.siteId,
|
|
141
|
+
})
|
|
142
|
+
.returning("id");
|
|
143
|
+
return { id: strVal(inserted.id) };
|
|
144
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { executeDdl } from "@paroicms/internal-server-lib";
|
|
1
2
|
export const dbSchemaName = "site-generator";
|
|
2
|
-
export const currentDbSchemaVersion =
|
|
3
|
-
export async function migrateSiteGeneratorDb(
|
|
3
|
+
export const currentDbSchemaVersion = 2;
|
|
4
|
+
export async function migrateSiteGeneratorDb(cn, { fromVersion, logger, ddlFile }) {
|
|
4
5
|
const toVersion = currentDbSchemaVersion;
|
|
5
|
-
|
|
6
|
+
let currentVersion = fromVersion;
|
|
6
7
|
if (currentVersion === 1) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
await cn.raw("drop table PaGenSession");
|
|
9
|
+
await cn.raw("drop table PaMetadata");
|
|
10
|
+
await executeDdl(cn, ddlFile);
|
|
11
|
+
currentVersion = currentDbSchemaVersion;
|
|
10
12
|
}
|
|
11
13
|
if (currentVersion !== toVersion) {
|
|
12
14
|
throw new Error(`version of ${dbSchemaName} database should be '${toVersion}', but is '${currentVersion}'`);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { strVal, strValOrUndef } from "@paroi/data-formatters-lib";
|
|
2
|
+
export function ensureType(value) {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
export function formatStepKind(value) {
|
|
6
|
+
const v = strVal(value);
|
|
7
|
+
if (v === "initialSchema" || v === "updateSchema" || v === "generateSite") {
|
|
8
|
+
return v;
|
|
9
|
+
}
|
|
10
|
+
throw new Error(`Invalid step kind "${v}"`);
|
|
11
|
+
}
|
|
12
|
+
export function formatSchemaStepKind(value) {
|
|
13
|
+
const v = strVal(value);
|
|
14
|
+
if (v === "initialSchema" || v === "updateSchema") {
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Invalid schema step kind "${v}"`);
|
|
18
|
+
}
|
|
19
|
+
export function formatGeneratedSiteStepKind(value) {
|
|
20
|
+
const v = strVal(value);
|
|
21
|
+
if (v === "generateSite") {
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Invalid generated site step kind "${v}"`);
|
|
25
|
+
}
|
|
26
|
+
export function formatStepStatus(value) {
|
|
27
|
+
const v = strVal(value);
|
|
28
|
+
if (v === "completed" || v === "pending" || v === "failed" || v === "noEffect") {
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Invalid step status "${v}"`);
|
|
32
|
+
}
|
|
33
|
+
export function formatActivityCode(value) {
|
|
34
|
+
const v = strValOrUndef(value);
|
|
35
|
+
if (v === undefined)
|
|
36
|
+
return;
|
|
37
|
+
if (v === "initial1" ||
|
|
38
|
+
v === "initial2" ||
|
|
39
|
+
v === "updating1" ||
|
|
40
|
+
v === "updating2" ||
|
|
41
|
+
v === "generatingSite" ||
|
|
42
|
+
v === "generatingContent") {
|
|
43
|
+
return v;
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Invalid activity code "${v}"`);
|
|
46
|
+
}
|