@aglyn/plugins-ai 1.0.0-beta.271 → 1.0.0-beta.272
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/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +4 -3
- package/src/lib/activity/ai-activity-actions.js +3 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +18 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.d.ts +41 -0
- package/src/lib/components/ai-site-import-job.component.js +707 -0
- package/src/lib/components/ai-site-import-job.component.js.map +1 -0
- package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
- package/src/lib/components/ai-site-import-start.component.js +335 -0
- package/src/lib/components/ai-site-import-start.component.js.map +1 -0
- package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
- package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
- package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.js +165 -91
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
- package/src/lib/jobs/ai-job-drafts.js +24 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
- package/src/lib/jobs/ai-job-import-step.js +809 -0
- package/src/lib/jobs/ai-job-import-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +39 -1
- package/src/lib/jobs/ai-job-page-language.js +62 -4
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-page-step.js +5 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +5 -0
- package/src/lib/jobs/ai-jobs.js +15 -3
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
- package/src/lib/jobs/ai-layout-owner-photos.js +47 -0
- package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +61 -3
- package/src/lib/jobs/ai-theme-brand-inputs.js +117 -27
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +20 -0
- package/src/lib/model/ai-credit-estimate.js +13 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +2 -2
- package/src/lib/model/ai-job-activity.js +6 -4
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-notice.js +18 -4
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +29 -2
- package/src/lib/model/ai-jobs.types.js +2 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import-view.d.ts +130 -0
- package/src/lib/model/ai-site-import-view.js +182 -0
- package/src/lib/model/ai-site-import-view.js.map +1 -0
- package/src/lib/model/ai-site-import.d.ts +302 -0
- package/src/lib/model/ai-site-import.js +526 -0
- package/src/lib/model/ai-site-import.js.map +1 -0
- package/src/lib/model/ai-site-start.d.ts +22 -9
- package/src/lib/model/ai-site-start.js +64 -23
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +11 -0
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/ai-site-import-route.d.ts +89 -0
- package/src/lib/server/ai-site-import-route.js +356 -0
- package/src/lib/server/ai-site-import-route.js.map +1 -0
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +163 -0
- package/src/lib/site-import/site-import-crawl.js +467 -0
- package/src/lib/site-import/site-import-crawl.js.map +1 -0
- package/src/lib/site-import/site-import-flag.d.ts +23 -0
- package/src/lib/site-import/site-import-flag.js +23 -0
- package/src/lib/site-import/site-import-flag.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +94 -0
- package/src/lib/site-import/site-import-html.js +462 -0
- package/src/lib/site-import/site-import-html.js.map +1 -0
- package/src/lib/site-import/site-import-images.d.ts +54 -0
- package/src/lib/site-import/site-import-images.js +204 -0
- package/src/lib/site-import/site-import-images.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +48 -0
- package/src/lib/site-import/site-import-page.js +150 -0
- package/src/lib/site-import/site-import-page.js.map +1 -0
- package/src/lib/site-import/site-import-review.d.ts +40 -0
- package/src/lib/site-import/site-import-review.js +149 -0
- package/src/lib/site-import/site-import-review.js.map +1 -0
- package/src/lib/site-import/site-import-robots.d.ts +52 -0
- package/src/lib/site-import/site-import-robots.js +126 -0
- package/src/lib/site-import/site-import-robots.js.map +1 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /*==========================================
|
|
18
|
+
* THE `import` JOB: BRING MY SITE, IN REVIEWED WAVES (AGL-3976).
|
|
19
|
+
*
|
|
20
|
+
* One step, many passes, each one unit of work (`aiSiteImportNextWork`):
|
|
21
|
+
*
|
|
22
|
+
* 1. READ the proved site (no model): the crawler reads it as AglynBot,
|
|
23
|
+
* main pages first, up to the plan's page count; the inventory is cut
|
|
24
|
+
* into waves (`model/ai-site-import.ts`), every page gets a ledger row,
|
|
25
|
+
* and Trust & Safety gets its row. The job then WAITS (`needs_review`,
|
|
26
|
+
* reason `wave`): nothing is brought in until the person starts wave 1.
|
|
27
|
+
* 2. START A WAVE (no model), on the person's say-so: the proof is still
|
|
28
|
+
* current, the day's limit has room, the plan still has room; the
|
|
29
|
+
* wave's pages are read again, fresh, and a page that became a sign-in
|
|
30
|
+
* page, went away or is now off limits to AglynBot is left out.
|
|
31
|
+
* 3. THE HEADER AND FOOTER, in the first wave: the layout step's own pass,
|
|
32
|
+
* linking the wave's pages.
|
|
33
|
+
* 4. EACH PAGE: its photos into the media library (no model, deduped by
|
|
34
|
+
* source key), then the page step's own passes in the layout language,
|
|
35
|
+
* told the page's text FENCED as untrusted data.
|
|
36
|
+
* 5. CLOSE THE WAVE: the job waits again, with the wave's pages as drafts
|
|
37
|
+
* to review, until the person starts the next one. The last wave ends
|
|
38
|
+
* the job.
|
|
39
|
+
*
|
|
40
|
+
* Every page is an item of the job's ledger, so a page that fails on our
|
|
41
|
+
* side gives back its own spend (the machine's `refundBuildCredits`) and the
|
|
42
|
+
* wave goes on. Nothing here publishes: every page is a draft, and the site
|
|
43
|
+
* stays unpublished until a member publishes it, which they can only reach
|
|
44
|
+
* once the import, behind its ownership proof, has written something.
|
|
45
|
+
*=========================================*/ import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
46
|
+
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
47
|
+
import { pluginMediaIngest } from "@aglyn/aglyn/plugin-manager/plugin-media-ingest";
|
|
48
|
+
import { listDomainOwnershipClaims, normalizeOwnershipDomain } from "@aglyn/tenant-data-admin/server/domain-ownership";
|
|
49
|
+
import { getOrgForUser } from "@aglyn/tenant-data-admin/server/organizations";
|
|
50
|
+
import { consumeRateLimit } from "@aglyn/tenant-data-admin/server/rate-limit-store";
|
|
51
|
+
import { isServerReleaseFlagOnForOrg } from "@aglyn/tenant-data-admin/server/release-flags";
|
|
52
|
+
import { AI_SITE_IMPORT_DOMAIN_INPUT, AI_SITE_IMPORT_FRAME_SLOT, AI_SITE_IMPORT_LEFT_OUT_COPY, AI_SITE_IMPORT_MAX_PAGES, AI_SITE_IMPORT_MIN_WORDS, AI_SITE_IMPORT_WAVE_PAGES, aiSiteImportInventory, aiSiteImportItemOpen, aiSiteImportLedger, aiSiteImportNextWork, aiSiteImportPageSettled, aiSiteImportPagesRead, aiSiteImportPhotosIn, aiSiteImportWaveClosed, aiSiteImportWaveMessage, aiSiteImportWaveStarted } from "../model/ai-site-import.js";
|
|
53
|
+
import { aiModelForStep } from "../providers/routing.js";
|
|
54
|
+
import { SITE_IMPORT_RELEASE_FLAG } from "../site-import/site-import-flag.js";
|
|
55
|
+
import { crawlSiteForImport } from "../site-import/site-import-crawl.js";
|
|
56
|
+
import { bringInSiteImportPhotos } from "../site-import/site-import-images.js";
|
|
57
|
+
import { siteImportLeftOutReason, siteImportListedPage, siteImportPageRead, siteImportPageWords } from "../site-import/site-import-page.js";
|
|
58
|
+
import { fileSiteImportReview } from "../site-import/site-import-review.js";
|
|
59
|
+
import { aiUnitErrorRetryable, aiUnitFailure, aiUnitSpend } from "./ai-build-unit-outcome.js";
|
|
60
|
+
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
61
|
+
import { AI_JOB_STEP_OVERHEAD_MS } from "./ai-job-budget.js";
|
|
62
|
+
import { readAiScreenRoom } from "./ai-job-drafts.js";
|
|
63
|
+
import { aiUnspentOutcome } from "./ai-job-generation.js";
|
|
64
|
+
import { AI_LAYOUT_PAGE_PHOTOS_INPUT } from "./ai-layout-owner-photos.js";
|
|
65
|
+
import { AI_LAYOUT_SITE_PAGES_INPUT, AI_LAYOUT_SITE_PAGES_MAX } from "./ai-job-layout-site-pages.js";
|
|
66
|
+
import { AI_JOB_PAGE_STEP_MINIMUM_MS } from "./ai-job-page-budget.js";
|
|
67
|
+
import { AI_LAYOUT_IMPORTED_PAGE_INPUT, AI_LAYOUT_LANGUAGE_INPUT } from "./ai-job-page-language.js";
|
|
68
|
+
import { aiSitePageWritten } from "./ai-job-site-step.js";
|
|
69
|
+
import { AI_STOCK_PHOTO_PAGES_INPUT } from "./ai-layout-stock-photos.js";
|
|
70
|
+
import { AI_JOB_BRIEF_MAX_CHARS } from "./ai-job-text-step.js";
|
|
71
|
+
import { aiApplyJobItemRecord, aiJobStepRunMinimumMs, aiJobStepRunnerFor, registerAiJobStep, registerAiJobStepPasses } from "./ai-jobs.js";
|
|
72
|
+
/** Waves (the read included) one workspace may start in a day. */ export const SITE_IMPORT_WAVES_PER_ORG_PER_DAY = 12;
|
|
73
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
74
|
+
/** The rate-limit key a workspace's imports share. */ export function siteImportRateKey(orgId) {
|
|
75
|
+
return `ai-import:${orgId}`;
|
|
76
|
+
}
|
|
77
|
+
/** The crawl's clock when the import reads the whole site. */ export const AI_SITE_IMPORT_READ_MS = 150000;
|
|
78
|
+
/** The crawl's clock when a wave reads its own pages again. */ export const AI_SITE_IMPORT_WAVE_READ_MS = 90000;
|
|
79
|
+
/** The clock for one page's photos: each is fetched and stored through the library's door. */ export const AI_SITE_IMPORT_PHOTOS_MS = 90000;
|
|
80
|
+
/** What the step's own reads and writes around a pass take: the claims, the room, the row. */ const OWN_READS_MS = 10000;
|
|
81
|
+
/** The least time each pass that asks no model needs. */ export const AI_SITE_IMPORT_PASS_MINIMUM_MS = {
|
|
82
|
+
read: AI_SITE_IMPORT_READ_MS + OWN_READS_MS + AI_JOB_STEP_OVERHEAD_MS,
|
|
83
|
+
wave: AI_SITE_IMPORT_WAVE_READ_MS + OWN_READS_MS + AI_JOB_STEP_OVERHEAD_MS,
|
|
84
|
+
photos: AI_SITE_IMPORT_PHOTOS_MS + OWN_READS_MS + AI_JOB_STEP_OVERHEAD_MS
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* The passes one import may take: a read, then for every page its photos,
|
|
88
|
+
* its answer and its listing, every wave's start and the header and footer,
|
|
89
|
+
* with room for re-asks.
|
|
90
|
+
*/ export const AI_SITE_IMPORT_MAX_PASSES = AI_SITE_IMPORT_MAX_PAGES * 4 + 60;
|
|
91
|
+
/** Customer-safe sentences. */ export const AI_SITE_IMPORT_COPY = {
|
|
92
|
+
noSite: 'Open the site to bring your website into.',
|
|
93
|
+
badDomain: 'This website’s address cannot be brought in.',
|
|
94
|
+
ownership: (domain)=>`Verify that you own ${domain} before bringing it in. A proof is good for 30 days; verify again, then start this wave again.`,
|
|
95
|
+
limit: `This workspace has started as many site waves as it can today (${SITE_IMPORT_WAVES_PER_ORG_PER_DAY}). Start this wave again tomorrow.`,
|
|
96
|
+
robots: (domain)=>`The robots.txt file of ${domain} could not be read just now, so nothing was read. Try again in a few minutes.`,
|
|
97
|
+
nothing: (domain)=>`No page of ${domain} could be read. Check that the site is up, then try again.`,
|
|
98
|
+
unavailable: 'Bringing a site in is not available on this workspace right now.',
|
|
99
|
+
frameUnavailable: 'The header and footer could not be built here; your pages use the site’s own layout.',
|
|
100
|
+
pageUnavailable: 'Pages cannot be built here right now.',
|
|
101
|
+
pageNotWritten: 'This page could not be written from its old page. Its spend is given back.',
|
|
102
|
+
plan: 'Bringing an existing site in is included on every paid plan.',
|
|
103
|
+
role: 'Only the workspace owner or an admin can bring a site in.'
|
|
104
|
+
};
|
|
105
|
+
async function readOwnership(orgId, domain) {
|
|
106
|
+
const claims = await listDomainOwnershipClaims(orgId);
|
|
107
|
+
const claim = claims.find((entry)=>entry.domain === domain);
|
|
108
|
+
if (!claim) return null;
|
|
109
|
+
return {
|
|
110
|
+
current: claim.current,
|
|
111
|
+
method: claim.provenMethod,
|
|
112
|
+
provenAt: claim.provenAt,
|
|
113
|
+
detail: claim.provenDetail
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function readSiteName(firestore, hostId) {
|
|
117
|
+
const host = await firestore.collection('hosts').doc(hostId).get();
|
|
118
|
+
const name = host.get('displayName');
|
|
119
|
+
return typeof name === 'string' ? name.trim() : '';
|
|
120
|
+
}
|
|
121
|
+
/** The proved domain an import job is for, or `null`. */ export function aiSiteImportDomainOf(job) {
|
|
122
|
+
var _ref;
|
|
123
|
+
var _job_inputs;
|
|
124
|
+
return normalizeOwnershipDomain(String((_ref = (_job_inputs = job.inputs) == null ? void 0 : _job_inputs[AI_SITE_IMPORT_DOMAIN_INPUT]) != null ? _ref : '')).domain;
|
|
125
|
+
}
|
|
126
|
+
/*------------------------------------------
|
|
127
|
+
* The jobs a unit is handed to
|
|
128
|
+
*-----------------------------------------*/ const pathOf = (slug)=>slug === '/' ? '/' : `/${slug}`;
|
|
129
|
+
/** A plan as a delegated unit reads it: confirmed, with what it builds and nothing else. */ function unitPlan(job, input) {
|
|
130
|
+
var _input_create, _input_screens;
|
|
131
|
+
return {
|
|
132
|
+
reuse: [],
|
|
133
|
+
create: (_input_create = input.create) != null ? _input_create : [],
|
|
134
|
+
screens: (_input_screens = input.screens) != null ? _input_screens : [],
|
|
135
|
+
labels: {},
|
|
136
|
+
status: 'confirmed',
|
|
137
|
+
proposedAt: job.createdAt,
|
|
138
|
+
confirmedAt: job.createdAt,
|
|
139
|
+
confirmedBy: job.createdBy
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const delivered = (items, slot)=>items.some((row)=>row.slot === slot && (row.status === 'succeeded' || row.status === 'degraded'));
|
|
143
|
+
/**
|
|
144
|
+
* The pages the header and a page may link: the ones of this wave and those
|
|
145
|
+
* before it that are in or still coming, home first, at most the header's
|
|
146
|
+
* seven. A page of a later wave is not linked: it may never be brought in.
|
|
147
|
+
*/ export function aiSiteImportLinkedPages(state, items, wave) {
|
|
148
|
+
const rows = new Map(items.map((row)=>[
|
|
149
|
+
row.slot,
|
|
150
|
+
row
|
|
151
|
+
]));
|
|
152
|
+
const pages = state.pages.filter((page)=>page.wave <= wave && !page.refused).filter((page)=>{
|
|
153
|
+
const row = rows.get(page.slot);
|
|
154
|
+
return !row || row.status === 'pending' || row.status === 'running' || row.status === 'succeeded' || row.status === 'degraded';
|
|
155
|
+
}).map((page)=>({
|
|
156
|
+
id: page.draftId,
|
|
157
|
+
label: page.title,
|
|
158
|
+
slug: pathOf(page.slug)
|
|
159
|
+
}));
|
|
160
|
+
return [
|
|
161
|
+
...pages.filter((page)=>page.slug === '/'),
|
|
162
|
+
...pages.filter((page)=>page.slug !== '/')
|
|
163
|
+
].slice(0, AI_LAYOUT_SITE_PAGES_MAX);
|
|
164
|
+
}
|
|
165
|
+
/** The header and footer's job: a layout of the site's name, linking the wave's pages, in the layout language. */ export function aiSiteImportFrameJob(job, state, wave, items) {
|
|
166
|
+
var _ref;
|
|
167
|
+
var _state_frame;
|
|
168
|
+
const draftId = (_ref = (_state_frame = state.frame) == null ? void 0 : _state_frame.draftId) != null ? _ref : `${job.$id}-${AI_SITE_IMPORT_FRAME_SLOT}`;
|
|
169
|
+
const creation = {
|
|
170
|
+
kind: 'layout',
|
|
171
|
+
name: 'Header and footer',
|
|
172
|
+
why: `The header and footer every page brought in from ${state.domain} renders inside.`,
|
|
173
|
+
duplicateOf: null,
|
|
174
|
+
fields: [
|
|
175
|
+
'header',
|
|
176
|
+
'nav',
|
|
177
|
+
'main',
|
|
178
|
+
'footer'
|
|
179
|
+
],
|
|
180
|
+
id: draftId
|
|
181
|
+
};
|
|
182
|
+
return _extends({}, job, {
|
|
183
|
+
$id: draftId,
|
|
184
|
+
kind: 'layout',
|
|
185
|
+
steps: [],
|
|
186
|
+
outputs: [],
|
|
187
|
+
items: null,
|
|
188
|
+
siteImport: null,
|
|
189
|
+
plan: unitPlan(job, {
|
|
190
|
+
create: [
|
|
191
|
+
creation
|
|
192
|
+
]
|
|
193
|
+
}),
|
|
194
|
+
inputs: {
|
|
195
|
+
originJobId: job.$id,
|
|
196
|
+
businessName: state.siteName,
|
|
197
|
+
[AI_LAYOUT_LANGUAGE_INPUT]: true,
|
|
198
|
+
[AI_LAYOUT_SITE_PAGES_INPUT]: aiSiteImportLinkedPages(state, items, wave.index)
|
|
199
|
+
},
|
|
200
|
+
brief: `Build the header and footer of “${state.siteName}”, a website brought in from ${state.domain}.`.slice(0, AI_JOB_BRIEF_MAX_CHARS)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** One page's job: a page job's plan of one screen, its sections from the old page's headings, its text fenced. */ export function aiSiteImportPageJob(job, state, page, items) {
|
|
204
|
+
var _ref, _ref1;
|
|
205
|
+
var _page_read, _page_read1, _page_photos;
|
|
206
|
+
const sections = (((_page_read = page.read) == null ? void 0 : _page_read.sections.length) ? page.read.sections : [
|
|
207
|
+
'Introduction'
|
|
208
|
+
]).map((name)=>({
|
|
209
|
+
name,
|
|
210
|
+
uses: [],
|
|
211
|
+
items: 0
|
|
212
|
+
}));
|
|
213
|
+
const screen = {
|
|
214
|
+
title: page.title,
|
|
215
|
+
slug: pathOf(page.slug),
|
|
216
|
+
// Inside the header and footer this import built, once it is in.
|
|
217
|
+
layout: state.frame && delivered(items, AI_SITE_IMPORT_FRAME_SLOT) ? state.frame.draftId : null,
|
|
218
|
+
template: null,
|
|
219
|
+
duplicateOf: null,
|
|
220
|
+
nav: false,
|
|
221
|
+
seoTitle: page.title,
|
|
222
|
+
seoDescription: '',
|
|
223
|
+
sections,
|
|
224
|
+
record: null,
|
|
225
|
+
id: page.draftId
|
|
226
|
+
};
|
|
227
|
+
// The other pages already in, whose stock photos this page does not repeat.
|
|
228
|
+
const others = state.pages.filter((entry)=>entry.slot !== page.slot && delivered(items, entry.slot)).map((entry)=>entry.draftId);
|
|
229
|
+
return _extends({}, job, {
|
|
230
|
+
$id: page.draftId,
|
|
231
|
+
kind: 'page',
|
|
232
|
+
steps: [],
|
|
233
|
+
outputs: [],
|
|
234
|
+
items: null,
|
|
235
|
+
siteImport: null,
|
|
236
|
+
plan: unitPlan(job, {
|
|
237
|
+
screens: [
|
|
238
|
+
screen
|
|
239
|
+
]
|
|
240
|
+
}),
|
|
241
|
+
inputs: _extends({
|
|
242
|
+
originJobId: job.$id,
|
|
243
|
+
businessName: state.siteName,
|
|
244
|
+
[AI_LAYOUT_LANGUAGE_INPUT]: true,
|
|
245
|
+
[AI_LAYOUT_SITE_PAGES_INPUT]: aiSiteImportLinkedPages(state, items, page.wave),
|
|
246
|
+
[AI_LAYOUT_IMPORTED_PAGE_INPUT]: {
|
|
247
|
+
url: page.url,
|
|
248
|
+
text: (_ref = (_page_read1 = page.read) == null ? void 0 : _page_read1.text) != null ? _ref : ''
|
|
249
|
+
},
|
|
250
|
+
[AI_LAYOUT_PAGE_PHOTOS_INPUT]: (_ref1 = (_page_photos = page.photos) == null ? void 0 : _page_photos.assets) != null ? _ref1 : []
|
|
251
|
+
}, others.length ? {
|
|
252
|
+
[AI_STOCK_PHOTO_PAGES_INPUT]: others
|
|
253
|
+
} : {}),
|
|
254
|
+
brief: `Bring in the page “${page.title}” of ${state.domain} as a page of “${state.siteName}”.`.slice(0, AI_JOB_BRIEF_MAX_CHARS)
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/** The least time the import's next pass needs: a read's, a photo pass's, or the time of the step its unit is handed to. */ export function aiSiteImportRunMinimumMs(job, runnerFor = aiJobStepRunnerFor) {
|
|
258
|
+
var _job_siteImport, _job_items;
|
|
259
|
+
const state = (_job_siteImport = job.siteImport) != null ? _job_siteImport : null;
|
|
260
|
+
const items = (_job_items = job.items) != null ? _job_items : [];
|
|
261
|
+
const work = aiSiteImportNextWork(state, items);
|
|
262
|
+
switch(work.kind){
|
|
263
|
+
case 'inventory':
|
|
264
|
+
return AI_SITE_IMPORT_PASS_MINIMUM_MS.read;
|
|
265
|
+
case 'start-wave':
|
|
266
|
+
case 'read':
|
|
267
|
+
return AI_SITE_IMPORT_PASS_MINIMUM_MS.wave;
|
|
268
|
+
case 'images':
|
|
269
|
+
return AI_SITE_IMPORT_PASS_MINIMUM_MS.photos;
|
|
270
|
+
case 'frame':
|
|
271
|
+
return runnerFor('layout') && state ? aiJobStepRunMinimumMs(aiSiteImportFrameJob(job, state, work.wave, items)) : 0;
|
|
272
|
+
case 'page':
|
|
273
|
+
return runnerFor('page') && state ? aiJobStepRunMinimumMs(aiSiteImportPageJob(job, state, work.page, items)) : 0;
|
|
274
|
+
default:
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/*------------------------------------------
|
|
279
|
+
* The step
|
|
280
|
+
*-----------------------------------------*/ const waveReview = (message)=>({
|
|
281
|
+
reason: 'wave',
|
|
282
|
+
message,
|
|
283
|
+
findings: []
|
|
284
|
+
});
|
|
285
|
+
/** The rows a wave's read left out, settled as not brought in, with the reason. */ function withLeftOutRows(items, reads) {
|
|
286
|
+
return items.map((row)=>{
|
|
287
|
+
const read = reads.get(row.slot);
|
|
288
|
+
return typeof read === 'string' && (row.status === 'pending' || row.status === 'running') ? _extends({}, row, {
|
|
289
|
+
status: 'skipped',
|
|
290
|
+
note: AI_SITE_IMPORT_LEFT_OUT_COPY[read]
|
|
291
|
+
}) : row;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** Whether every row of the wave is settled, its header and footer included. */ function waveSettled(state, wave, items) {
|
|
295
|
+
if (wave.frame && state.frame && aiSiteImportItemOpen(items, AI_SITE_IMPORT_FRAME_SLOT)) return false;
|
|
296
|
+
return wave.slots.every((slot)=>!aiSiteImportItemOpen(items, slot));
|
|
297
|
+
}
|
|
298
|
+
export function createAiJobImportStep(deps = {}) {
|
|
299
|
+
var _deps_crawl, _deps_runnerFor, _deps_ownership, _deps_rateLimit, _deps_releaseFlagOn, _deps_ingest, _deps_readRoom, _deps_readSiteName, _deps_fileReview, _deps_pageWritten, _deps_mintId, _deps_clock;
|
|
300
|
+
const crawl = (_deps_crawl = deps.crawl) != null ? _deps_crawl : crawlSiteForImport;
|
|
301
|
+
const runnerFor = (_deps_runnerFor = deps.runnerFor) != null ? _deps_runnerFor : aiJobStepRunnerFor;
|
|
302
|
+
const ownershipOf = (_deps_ownership = deps.ownership) != null ? _deps_ownership : readOwnership;
|
|
303
|
+
const rateLimit = (_deps_rateLimit = deps.rateLimit) != null ? _deps_rateLimit : consumeRateLimit;
|
|
304
|
+
const flagOn = (_deps_releaseFlagOn = deps.releaseFlagOn) != null ? _deps_releaseFlagOn : (orgId)=>isServerReleaseFlagOnForOrg(SITE_IMPORT_RELEASE_FLAG, orgId);
|
|
305
|
+
const ingestOf = (_deps_ingest = deps.ingest) != null ? _deps_ingest : pluginMediaIngest;
|
|
306
|
+
const readRoom = (_deps_readRoom = deps.readRoom) != null ? _deps_readRoom : readAiScreenRoom;
|
|
307
|
+
const siteNameOf = (_deps_readSiteName = deps.readSiteName) != null ? _deps_readSiteName : readSiteName;
|
|
308
|
+
const fileReview = (_deps_fileReview = deps.fileReview) != null ? _deps_fileReview : fileSiteImportReview;
|
|
309
|
+
const pageWritten = (_deps_pageWritten = deps.pageWritten) != null ? _deps_pageWritten : aiSitePageWritten;
|
|
310
|
+
const mintId = (_deps_mintId = deps.mintId) != null ? _deps_mintId : createResourceUid;
|
|
311
|
+
const clock = (_deps_clock = deps.clock) != null ? _deps_clock : Date.now;
|
|
312
|
+
return async (context)=>{
|
|
313
|
+
var _job_siteImport, _job_items;
|
|
314
|
+
const { job, firestore, now, signal } = context;
|
|
315
|
+
const model = aiModelForStep('job.page');
|
|
316
|
+
const unspent = (rest = {})=>_extends({}, aiUnspentOutcome(model), rest);
|
|
317
|
+
const hostId = job.hostId;
|
|
318
|
+
if (!hostId) return unspent({
|
|
319
|
+
failure: AI_SITE_IMPORT_COPY.noSite
|
|
320
|
+
});
|
|
321
|
+
const domain = aiSiteImportDomainOf(job);
|
|
322
|
+
if (!domain) return unspent({
|
|
323
|
+
failure: AI_SITE_IMPORT_COPY.badDomain
|
|
324
|
+
});
|
|
325
|
+
const state = (_job_siteImport = job.siteImport) != null ? _job_siteImport : null;
|
|
326
|
+
const items = (_job_items = job.items) != null ? _job_items : [];
|
|
327
|
+
const work = aiSiteImportNextWork(state, items);
|
|
328
|
+
const nowIso = now.toISOString();
|
|
329
|
+
const review = (event, next, ledger, wave)=>fileReview(firestore, _extends({
|
|
330
|
+
orgId: job.orgId,
|
|
331
|
+
hostId,
|
|
332
|
+
jobId: job.$id,
|
|
333
|
+
state: next,
|
|
334
|
+
items: ledger,
|
|
335
|
+
event,
|
|
336
|
+
nowMs: now.getTime()
|
|
337
|
+
}, wave !== undefined ? {
|
|
338
|
+
wave
|
|
339
|
+
} : {}));
|
|
340
|
+
/** Closes the wave when this pass settled its last row: the review, or the job's end after the last wave. */ const closing = async (next, ledger, wave, outcome)=>{
|
|
341
|
+
var _closed_waves_find;
|
|
342
|
+
if (!waveSettled(next, wave, ledger)) return _extends({}, outcome, {
|
|
343
|
+
siteImport: next,
|
|
344
|
+
continue: true
|
|
345
|
+
});
|
|
346
|
+
const closed = aiSiteImportWaveClosed(next, wave.index, nowIso);
|
|
347
|
+
await review('wave-finished', closed, ledger, wave.index);
|
|
348
|
+
const waiting = ((_closed_waves_find = closed.waves.find((entry)=>entry.index === wave.index)) == null ? void 0 : _closed_waves_find.status) === 'review';
|
|
349
|
+
return waiting ? _extends({}, outcome, {
|
|
350
|
+
siteImport: closed,
|
|
351
|
+
review: waveReview(aiSiteImportWaveMessage(closed, ledger))
|
|
352
|
+
}) : _extends({}, outcome, {
|
|
353
|
+
siteImport: closed
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
/** The proof, the flag and the day's limit, asked before anything is read. */ const gate = async ()=>{
|
|
357
|
+
if (!await flagOn(job.orgId)) return unspent({
|
|
358
|
+
review: waveReview(AI_SITE_IMPORT_COPY.unavailable)
|
|
359
|
+
});
|
|
360
|
+
const ownership = await ownershipOf(job.orgId, domain);
|
|
361
|
+
if (!(ownership == null ? void 0 : ownership.current)) {
|
|
362
|
+
return unspent({
|
|
363
|
+
review: {
|
|
364
|
+
reason: 'ownership',
|
|
365
|
+
message: AI_SITE_IMPORT_COPY.ownership(domain),
|
|
366
|
+
findings: []
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const window = await rateLimit(siteImportRateKey(job.orgId), {
|
|
371
|
+
limit: SITE_IMPORT_WAVES_PER_ORG_PER_DAY,
|
|
372
|
+
windowMs: DAY_MS
|
|
373
|
+
});
|
|
374
|
+
if (!window.allowed) return unspent({
|
|
375
|
+
review: {
|
|
376
|
+
reason: 'limit',
|
|
377
|
+
message: AI_SITE_IMPORT_COPY.limit,
|
|
378
|
+
findings: []
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
ownership
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
// ── 1. Read the site ──
|
|
386
|
+
if (work.kind === 'inventory') {
|
|
387
|
+
var _context_org;
|
|
388
|
+
const passed = await gate();
|
|
389
|
+
if (!('ownership' in passed)) return passed;
|
|
390
|
+
const room = await readRoom(firestore, {
|
|
391
|
+
hostId,
|
|
392
|
+
org: (_context_org = context.org) != null ? _context_org : null
|
|
393
|
+
});
|
|
394
|
+
const left = room.limit === null ? AI_SITE_IMPORT_MAX_PAGES : Math.max(0, room.limit - room.used);
|
|
395
|
+
const crawled = await crawl(_extends({
|
|
396
|
+
domain,
|
|
397
|
+
// A wave's worth past the room, so what the plan leaves out is said.
|
|
398
|
+
maxPages: Math.min(AI_SITE_IMPORT_MAX_PAGES, left + AI_SITE_IMPORT_WAVE_PAGES),
|
|
399
|
+
timeBudgetMs: AI_SITE_IMPORT_READ_MS,
|
|
400
|
+
shallowFirst: true
|
|
401
|
+
}, signal ? {
|
|
402
|
+
signal
|
|
403
|
+
} : {}), deps.crawlSeams);
|
|
404
|
+
if (crawled.refused) return unspent({
|
|
405
|
+
failure: AI_SITE_IMPORT_COPY.badDomain
|
|
406
|
+
});
|
|
407
|
+
if (crawled.stoppedBy === 'robots-unreachable') return unspent({
|
|
408
|
+
review: waveReview(AI_SITE_IMPORT_COPY.robots(domain))
|
|
409
|
+
});
|
|
410
|
+
if (!crawled.pages.length) return unspent({
|
|
411
|
+
review: waveReview(AI_SITE_IMPORT_COPY.nothing(domain))
|
|
412
|
+
});
|
|
413
|
+
const inventory = aiSiteImportInventory({
|
|
414
|
+
domain,
|
|
415
|
+
siteName: await siteNameOf(firestore, hostId),
|
|
416
|
+
ownership: {
|
|
417
|
+
method: passed.ownership.method,
|
|
418
|
+
provenAt: passed.ownership.provenAt,
|
|
419
|
+
detail: passed.ownership.detail
|
|
420
|
+
},
|
|
421
|
+
readAt: nowIso,
|
|
422
|
+
pages: crawled.pages.map(siteImportListedPage),
|
|
423
|
+
menu: siteImportMenu(crawled),
|
|
424
|
+
skipped: crawled.skipped.map((entry)=>({
|
|
425
|
+
url: entry.url,
|
|
426
|
+
reason: siteImportLeftOutReason(entry.reason)
|
|
427
|
+
})),
|
|
428
|
+
skippedOverflow: crawled.skippedOverflow,
|
|
429
|
+
stoppedBy: crawled.stoppedBy,
|
|
430
|
+
pageLimit: room.limit,
|
|
431
|
+
existingPages: room.used,
|
|
432
|
+
mintId
|
|
433
|
+
});
|
|
434
|
+
const ledger = aiSiteImportLedger(inventory);
|
|
435
|
+
const reviewId = await review('read', inventory, ledger);
|
|
436
|
+
const next = _extends({}, inventory, {
|
|
437
|
+
reviewId
|
|
438
|
+
});
|
|
439
|
+
return unspent({
|
|
440
|
+
items: ledger,
|
|
441
|
+
siteImport: next,
|
|
442
|
+
review: waveReview(aiSiteImportWaveMessage(next, ledger))
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (!state) return unspent({
|
|
446
|
+
failure: AI_SITE_IMPORT_COPY.badDomain
|
|
447
|
+
});
|
|
448
|
+
if (work.kind === 'done') return unspent();
|
|
449
|
+
// ── 2. Start a wave ──
|
|
450
|
+
if (work.kind === 'start-wave') {
|
|
451
|
+
var _context_org1;
|
|
452
|
+
const passed = await gate();
|
|
453
|
+
if (!('ownership' in passed)) return passed;
|
|
454
|
+
const wave = work.wave;
|
|
455
|
+
const pages = wave.slots.map((slot)=>state.pages.find((page)=>page.slot === slot)).filter((page)=>!!page);
|
|
456
|
+
const reads = new Map();
|
|
457
|
+
// Never past the plan: the room is read again, since pages may have been added since the site was read.
|
|
458
|
+
const room = await readRoom(firestore, {
|
|
459
|
+
hostId,
|
|
460
|
+
org: (_context_org1 = context.org) != null ? _context_org1 : null
|
|
461
|
+
});
|
|
462
|
+
let left = room.limit === null ? Number.POSITIVE_INFINITY : Math.max(0, room.limit - room.used);
|
|
463
|
+
const toRead = [];
|
|
464
|
+
for (const page of pages){
|
|
465
|
+
if (left > 0) {
|
|
466
|
+
toRead.push(page);
|
|
467
|
+
left -= 1;
|
|
468
|
+
} else {
|
|
469
|
+
reads.set(page.slot, 'plan-limit');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (toRead.length) {
|
|
473
|
+
const crawled = await crawl(_extends({
|
|
474
|
+
domain,
|
|
475
|
+
maxPages: toRead.length,
|
|
476
|
+
only: toRead.map((page)=>page.url),
|
|
477
|
+
timeBudgetMs: AI_SITE_IMPORT_WAVE_READ_MS
|
|
478
|
+
}, signal ? {
|
|
479
|
+
signal
|
|
480
|
+
} : {}), deps.crawlSeams);
|
|
481
|
+
if (crawled.stoppedBy === 'robots-unreachable') return unspent({
|
|
482
|
+
review: waveReview(AI_SITE_IMPORT_COPY.robots(domain))
|
|
483
|
+
});
|
|
484
|
+
// A read the clock cut short leaves its other pages for the next pass, never "unreadable".
|
|
485
|
+
for (const [slot, read] of siteImportWaveReads(toRead, crawled, nowIso, {
|
|
486
|
+
cutShort: crawled.stoppedBy === 'time'
|
|
487
|
+
})){
|
|
488
|
+
reads.set(slot, read);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
let ledger = withLeftOutRows(items, reads);
|
|
492
|
+
// A wave none of whose pages could be read builds no header for them.
|
|
493
|
+
const anyRead = [
|
|
494
|
+
...reads.values()
|
|
495
|
+
].some((read)=>typeof read !== 'string');
|
|
496
|
+
if (wave.frame && !anyRead && reads.size === pages.length) {
|
|
497
|
+
ledger = ledger.map((row)=>row.slot === AI_SITE_IMPORT_FRAME_SLOT && (row.status === 'pending' || row.status === 'running') ? _extends({}, row, {
|
|
498
|
+
status: 'skipped',
|
|
499
|
+
note: 'Not built: no page of this wave could be read.'
|
|
500
|
+
}) : row);
|
|
501
|
+
}
|
|
502
|
+
const started = aiSiteImportWaveStarted(state, wave.index, nowIso, reads);
|
|
503
|
+
await review('wave-started', started, ledger, wave.index);
|
|
504
|
+
const running = started.waves.find((entry)=>entry.index === wave.index);
|
|
505
|
+
return closing(started, ledger, running, unspent({
|
|
506
|
+
items: ledger
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
const wave = work.wave;
|
|
510
|
+
// ── 2b. The rest of a wave's read, where the clock cut it short ──
|
|
511
|
+
if (work.kind === 'read') {
|
|
512
|
+
const crawled = await crawl(_extends({
|
|
513
|
+
domain,
|
|
514
|
+
maxPages: work.pages.length,
|
|
515
|
+
only: work.pages.map((page)=>page.url),
|
|
516
|
+
timeBudgetMs: AI_SITE_IMPORT_WAVE_READ_MS
|
|
517
|
+
}, signal ? {
|
|
518
|
+
signal
|
|
519
|
+
} : {}), deps.crawlSeams);
|
|
520
|
+
if (crawled.stoppedBy === 'robots-unreachable') return unspent({
|
|
521
|
+
review: waveReview(AI_SITE_IMPORT_COPY.robots(domain))
|
|
522
|
+
});
|
|
523
|
+
const reads = siteImportWaveReads(work.pages, crawled, nowIso, {
|
|
524
|
+
cutShort: crawled.stoppedBy === 'time'
|
|
525
|
+
});
|
|
526
|
+
const ledger = withLeftOutRows(items, reads);
|
|
527
|
+
return closing(aiSiteImportPagesRead(state, reads), ledger, wave, unspent({
|
|
528
|
+
items: ledger
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
// ── 3. The header and footer ──
|
|
532
|
+
if (work.kind === 'frame') {
|
|
533
|
+
const runner = runnerFor('layout');
|
|
534
|
+
if (!runner) {
|
|
535
|
+
const item = {
|
|
536
|
+
slot: AI_SITE_IMPORT_FRAME_SLOT,
|
|
537
|
+
status: 'skipped',
|
|
538
|
+
note: AI_SITE_IMPORT_COPY.frameUnavailable
|
|
539
|
+
};
|
|
540
|
+
return closing(state, aiApplyJobItemRecord(items, item, 0, now), wave, unspent({
|
|
541
|
+
item
|
|
542
|
+
}));
|
|
543
|
+
}
|
|
544
|
+
const derived = aiSiteImportFrameJob(job, state, wave, items);
|
|
545
|
+
return runUnit(AI_SITE_IMPORT_FRAME_SLOT, runner, derived, state, null);
|
|
546
|
+
}
|
|
547
|
+
// ── 4a. A page's photos ──
|
|
548
|
+
if (work.kind === 'images') {
|
|
549
|
+
var _ref, _ref1;
|
|
550
|
+
var _work_page_read, _work_page_read1;
|
|
551
|
+
const photos = (signal == null ? void 0 : signal.aborted) ? {
|
|
552
|
+
brought: 0,
|
|
553
|
+
reused: 0,
|
|
554
|
+
skipped: (_ref = (_work_page_read = work.page.read) == null ? void 0 : _work_page_read.images.length) != null ? _ref : 0,
|
|
555
|
+
assets: []
|
|
556
|
+
} : await bringInSiteImportPhotos(_extends({
|
|
557
|
+
hostId,
|
|
558
|
+
uid: job.createdBy,
|
|
559
|
+
domain,
|
|
560
|
+
images: (_ref1 = (_work_page_read1 = work.page.read) == null ? void 0 : _work_page_read1.images) != null ? _ref1 : [],
|
|
561
|
+
deadlineAt: clock() + AI_SITE_IMPORT_PHOTOS_MS
|
|
562
|
+
}, signal ? {
|
|
563
|
+
signal
|
|
564
|
+
} : {}), _extends({
|
|
565
|
+
ingest: ingestOf()
|
|
566
|
+
}, deps.fetchImage ? {
|
|
567
|
+
fetch: deps.fetchImage
|
|
568
|
+
} : {}));
|
|
569
|
+
return unspent({
|
|
570
|
+
item: {
|
|
571
|
+
slot: work.page.slot,
|
|
572
|
+
status: 'running'
|
|
573
|
+
},
|
|
574
|
+
siteImport: aiSiteImportPhotosIn(state, work.page.slot, photos),
|
|
575
|
+
continue: true
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
// ── 4b. The page ──
|
|
579
|
+
if (work.kind === 'page') {
|
|
580
|
+
const runner = runnerFor('page');
|
|
581
|
+
if (!runner) {
|
|
582
|
+
const item = {
|
|
583
|
+
slot: work.page.slot,
|
|
584
|
+
status: 'failed',
|
|
585
|
+
failure: {
|
|
586
|
+
ours: true,
|
|
587
|
+
reason: 'unavailable',
|
|
588
|
+
message: AI_SITE_IMPORT_COPY.pageUnavailable
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
return closing(state, aiApplyJobItemRecord(items, item, 0, now), wave, unspent({
|
|
592
|
+
item
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
return runUnit(work.page.slot, runner, aiSiteImportPageJob(job, state, work.page, items), state, work.page);
|
|
596
|
+
}
|
|
597
|
+
// ── 5. Close the wave ──
|
|
598
|
+
return closing(state, items, wave, unspent());
|
|
599
|
+
/** One delegated pass: the unit's spend is the pass's, what it came to is its row. */ async function runUnit(slot, runner, derived, current, page) {
|
|
600
|
+
let outcome;
|
|
601
|
+
try {
|
|
602
|
+
outcome = await runner(_extends({}, context, {
|
|
603
|
+
job: derived
|
|
604
|
+
}));
|
|
605
|
+
} catch (error) {
|
|
606
|
+
var _ref;
|
|
607
|
+
// A provider that will answer later, a budget that ended, a cancel: the machine's.
|
|
608
|
+
if (aiUnitErrorRetryable(error) || ((_ref = signal == null ? void 0 : signal.aborted) != null ? _ref : false)) throw error;
|
|
609
|
+
console.error('ai site import unit threw', {
|
|
610
|
+
orgId: job.orgId,
|
|
611
|
+
jobId: job.$id,
|
|
612
|
+
slot,
|
|
613
|
+
error
|
|
614
|
+
});
|
|
615
|
+
const item = {
|
|
616
|
+
slot,
|
|
617
|
+
status: 'failed',
|
|
618
|
+
failure: {
|
|
619
|
+
ours: true,
|
|
620
|
+
reason: 'provider',
|
|
621
|
+
message: AI_SITE_IMPORT_COPY.pageNotWritten
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
return closing(current, aiApplyJobItemRecord(items, item, 0, now), wave, unspent({
|
|
625
|
+
item
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
const spent = aiUnitSpend(outcome);
|
|
629
|
+
const stopped = aiUnitFailure(slot, outcome);
|
|
630
|
+
if ((stopped == null ? void 0 : stopped.status) === 'running') return _extends({}, spent, {
|
|
631
|
+
outputs: [],
|
|
632
|
+
item: stopped,
|
|
633
|
+
siteImport: current,
|
|
634
|
+
continue: true
|
|
635
|
+
});
|
|
636
|
+
let item;
|
|
637
|
+
if (stopped) {
|
|
638
|
+
item = stopped;
|
|
639
|
+
} else if (page && derived.plan) {
|
|
640
|
+
// A page counts as in only when its sections are in it (AGL-3596).
|
|
641
|
+
const screen = derived.plan.screens[0];
|
|
642
|
+
const written = await pageWritten(firestore, {
|
|
643
|
+
hostId: hostId,
|
|
644
|
+
unitJobId: derived.$id,
|
|
645
|
+
screen,
|
|
646
|
+
draftId: page.draftId,
|
|
647
|
+
layoutLanguage: true
|
|
648
|
+
});
|
|
649
|
+
item = written ? {
|
|
650
|
+
slot,
|
|
651
|
+
status: 'succeeded',
|
|
652
|
+
outputs: spent.outputs.map((output)=>output.id)
|
|
653
|
+
} : {
|
|
654
|
+
slot,
|
|
655
|
+
status: 'failed',
|
|
656
|
+
failure: {
|
|
657
|
+
ours: true,
|
|
658
|
+
reason: 'step-failure',
|
|
659
|
+
message: AI_SITE_IMPORT_COPY.pageNotWritten
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
} else {
|
|
663
|
+
item = {
|
|
664
|
+
slot,
|
|
665
|
+
status: 'succeeded',
|
|
666
|
+
outputs: spent.outputs.map((output)=>output.id)
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const settledState = page ? aiSiteImportPageSettled(current, slot) : current;
|
|
670
|
+
return closing(settledState, aiApplyJobItemRecord(items, item, 0, now), wave, _extends({}, spent, {
|
|
671
|
+
outputs: item.status === 'succeeded' ? spent.outputs : [],
|
|
672
|
+
item
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
/** The home page's menu, as same-site paths in its order. */ export function siteImportMenu(crawled) {
|
|
678
|
+
var _crawled_pages_find, _ref;
|
|
679
|
+
const home = (_crawled_pages_find = crawled.pages.find((page)=>page.foundBy === 'home')) != null ? _crawled_pages_find : crawled.pages[0];
|
|
680
|
+
const paths = [];
|
|
681
|
+
for (const link of (_ref = home == null ? void 0 : home.navLinks) != null ? _ref : []){
|
|
682
|
+
try {
|
|
683
|
+
const url = new URL(link.url);
|
|
684
|
+
if (!crawled.origins.includes(url.origin)) continue;
|
|
685
|
+
const path = url.pathname.replace(/\/+$/, '') || '/';
|
|
686
|
+
if (!paths.includes(path)) paths.push(path);
|
|
687
|
+
} catch (unused) {
|
|
688
|
+
// A menu entry that is not an address names no page.
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return paths;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* What a wave's read came to for each page: the page read again, or why it
|
|
695
|
+
* is left out now. Matched by address; a page the site sent to another of its
|
|
696
|
+
* own addresses is matched in the order the wave asked for it.
|
|
697
|
+
*/ export function siteImportWaveReads(asked, crawled, at, options = {}) {
|
|
698
|
+
const reads = new Map();
|
|
699
|
+
const pathKey = (url)=>{
|
|
700
|
+
try {
|
|
701
|
+
const parsed = new URL(url);
|
|
702
|
+
return `${parsed.pathname.replace(/\/+$/, '') || '/'}${parsed.search}`;
|
|
703
|
+
} catch (unused) {
|
|
704
|
+
return url;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const pages = new Map(crawled.pages.map((page)=>[
|
|
708
|
+
pathKey(page.url),
|
|
709
|
+
page
|
|
710
|
+
]));
|
|
711
|
+
const skipped = new Map(crawled.skipped.map((entry)=>[
|
|
712
|
+
pathKey(entry.url),
|
|
713
|
+
entry.reason
|
|
714
|
+
]));
|
|
715
|
+
const unmatched = [
|
|
716
|
+
...crawled.pages
|
|
717
|
+
];
|
|
718
|
+
const take = (page)=>{
|
|
719
|
+
const index = unmatched.indexOf(page);
|
|
720
|
+
if (index !== -1) unmatched.splice(index, 1);
|
|
721
|
+
};
|
|
722
|
+
const pending = [];
|
|
723
|
+
for (const page of asked){
|
|
724
|
+
const found = pages.get(pathKey(page.url));
|
|
725
|
+
if (found) {
|
|
726
|
+
take(found);
|
|
727
|
+
reads.set(page.slot, siteImportReadOrEmpty(found, at));
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const reason = skipped.get(pathKey(page.url));
|
|
731
|
+
if (reason) {
|
|
732
|
+
reads.set(page.slot, siteImportLeftOutReason(reason));
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
pending.push(page);
|
|
736
|
+
}
|
|
737
|
+
for (const page of pending){
|
|
738
|
+
const found = unmatched.shift();
|
|
739
|
+
if (found) reads.set(page.slot, siteImportReadOrEmpty(found, at));
|
|
740
|
+
else if (!options.cutShort) reads.set(page.slot, 'unreadable');
|
|
741
|
+
}
|
|
742
|
+
return reads;
|
|
743
|
+
}
|
|
744
|
+
function siteImportReadOrEmpty(page, at) {
|
|
745
|
+
if (siteImportPageWords(page) < AI_SITE_IMPORT_MIN_WORDS && !page.images.length) return 'empty';
|
|
746
|
+
return siteImportPageRead(page, at);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* What an import asks before it exists and before each wave (AGL-3976): the
|
|
750
|
+
* release flag, a site to bring it into, a paid plan, the owner or an admin,
|
|
751
|
+
* and a CURRENT proof that the workspace owns the domain. Nothing is spent on
|
|
752
|
+
* a refusal, and the day's limit is the step's to count, once a wave runs.
|
|
753
|
+
*/ export function createAiSiteImportAdmission(deps = {}) {
|
|
754
|
+
var _deps_releaseFlagOn, _deps_ownership, _deps_memberRole;
|
|
755
|
+
const flagOn = (_deps_releaseFlagOn = deps.releaseFlagOn) != null ? _deps_releaseFlagOn : (orgId)=>isServerReleaseFlagOnForOrg(SITE_IMPORT_RELEASE_FLAG, orgId);
|
|
756
|
+
const ownershipOf = (_deps_ownership = deps.ownership) != null ? _deps_ownership : readOwnership;
|
|
757
|
+
const memberRole = (_deps_memberRole = deps.memberRole) != null ? _deps_memberRole : async (uid, orgId)=>{
|
|
758
|
+
var _ref;
|
|
759
|
+
var _member, _this;
|
|
760
|
+
return (_ref = (_this = await getOrgForUser(uid, orgId)) == null ? void 0 : (_member = _this.member) == null ? void 0 : _member.role) != null ? _ref : null;
|
|
761
|
+
};
|
|
762
|
+
return async (context)=>{
|
|
763
|
+
var _context_inputs_AI_SITE_IMPORT_DOMAIN_INPUT;
|
|
764
|
+
if (!await flagOn(context.orgId)) return {
|
|
765
|
+
status: 404,
|
|
766
|
+
error: 'Not found'
|
|
767
|
+
};
|
|
768
|
+
if (!context.hostId && !context.siteToCome) return {
|
|
769
|
+
status: 400,
|
|
770
|
+
error: AI_SITE_IMPORT_COPY.noSite
|
|
771
|
+
};
|
|
772
|
+
if (resolveEffectivePlan(context.org) === 'free') return {
|
|
773
|
+
status: 403,
|
|
774
|
+
error: AI_SITE_IMPORT_COPY.plan
|
|
775
|
+
};
|
|
776
|
+
const role = context.uid ? await memberRole(context.uid, context.orgId) : null;
|
|
777
|
+
if (role !== 'owner' && role !== 'admin') return {
|
|
778
|
+
status: 403,
|
|
779
|
+
error: AI_SITE_IMPORT_COPY.role
|
|
780
|
+
};
|
|
781
|
+
const { domain, error } = normalizeOwnershipDomain(String((_context_inputs_AI_SITE_IMPORT_DOMAIN_INPUT = context.inputs[AI_SITE_IMPORT_DOMAIN_INPUT]) != null ? _context_inputs_AI_SITE_IMPORT_DOMAIN_INPUT : ''));
|
|
782
|
+
if (!domain) return {
|
|
783
|
+
status: 400,
|
|
784
|
+
error: error != null ? error : AI_SITE_IMPORT_COPY.badDomain
|
|
785
|
+
};
|
|
786
|
+
const ownership = await ownershipOf(context.orgId, domain);
|
|
787
|
+
if (!(ownership == null ? void 0 : ownership.current)) return {
|
|
788
|
+
status: 403,
|
|
789
|
+
error: AI_SITE_IMPORT_COPY.ownership(domain)
|
|
790
|
+
};
|
|
791
|
+
return null;
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* The import kind's registrations (AGL-3976), by a call from `server.ts`,
|
|
796
|
+
* never by an import: its runner with each pass's least time, its pass bound
|
|
797
|
+
* and its admission.
|
|
798
|
+
*/ export function registerAiSiteImportJob(deps = {}) {
|
|
799
|
+
var _deps_runnerFor;
|
|
800
|
+
const runnerFor = (_deps_runnerFor = deps.runnerFor) != null ? _deps_runnerFor : aiJobStepRunnerFor;
|
|
801
|
+
registerAiJobStep('import', createAiJobImportStep(deps), {
|
|
802
|
+
minimumMs: AI_JOB_PAGE_STEP_MINIMUM_MS,
|
|
803
|
+
minimumMsFor: (job)=>aiSiteImportRunMinimumMs(job, runnerFor)
|
|
804
|
+
});
|
|
805
|
+
registerAiJobStepPasses('import', AI_SITE_IMPORT_MAX_PASSES);
|
|
806
|
+
registerAiJobAdmission('import', createAiSiteImportAdmission());
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
//# sourceMappingURL=ai-job-import-step.js.map
|