@xuda.io/ai_module 1.1.5613 → 1.1.5615
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/index.mjs +655 -10
- package/index_ms.mjs +28 -0
- package/index_msa.mjs +28 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -2085,6 +2085,10 @@ const create_image = async function (uid, prompt, model = 'gpt-image-1-mini', si
|
|
|
2085
2085
|
prompt,
|
|
2086
2086
|
size,
|
|
2087
2087
|
n,
|
|
2088
|
+
// Output is downscaled to width×height (default 256²) right below, so a
|
|
2089
|
+
// high-quality 1024² render is wasted. 'low' is indistinguishable at
|
|
2090
|
+
// thumbnail size and 2-8x cheaper. Tunable via _conf.chat_image_quality.
|
|
2091
|
+
quality: _conf.chat_image_quality || 'low',
|
|
2088
2092
|
});
|
|
2089
2093
|
report_ai_status(model);
|
|
2090
2094
|
} catch (err) {
|
|
@@ -9178,6 +9182,78 @@ export const is_spam_email = async function (uid, email, subject, account_profil
|
|
|
9178
9182
|
} catch (error) {}
|
|
9179
9183
|
};
|
|
9180
9184
|
|
|
9185
|
+
// AI screening for a marketplace publish. Receives plain-JSON context from
|
|
9186
|
+
// marketplace_module (zod schemas cannot cross the broker, so the prompt +
|
|
9187
|
+
// structured schema must live here). Returns a structured verdict across the
|
|
9188
|
+
// approval rule dimensions. The caller (marketplace_approval_review) merges
|
|
9189
|
+
// this with deterministic account-standing checks and decides the final stat.
|
|
9190
|
+
export const marketplace_review_item = async function (req) {
|
|
9191
|
+
const { uid, item_type, item = {}, account_context = {}, account_name = '', model = _conf.default_ai_model, account_profile_info } = req || {};
|
|
9192
|
+
|
|
9193
|
+
const check_schema = z.object({
|
|
9194
|
+
flag: z.boolean().describe('true if this concern is present'),
|
|
9195
|
+
severity: z.enum(['none', 'low', 'medium', 'high']).describe('how serious the concern is'),
|
|
9196
|
+
note: z.string().describe('one short sentence of evidence; empty string if no concern'),
|
|
9197
|
+
});
|
|
9198
|
+
|
|
9199
|
+
const response_format = z.object({
|
|
9200
|
+
decision: z.enum(['approve', 'reject', 'manual_review']).describe('approve = safe to publish; reject = clear violation; manual_review = uncertain, needs a human'),
|
|
9201
|
+
confidence: z.number().min(0).max(100).describe('confidence in the decision, 0-100'),
|
|
9202
|
+
checks: z.object({
|
|
9203
|
+
ai_bot_activity: check_schema.describe('automated/bot publishing: high velocity, near-duplicate items, low-effort generated bulk'),
|
|
9204
|
+
commercial_undisclosed: check_schema.describe('undisclosed commercial intent: external paid links, lead-gen, off-platform payment, advertising inconsistent with declared price/category'),
|
|
9205
|
+
harmful_code: check_schema.describe('destructive ops, data exfiltration, crypto-miners, obfuscation, malicious network calls in the submitted code'),
|
|
9206
|
+
suspicious_activity: check_schema.describe('mismatched metadata, hidden payloads, evasion patterns'),
|
|
9207
|
+
malformed_account_name: check_schema.describe('gibberish, homoglyphs, embedded URLs, or impersonation in the account name'),
|
|
9208
|
+
spam_low_quality: check_schema.describe('spam, filler, or low-quality content with no real utility'),
|
|
9209
|
+
impersonation_trademark: check_schema.describe('impersonates a brand/person or abuses a trademark'),
|
|
9210
|
+
prohibited_content: check_schema.describe('adult, illegal, hateful, or otherwise disallowed content'),
|
|
9211
|
+
plagiarism_duplicate: check_schema.describe('appears copied from or duplicates an existing item'),
|
|
9212
|
+
secrets_pii_leak: check_schema.describe('hardcoded API keys, tokens, credentials, or exposed PII in the code'),
|
|
9213
|
+
}),
|
|
9214
|
+
reasons: z.array(z.string()).describe('short user-facing lines explaining the decision (empty if approved cleanly)'),
|
|
9215
|
+
});
|
|
9216
|
+
|
|
9217
|
+
const prompt = `You are the xuda marketplace moderation reviewer. Decide whether the following ${item_type} should be published to the public marketplace.
|
|
9218
|
+
|
|
9219
|
+
Account name: ${account_name}
|
|
9220
|
+
Account context (signals): ${JSON.stringify(account_context)}
|
|
9221
|
+
|
|
9222
|
+
Item:
|
|
9223
|
+
- name: ${item.name || ''}
|
|
9224
|
+
- description: ${item.description || ''}
|
|
9225
|
+
- category: ${item.category || ''}
|
|
9226
|
+
- tags: ${JSON.stringify(item.tags || [])}
|
|
9227
|
+
- price: ${item.price ?? 0}
|
|
9228
|
+
- code/content summary: ${item.code_summary || '(none provided)'}
|
|
9229
|
+
|
|
9230
|
+
Evaluate every check honestly. Set decision to:
|
|
9231
|
+
- "approve" when no medium/high concerns and the item is genuine and useful.
|
|
9232
|
+
- "reject" when there is a clear high-severity violation (harmful code, leaked secrets, prohibited content, obvious impersonation/spam, or clear bot abuse).
|
|
9233
|
+
- "manual_review" when concerns are present but ambiguous, or you are not confident.
|
|
9234
|
+
Keep "reasons" concise, factual, and user-facing (no internal jargon).`;
|
|
9235
|
+
|
|
9236
|
+
const ret = await submit_chat_gpt_prompt({
|
|
9237
|
+
uid,
|
|
9238
|
+
prompt,
|
|
9239
|
+
model,
|
|
9240
|
+
response_format,
|
|
9241
|
+
metadata: { func: 'marketplace_review_item', item_type },
|
|
9242
|
+
account_profile_info,
|
|
9243
|
+
});
|
|
9244
|
+
|
|
9245
|
+
if (ret.code < 0) {
|
|
9246
|
+
return { code: ret.code, data: ret.data };
|
|
9247
|
+
}
|
|
9248
|
+
|
|
9249
|
+
try {
|
|
9250
|
+
const verdict = JSON.parse(ret.data);
|
|
9251
|
+
return { code: 1, data: { ...verdict, model } };
|
|
9252
|
+
} catch (err) {
|
|
9253
|
+
return { code: -1, data: `marketplace_review_item parse error: ${err.message}` };
|
|
9254
|
+
}
|
|
9255
|
+
};
|
|
9256
|
+
|
|
9181
9257
|
export const is_business_contact = async function (uid, email, name, subject, body, account_profile_info) {
|
|
9182
9258
|
let prompt = `detect if the email "${email}", subject "${subject}", name "${name}", or body "${body}" is business or personal. Return true if it is a business , false otherwise.
|
|
9183
9259
|
|
|
@@ -9403,9 +9479,9 @@ async function restoreFaceWithOpenAI(base64Image, ctx = {}) {
|
|
|
9403
9479
|
response = await client.images.edit({
|
|
9404
9480
|
model,
|
|
9405
9481
|
image: imageFile,
|
|
9406
|
-
input_fidelity: 'high',
|
|
9407
|
-
quality: '
|
|
9408
|
-
size: 'auto'
|
|
9482
|
+
input_fidelity: 'high', // identity preservation — keep high (don't degrade)
|
|
9483
|
+
quality: _conf.avatar_image_quality || 'low', // 'low' approved 2026-06-02 (A/B vs high/medium); identity held by input_fidelity:high. ~$0.08 vs $0.23 high. Override via _conf.avatar_image_quality.
|
|
9484
|
+
size: _conf.avatar_image_size || '1024x1024', // was 'auto' (could pick 1536 = ~1.5x output tokens); avatars are square
|
|
9409
9485
|
prompt:
|
|
9410
9486
|
'Restore this portrait photograph. Remove scratches, dust, grain, noise, faded color, and any age-related damage. Recover natural skin texture, sharpen face details, and balance lighting and color. Preserve the exact same person completely unchanged: same face shape, same eye shape and color, same nose, same mouth, same hairline and hair, same skin tone, same age, same gender, same facial expression, and same clothing. Do not alter, beautify, age, or de-age the person. Output a clean, naturally lit photograph of the same person.',
|
|
9411
9487
|
});
|
|
@@ -14256,7 +14332,7 @@ export const get_widget_embed_snippet = async function (req, job_id, headers) {
|
|
|
14256
14332
|
};
|
|
14257
14333
|
|
|
14258
14334
|
const _widget_signup_core = async function (req, job_id, headers, opts) {
|
|
14259
|
-
const { profile_id, email, name, widget_origin, google_sub, signup_method } = req;
|
|
14335
|
+
const { profile_id, email, name, widget_origin, google_sub, signup_method, visitor_lang } = req;
|
|
14260
14336
|
const [owner_uid_part, pid_part] = String(profile_id || '').split('.');
|
|
14261
14337
|
if (!owner_uid_part || !pid_part) return { code: -404, data: 'invalid_profile' };
|
|
14262
14338
|
|
|
@@ -14268,19 +14344,19 @@ const _widget_signup_core = async function (req, job_id, headers, opts) {
|
|
|
14268
14344
|
if (ap_doc.widget_enabled === false) return { code: -404, data: 'widget_disabled' };
|
|
14269
14345
|
|
|
14270
14346
|
const visitor_uid = await _widget_find_or_create_visitor(email, name, google_sub);
|
|
14271
|
-
return await _widget_signup_finalize(visitor_uid, email, name, widget_origin, signup_method, account_profile_info, canonical_owner_uid, canonical_profile_id, ap_doc, job_id, headers);
|
|
14347
|
+
return await _widget_signup_finalize(visitor_uid, email, name, widget_origin, signup_method, account_profile_info, canonical_owner_uid, canonical_profile_id, ap_doc, job_id, headers, visitor_lang);
|
|
14272
14348
|
};
|
|
14273
14349
|
|
|
14274
14350
|
export const widget_signup = async function (req, job_id, headers) {
|
|
14275
14351
|
try {
|
|
14276
|
-
let { profile_id, email, name, widget_origin } = req;
|
|
14352
|
+
let { profile_id, email, name, widget_origin, visitor_lang } = req;
|
|
14277
14353
|
if (typeof email !== 'string' || typeof name !== 'string') return { code: -1, data: 'invalid_input' };
|
|
14278
14354
|
email = email.toLowerCase().trim();
|
|
14279
14355
|
name = name.trim();
|
|
14280
14356
|
if (!email || !name) return { code: -1, data: 'invalid_input' };
|
|
14281
14357
|
if (!WIDGET_EMAIL_RE.test(email)) return { code: -1, data: 'invalid_email' };
|
|
14282
14358
|
|
|
14283
|
-
return await _widget_signup_core({ profile_id, email, name, widget_origin, signup_method: 'email' }, job_id, headers);
|
|
14359
|
+
return await _widget_signup_core({ profile_id, email, name, widget_origin, visitor_lang, signup_method: 'email' }, job_id, headers);
|
|
14284
14360
|
} catch (err) {
|
|
14285
14361
|
return { code: -1, data: err.message || String(err) };
|
|
14286
14362
|
}
|
|
@@ -14288,7 +14364,7 @@ export const widget_signup = async function (req, job_id, headers) {
|
|
|
14288
14364
|
|
|
14289
14365
|
export const widget_signup_google = async function (req, job_id, headers) {
|
|
14290
14366
|
try {
|
|
14291
|
-
const { profile_id, id_token, widget_origin } = req;
|
|
14367
|
+
const { profile_id, id_token, widget_origin, visitor_lang } = req;
|
|
14292
14368
|
if (!id_token || typeof id_token !== 'string') return { code: -1, data: 'missing_id_token' };
|
|
14293
14369
|
|
|
14294
14370
|
let payload;
|
|
@@ -14304,13 +14380,13 @@ export const widget_signup_google = async function (req, job_id, headers) {
|
|
|
14304
14380
|
const name = String(payload.name || payload.email?.split('@')[0] || 'Friend').trim();
|
|
14305
14381
|
if (!email) return { code: -1, data: 'no_email_in_token' };
|
|
14306
14382
|
|
|
14307
|
-
return await _widget_signup_core({ profile_id, email, name, widget_origin, google_sub: payload.sub, signup_method: 'google' }, job_id, headers);
|
|
14383
|
+
return await _widget_signup_core({ profile_id, email, name, widget_origin, visitor_lang, google_sub: payload.sub, signup_method: 'google' }, job_id, headers);
|
|
14308
14384
|
} catch (err) {
|
|
14309
14385
|
return { code: -1, data: err.message || String(err) };
|
|
14310
14386
|
}
|
|
14311
14387
|
};
|
|
14312
14388
|
|
|
14313
|
-
const _widget_signup_finalize = async function (visitor_uid, email, name, widget_origin, signup_method, account_profile_info, canonical_owner_uid, canonical_profile_id, ap_doc, job_id, headers) {
|
|
14389
|
+
const _widget_signup_finalize = async function (visitor_uid, email, name, widget_origin, signup_method, account_profile_info, canonical_owner_uid, canonical_profile_id, ap_doc, job_id, headers, visitor_lang) {
|
|
14314
14390
|
try {
|
|
14315
14391
|
let team_req_doc = await _widget_find_existing_team_req(visitor_uid, canonical_profile_id);
|
|
14316
14392
|
let assigned_uid;
|
|
@@ -14374,12 +14450,20 @@ const _widget_signup_finalize = async function (visitor_uid, email, name, widget
|
|
|
14374
14450
|
|
|
14375
14451
|
const widget_token = `wds_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
14376
14452
|
const now = Date.now();
|
|
14453
|
+
// Normalize visitor_lang to a short tag (e.g. "ja-JP" → "ja"). The
|
|
14454
|
+
// auto-reply agent reads this from the session to respond in the
|
|
14455
|
+
// visitor's language. Empty / missing → English fallback at reply time.
|
|
14456
|
+
const visitor_lang_base = String(visitor_lang || '')
|
|
14457
|
+
.toLowerCase()
|
|
14458
|
+
.split('-')[0]
|
|
14459
|
+
.slice(0, 8);
|
|
14377
14460
|
const session_doc = {
|
|
14378
14461
|
_id: widget_token,
|
|
14379
14462
|
docType: 'widget_session',
|
|
14380
14463
|
stat: 2,
|
|
14381
14464
|
state,
|
|
14382
14465
|
visitor_uid,
|
|
14466
|
+
visitor_lang: visitor_lang_base || '',
|
|
14383
14467
|
team_req_id: team_req_doc._id,
|
|
14384
14468
|
assigned_uid,
|
|
14385
14469
|
owner_uid: canonical_owner_uid,
|
|
@@ -14398,6 +14482,12 @@ const _widget_signup_finalize = async function (visitor_uid, email, name, widget
|
|
|
14398
14482
|
const assigned_avatar = assigned_info_ret?.data?.profile_picture || assigned_info_ret?.data?.profile_avatar || '';
|
|
14399
14483
|
const greeting = ap_doc?.widget_config?.greeting || WIDGET_DEFAULT_GREETING;
|
|
14400
14484
|
|
|
14485
|
+
// Surface the visitor's first name so the chat widget can personalize the
|
|
14486
|
+
// greeting on the page ("Hi {visitor_first_name}, I'm Valentina..."). The
|
|
14487
|
+
// widget reads this from the persisted session and replaces the
|
|
14488
|
+
// {visitor_name} placeholder client-side.
|
|
14489
|
+
const visitor_first_name = _widget_split_name(name).first_name || '';
|
|
14490
|
+
|
|
14401
14491
|
return {
|
|
14402
14492
|
code: 1,
|
|
14403
14493
|
data: {
|
|
@@ -14407,6 +14497,7 @@ const _widget_signup_finalize = async function (visitor_uid, email, name, widget
|
|
|
14407
14497
|
assigned_avatar,
|
|
14408
14498
|
greeting,
|
|
14409
14499
|
conversation_id,
|
|
14500
|
+
visitor_first_name,
|
|
14410
14501
|
signup_method: signup_method || 'email',
|
|
14411
14502
|
account_active: signup_method === 'google',
|
|
14412
14503
|
signin_url: signup_method === 'google' ? `https://${_conf.domain || 'xuda.ai'}/dashboard/login` : undefined,
|
|
@@ -14542,3 +14633,557 @@ export const widget_get_messages = async function (req, job_id, headers) {
|
|
|
14542
14633
|
return { code: -1, data: err.message || String(err) };
|
|
14543
14634
|
}
|
|
14544
14635
|
};
|
|
14636
|
+
|
|
14637
|
+
// ============================================================================
|
|
14638
|
+
// Static-website generation pipeline (helpers + orchestrator)
|
|
14639
|
+
// ============================================================================
|
|
14640
|
+
//
|
|
14641
|
+
// pick_image_source - three-bucket router (Pexels / gpt-image-1 / chatgpt-image-latest)
|
|
14642
|
+
// pexels_search - first consumer of _conf.pexels.api_key
|
|
14643
|
+
// estimate_static_website_cost - credit pre-check for create + modify
|
|
14644
|
+
// ingest_attachments - categorize user-uploaded files into the output tree + manifest
|
|
14645
|
+
// generate_site_files - planning prompt + per-page HTML + per-image source + SEO files
|
|
14646
|
+
// modify_site_files - modify-by-prompt over existing file set
|
|
14647
|
+
//
|
|
14648
|
+
// Image-source bucket order (cheapest -> most expensive): Pexels (free) ->
|
|
14649
|
+
// gpt-image-1 (cheap AI) -> chatgpt-image-latest (premium AI). Decision is
|
|
14650
|
+
// driven by membership/workspace tier and credit balance so a paid user
|
|
14651
|
+
// running low auto-degrades from premium to cheap rather than failing.
|
|
14652
|
+
|
|
14653
|
+
const LOW_CREDIT_THRESHOLD = _conf.static_website?.low_credit_threshold ?? 10;
|
|
14654
|
+
const HIGH_CREDIT_THRESHOLD = _conf.static_website?.high_credit_threshold ?? 100;
|
|
14655
|
+
|
|
14656
|
+
export const pick_image_source = (account_doc, credit_balance) => {
|
|
14657
|
+
const free_mem = (account_doc?.membership_plan || 'free') === 'free';
|
|
14658
|
+
const free_ws = (account_doc?.ai_workspace_plan || 'free_ai_workspace') === 'free_ai_workspace';
|
|
14659
|
+
const credits = Number(credit_balance ?? 0);
|
|
14660
|
+
const paid_tier = !free_mem || !free_ws;
|
|
14661
|
+
|
|
14662
|
+
// Bucket 1: no AI option (fully-free + no credits)
|
|
14663
|
+
if (!paid_tier && credits < LOW_CREDIT_THRESHOLD) return 'pexels';
|
|
14664
|
+
// Bucket 3: premium, only for paid users with a comfortable buffer
|
|
14665
|
+
if (paid_tier && credits >= HIGH_CREDIT_THRESHOLD) return 'chatgpt-image-latest';
|
|
14666
|
+
// Bucket 2: cheaper AI - free users with credits, paid users running low (auto-degrade)
|
|
14667
|
+
return 'gpt-image-1';
|
|
14668
|
+
};
|
|
14669
|
+
|
|
14670
|
+
export const pexels_search = async (query, n = 1) => {
|
|
14671
|
+
const api_key = _conf.pexels?.api_key;
|
|
14672
|
+
if (!api_key) throw new Error('pexels.api_key not configured');
|
|
14673
|
+
const url = `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=${Math.max(1, Math.min(80, Number(n) || 1))}`;
|
|
14674
|
+
const r = await fetch(url, { headers: { Authorization: api_key } });
|
|
14675
|
+
if (!r.ok) throw new Error(`pexels search failed: ${r.status}`);
|
|
14676
|
+
const j = await r.json();
|
|
14677
|
+
const photos = (j.photos || []).slice(0, n);
|
|
14678
|
+
return Promise.all(
|
|
14679
|
+
photos.map(async (p) => {
|
|
14680
|
+
const img = await fetch(p.src.large || p.src.medium || p.src.original);
|
|
14681
|
+
if (!img.ok) throw new Error(`pexels fetch failed: ${img.status}`);
|
|
14682
|
+
const buf = Buffer.from(await img.arrayBuffer());
|
|
14683
|
+
return {
|
|
14684
|
+
content_b64: buf.toString('base64'),
|
|
14685
|
+
content_type: 'image/jpeg',
|
|
14686
|
+
src_url: p.url,
|
|
14687
|
+
photographer: p.photographer,
|
|
14688
|
+
photographer_url: p.photographer_url,
|
|
14689
|
+
};
|
|
14690
|
+
}),
|
|
14691
|
+
);
|
|
14692
|
+
};
|
|
14693
|
+
|
|
14694
|
+
// Expected per-tier work for the credit estimator. Real generated count is
|
|
14695
|
+
// usually a fraction of the hard cap (page_count_limit in PLAN_OBJ).
|
|
14696
|
+
const EXPECTED_STATIC_WEBSITE_WORK = {
|
|
14697
|
+
landing: { pages: 1, imgs_per_page: 2, seo: 0, css: 1 },
|
|
14698
|
+
full: { pages: 8, imgs_per_page: 3, seo: 0, css: 1 },
|
|
14699
|
+
seo: { pages: 25, imgs_per_page: 3, seo: 2, css: 1 },
|
|
14700
|
+
enterprise: { pages: 100, imgs_per_page: 3, seo: 2, css: 1 },
|
|
14701
|
+
};
|
|
14702
|
+
|
|
14703
|
+
const _image_price = (image_source) => {
|
|
14704
|
+
if (image_source === 'pexels') return 0;
|
|
14705
|
+
const m = _conf.ai_models?.[image_source];
|
|
14706
|
+
return m?.price ?? Math.max(m?.input ?? 0, m?.output ?? 0) ?? 5;
|
|
14707
|
+
};
|
|
14708
|
+
|
|
14709
|
+
const _text_price = () => {
|
|
14710
|
+
const k = _conf.default_ai_model || 'gpt-5.4-mini';
|
|
14711
|
+
const m = _conf.ai_models?.[k];
|
|
14712
|
+
return m?.price ?? 1;
|
|
14713
|
+
};
|
|
14714
|
+
|
|
14715
|
+
export const estimate_static_website_cost = (req) => {
|
|
14716
|
+
const plan_tier = req?.plan_tier;
|
|
14717
|
+
const image_source = req?.image_source || 'gpt-image-1';
|
|
14718
|
+
const mode = req?.mode || 'create';
|
|
14719
|
+
const current_page_count = Number(req?.current_page_count || 0);
|
|
14720
|
+
|
|
14721
|
+
const W = EXPECTED_STATIC_WEBSITE_WORK[plan_tier] || EXPECTED_STATIC_WEBSITE_WORK.landing;
|
|
14722
|
+
const pages =
|
|
14723
|
+
mode === 'modify'
|
|
14724
|
+
? Math.max(1, Math.ceil(current_page_count * 0.3)) // modify touches ~30% by default
|
|
14725
|
+
: W.pages;
|
|
14726
|
+
const text_calls = 1 /* site plan */ + W.css + pages /* per-page HTML */ + W.seo;
|
|
14727
|
+
const image_calls = pages * W.imgs_per_page;
|
|
14728
|
+
|
|
14729
|
+
const text_unit = _text_price();
|
|
14730
|
+
const image_unit = _image_price(image_source);
|
|
14731
|
+
const credits_needed = text_calls * text_unit + image_calls * image_unit;
|
|
14732
|
+
|
|
14733
|
+
return {
|
|
14734
|
+
credits_needed,
|
|
14735
|
+
breakdown: { text_calls, text_unit, image_calls, image_unit, pages, mode },
|
|
14736
|
+
image_source,
|
|
14737
|
+
};
|
|
14738
|
+
};
|
|
14739
|
+
|
|
14740
|
+
// ── Attachment ingestion ────────────────────────────────────────────────
|
|
14741
|
+
//
|
|
14742
|
+
// Shape: [{ filename, mime_type, drive_path? | content_b64? | url? }]
|
|
14743
|
+
// At least one source field must be present. Files land in the deployed site
|
|
14744
|
+
// at a stable /uploads/{img,docs,text,data,files}/{slug} path so the LLM can
|
|
14745
|
+
// reference them by URL in generated HTML.
|
|
14746
|
+
|
|
14747
|
+
const _categorize_attachment = (att) => {
|
|
14748
|
+
const mt = (att?.mime_type || '').toLowerCase();
|
|
14749
|
+
if (mt.startsWith('image/')) return 'image';
|
|
14750
|
+
if (mt === 'application/pdf') return 'pdf';
|
|
14751
|
+
if (mt === 'text/csv' || mt === 'application/vnd.ms-excel') return 'data';
|
|
14752
|
+
if (mt === 'application/json' || mt === 'text/markdown' || mt.startsWith('text/')) return 'text';
|
|
14753
|
+
return 'binary';
|
|
14754
|
+
};
|
|
14755
|
+
|
|
14756
|
+
const _load_attachment_bytes = async (att) => {
|
|
14757
|
+
if (att.content_b64) return Buffer.from(att.content_b64, 'base64');
|
|
14758
|
+
if (att.drive_path) {
|
|
14759
|
+
const abs = path.join(_conf.studio_drive_path, att.drive_path);
|
|
14760
|
+
return await readFile(abs);
|
|
14761
|
+
}
|
|
14762
|
+
if (att.url) {
|
|
14763
|
+
const r = await fetch(att.url);
|
|
14764
|
+
if (!r.ok) throw new Error(`attachment fetch ${att.url}: ${r.status}`);
|
|
14765
|
+
return Buffer.from(await r.arrayBuffer());
|
|
14766
|
+
}
|
|
14767
|
+
throw new Error(`attachment ${att.filename} has no source (drive_path | content_b64 | url)`);
|
|
14768
|
+
};
|
|
14769
|
+
|
|
14770
|
+
export const ingest_attachments = async (attachments = []) => {
|
|
14771
|
+
const out = { manifest: [], file_entries: [], text_corpus: [] };
|
|
14772
|
+
for (const att of attachments) {
|
|
14773
|
+
if (!att || !att.filename) continue;
|
|
14774
|
+
const kind = _categorize_attachment(att);
|
|
14775
|
+
let bytes;
|
|
14776
|
+
try {
|
|
14777
|
+
bytes = await _load_attachment_bytes(att);
|
|
14778
|
+
} catch (err) {
|
|
14779
|
+
console.error('[ingest_attachments] skip', att.filename, err.message);
|
|
14780
|
+
continue;
|
|
14781
|
+
}
|
|
14782
|
+
const slug = String(att.filename)
|
|
14783
|
+
.replace(/[^a-z0-9.-]/gi, '_')
|
|
14784
|
+
.toLowerCase();
|
|
14785
|
+
let public_path;
|
|
14786
|
+
if (kind === 'image') public_path = `/uploads/img/${slug}`;
|
|
14787
|
+
else if (kind === 'pdf') public_path = `/uploads/docs/${slug}`;
|
|
14788
|
+
else if (kind === 'text') public_path = `/uploads/text/${slug}`;
|
|
14789
|
+
else if (kind === 'data') public_path = `/uploads/data/${slug}`;
|
|
14790
|
+
else public_path = `/uploads/files/${slug}`;
|
|
14791
|
+
|
|
14792
|
+
out.file_entries.push({
|
|
14793
|
+
path: public_path,
|
|
14794
|
+
content_b64: bytes.toString('base64'),
|
|
14795
|
+
content_type: att.mime_type || 'application/octet-stream',
|
|
14796
|
+
});
|
|
14797
|
+
|
|
14798
|
+
const manifest_entry = { filename: att.filename, kind, public_path };
|
|
14799
|
+
if (kind === 'text' || kind === 'data') {
|
|
14800
|
+
const text = bytes.toString('utf8').slice(0, 8000);
|
|
14801
|
+
manifest_entry.excerpt = text;
|
|
14802
|
+
out.text_corpus.push({ filename: att.filename, text });
|
|
14803
|
+
}
|
|
14804
|
+
if (kind === 'image') {
|
|
14805
|
+
manifest_entry.usage = `Embed via <img src="${public_path}" alt="..."> on relevant pages.`;
|
|
14806
|
+
}
|
|
14807
|
+
out.manifest.push(manifest_entry);
|
|
14808
|
+
}
|
|
14809
|
+
return out;
|
|
14810
|
+
};
|
|
14811
|
+
|
|
14812
|
+
// ── Generation orchestrator ─────────────────────────────────────────────
|
|
14813
|
+
//
|
|
14814
|
+
// generate_site_files runs the prompt -> plan -> pages -> images -> SEO chain.
|
|
14815
|
+
// Returns a flat [{ path, content_b64, content_type }] list ready for
|
|
14816
|
+
// domains_module.deploy_site.
|
|
14817
|
+
|
|
14818
|
+
const _safe_json_parse = (s) => {
|
|
14819
|
+
if (!s || typeof s !== 'string') return null;
|
|
14820
|
+
// The LLM sometimes wraps JSON in ```json fences despite instructions.
|
|
14821
|
+
const cleaned = s
|
|
14822
|
+
.trim()
|
|
14823
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
14824
|
+
.replace(/\s*```\s*$/i, '');
|
|
14825
|
+
try {
|
|
14826
|
+
return JSON.parse(cleaned);
|
|
14827
|
+
} catch (_) {
|
|
14828
|
+
return null;
|
|
14829
|
+
}
|
|
14830
|
+
};
|
|
14831
|
+
|
|
14832
|
+
const _chat = async (uid, prompt, account_profile_info, metadata) => {
|
|
14833
|
+
const ret = await submit_chat_gpt_prompt({
|
|
14834
|
+
uid,
|
|
14835
|
+
prompt,
|
|
14836
|
+
model: _conf.default_ai_model,
|
|
14837
|
+
metadata: metadata || {},
|
|
14838
|
+
account_profile_info,
|
|
14839
|
+
});
|
|
14840
|
+
if (ret.code < 0) throw new Error(`LLM call failed: ${ret.data}`);
|
|
14841
|
+
return ret.data || '';
|
|
14842
|
+
};
|
|
14843
|
+
|
|
14844
|
+
const _site_planning_prompt = (user_prompt, attachments_manifest, plan_tier, page_count_limit) => {
|
|
14845
|
+
const W = EXPECTED_STATIC_WEBSITE_WORK[plan_tier] || EXPECTED_STATIC_WEBSITE_WORK.landing;
|
|
14846
|
+
const default_pages = Math.min(W.pages, page_count_limit);
|
|
14847
|
+
return `You are planning a static website. Output STRICT JSON only, no prose, no markdown fences.
|
|
14848
|
+
|
|
14849
|
+
User brief:
|
|
14850
|
+
${user_prompt}
|
|
14851
|
+
|
|
14852
|
+
User-uploaded files (manifest). The generated pages MUST use these where appropriate: embed images via <img src="$public_path">, quote / paraphrase text excerpts where they fit, link to PDFs, structure pages around data when relevant.
|
|
14853
|
+
${JSON.stringify(attachments_manifest, null, 2)}
|
|
14854
|
+
|
|
14855
|
+
Hard rules:
|
|
14856
|
+
- Hosting tier: ${plan_tier}. Maximum ${page_count_limit} pages total. Aim for ${default_pages} pages unless the brief clearly implies more.
|
|
14857
|
+
- Each page has a slug (kebab-case path component, "" for home), title, and an ordered list of sections.
|
|
14858
|
+
- Each page also lists image_specs: image slots needed on that page. For each slot, decide source: 'attachment' (when an uploaded image fits) OR 'generate' (when one must be created).
|
|
14859
|
+
- For 'generate' slots, write a concise visual prompt AND a Pexels-friendly query string.
|
|
14860
|
+
- For 'attachment' slots, set attachment_path to one of the public_paths from the manifest.
|
|
14861
|
+
- style_directive is one paragraph describing the desired visual style (palette, typography, density) used to generate the global stylesheet.
|
|
14862
|
+
|
|
14863
|
+
Schema:
|
|
14864
|
+
{
|
|
14865
|
+
"pages": [
|
|
14866
|
+
{
|
|
14867
|
+
"slug": "string (empty for home)",
|
|
14868
|
+
"title": "string",
|
|
14869
|
+
"sections": [{"kind": "prose|image|quote|data_table|callout", "...": "..."}],
|
|
14870
|
+
"image_specs": [
|
|
14871
|
+
{"slot": "kebab-slug", "source": "generate|attachment", "prompt": "...", "query": "...", "attachment_path": "/uploads/...", "aspect": "16/10|1/1|3/2"}
|
|
14872
|
+
]
|
|
14873
|
+
}
|
|
14874
|
+
],
|
|
14875
|
+
"style_directive": "string"
|
|
14876
|
+
}
|
|
14877
|
+
|
|
14878
|
+
Respond with ONLY the JSON object.`;
|
|
14879
|
+
};
|
|
14880
|
+
|
|
14881
|
+
const _page_html_prompt = (page, style_directive, attachments_manifest) => `Generate a single static HTML page. Output ONLY the HTML, no markdown fences, no commentary.
|
|
14882
|
+
|
|
14883
|
+
Page brief:
|
|
14884
|
+
${JSON.stringify(page, null, 2)}
|
|
14885
|
+
|
|
14886
|
+
Global style directive (matches /style.css that's already written):
|
|
14887
|
+
${style_directive}
|
|
14888
|
+
|
|
14889
|
+
Attachments available (use these public_paths verbatim when referenced):
|
|
14890
|
+
${JSON.stringify(attachments_manifest, null, 2)}
|
|
14891
|
+
|
|
14892
|
+
Hard rules:
|
|
14893
|
+
- DOCTYPE html, lang="en", responsive viewport meta.
|
|
14894
|
+
- Link to /style.css.
|
|
14895
|
+
- Reference images from /img/{slot}.{ext} for 'generate' slots and from their public_path for 'attachment' slots, exactly as listed in image_specs.
|
|
14896
|
+
- Semantic HTML5 (header, main, section, footer). No inline styles unless absolutely necessary.
|
|
14897
|
+
- No external CDNs, no JavaScript.
|
|
14898
|
+
- Title from page.title.
|
|
14899
|
+
|
|
14900
|
+
Respond with ONLY the raw HTML document.`;
|
|
14901
|
+
|
|
14902
|
+
const _css_prompt = (style_directive, plan_tier) => `Generate ONE global stylesheet for a ${plan_tier}-tier static website. Output ONLY the CSS, no markdown fences, no commentary.
|
|
14903
|
+
|
|
14904
|
+
Style directive:
|
|
14905
|
+
${style_directive}
|
|
14906
|
+
|
|
14907
|
+
Hard rules:
|
|
14908
|
+
- Modern CSS only (no preprocessors). Use CSS variables for palette.
|
|
14909
|
+
- Responsive: mobile-first, breakpoints at 640px and 980px.
|
|
14910
|
+
- Typography: choose a serif heading + sans-serif body OR all sans, consistent with the directive.
|
|
14911
|
+
- Include base resets, layout containers, header, footer, basic image styling (max-width 100%, rounded), button styles.
|
|
14912
|
+
- Keep under 6 KB.
|
|
14913
|
+
|
|
14914
|
+
Respond with ONLY the raw CSS text.`;
|
|
14915
|
+
|
|
14916
|
+
const _sitemap_xml = (pages, hostname) => {
|
|
14917
|
+
const base = `https://${hostname}`;
|
|
14918
|
+
const urls = pages
|
|
14919
|
+
.map((p) => {
|
|
14920
|
+
const loc = p.slug ? `${base}/${p.slug}/` : `${base}/`;
|
|
14921
|
+
return ` <url><loc>${loc}</loc></url>`;
|
|
14922
|
+
})
|
|
14923
|
+
.join('\n');
|
|
14924
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
|
|
14925
|
+
};
|
|
14926
|
+
|
|
14927
|
+
const _robots_txt = (hostname) => `User-agent: *\nAllow: /\nSitemap: https://${hostname}/sitemap.xml\n`;
|
|
14928
|
+
|
|
14929
|
+
// Picks an extension for a generated image filename from its content-type.
|
|
14930
|
+
const _image_ext = (content_type) => {
|
|
14931
|
+
if (!content_type) return 'png';
|
|
14932
|
+
if (/webp/i.test(content_type)) return 'webp';
|
|
14933
|
+
if (/jpe?g/i.test(content_type)) return 'jpg';
|
|
14934
|
+
if (/png/i.test(content_type)) return 'png';
|
|
14935
|
+
if (/svg/i.test(content_type)) return 'svg';
|
|
14936
|
+
return 'png';
|
|
14937
|
+
};
|
|
14938
|
+
|
|
14939
|
+
export const generate_site_files = async (req) => {
|
|
14940
|
+
try {
|
|
14941
|
+
const { uid, prompt, attachments, plan_tier, page_count_limit, image_source, account_profile_info, hostname = 'sites.xuda.io' } = req;
|
|
14942
|
+
if (!uid || !prompt) return { code: -1, data: 'uid + prompt required' };
|
|
14943
|
+
if (!plan_tier) return { code: -1, data: 'plan_tier required' };
|
|
14944
|
+
|
|
14945
|
+
// 1. Ingest attachments (copies them into the output file list verbatim)
|
|
14946
|
+
const ingestion = await ingest_attachments(attachments || []);
|
|
14947
|
+
|
|
14948
|
+
// 2. Plan the site
|
|
14949
|
+
const plan_text = await _chat(uid, _site_planning_prompt(prompt, ingestion.manifest, plan_tier, page_count_limit), account_profile_info, { stage: 'static_website_plan', plan_tier });
|
|
14950
|
+
const plan = _safe_json_parse(plan_text);
|
|
14951
|
+
if (!plan || !Array.isArray(plan.pages) || !plan.pages.length) {
|
|
14952
|
+
return { code: -2, data: 'planning JSON malformed', plan_text };
|
|
14953
|
+
}
|
|
14954
|
+
|
|
14955
|
+
// 3. Hard-cap page count
|
|
14956
|
+
plan.pages = plan.pages.slice(0, Math.max(1, Math.min(page_count_limit, plan.pages.length)));
|
|
14957
|
+
|
|
14958
|
+
// 4. Global CSS
|
|
14959
|
+
const css = await _chat(uid, _css_prompt(plan.style_directive || '', plan_tier), account_profile_info, { stage: 'static_website_css', plan_tier });
|
|
14960
|
+
|
|
14961
|
+
// 5. Per-page HTML
|
|
14962
|
+
const html_files = [];
|
|
14963
|
+
for (const page of plan.pages) {
|
|
14964
|
+
const html = await _chat(uid, _page_html_prompt(page, plan.style_directive || '', ingestion.manifest), account_profile_info, { stage: 'static_website_page', plan_tier, slug: page.slug || '' });
|
|
14965
|
+
const rel = page.slug ? `/${page.slug}/index.html` : '/index.html';
|
|
14966
|
+
html_files.push({
|
|
14967
|
+
path: rel,
|
|
14968
|
+
content_b64: Buffer.from(html, 'utf8').toString('base64'),
|
|
14969
|
+
content_type: 'text/html',
|
|
14970
|
+
});
|
|
14971
|
+
}
|
|
14972
|
+
|
|
14973
|
+
// 6. Per-image generation
|
|
14974
|
+
const image_files = [];
|
|
14975
|
+
for (const page of plan.pages) {
|
|
14976
|
+
const specs = Array.isArray(page.image_specs) ? page.image_specs : [];
|
|
14977
|
+
for (const spec of specs) {
|
|
14978
|
+
try {
|
|
14979
|
+
if (spec.source === 'attachment') {
|
|
14980
|
+
// already in ingestion.file_entries — nothing to add
|
|
14981
|
+
continue;
|
|
14982
|
+
}
|
|
14983
|
+
if (image_source === 'pexels') {
|
|
14984
|
+
const [img] = await pexels_search(spec.query || spec.prompt || page.title, 1);
|
|
14985
|
+
if (!img) continue;
|
|
14986
|
+
image_files.push({
|
|
14987
|
+
path: `/img/${spec.slot}.jpg`,
|
|
14988
|
+
content_b64: img.content_b64,
|
|
14989
|
+
content_type: 'image/jpeg',
|
|
14990
|
+
});
|
|
14991
|
+
} else {
|
|
14992
|
+
const b64 = await create_image(
|
|
14993
|
+
uid,
|
|
14994
|
+
spec.prompt || spec.query || page.title,
|
|
14995
|
+
image_source, // 'gpt-image-1' | 'chatgpt-image-latest'
|
|
14996
|
+
'1024x1024',
|
|
14997
|
+
1,
|
|
14998
|
+
1024,
|
|
14999
|
+
1024, // full-resolution output
|
|
15000
|
+
{ kind: 'static_website', slot: spec.slot },
|
|
15001
|
+
account_profile_info,
|
|
15002
|
+
);
|
|
15003
|
+
if (!b64) continue;
|
|
15004
|
+
image_files.push({
|
|
15005
|
+
path: `/img/${spec.slot}.png`,
|
|
15006
|
+
content_b64: b64,
|
|
15007
|
+
content_type: 'image/png',
|
|
15008
|
+
});
|
|
15009
|
+
}
|
|
15010
|
+
} catch (err) {
|
|
15011
|
+
console.error('[generate_site_files] image fail', spec.slot, err.message);
|
|
15012
|
+
}
|
|
15013
|
+
}
|
|
15014
|
+
}
|
|
15015
|
+
|
|
15016
|
+
// 7. SEO files (seo + enterprise tiers only)
|
|
15017
|
+
const seo_files = [];
|
|
15018
|
+
if (plan_tier === 'seo' || plan_tier === 'enterprise') {
|
|
15019
|
+
seo_files.push({
|
|
15020
|
+
path: '/sitemap.xml',
|
|
15021
|
+
content_b64: Buffer.from(_sitemap_xml(plan.pages, hostname), 'utf8').toString('base64'),
|
|
15022
|
+
content_type: 'application/xml',
|
|
15023
|
+
});
|
|
15024
|
+
seo_files.push({
|
|
15025
|
+
path: '/robots.txt',
|
|
15026
|
+
content_b64: Buffer.from(_robots_txt(hostname), 'utf8').toString('base64'),
|
|
15027
|
+
content_type: 'text/plain',
|
|
15028
|
+
});
|
|
15029
|
+
}
|
|
15030
|
+
|
|
15031
|
+
// 8. Compose final flat file list
|
|
15032
|
+
const css_file = {
|
|
15033
|
+
path: '/style.css',
|
|
15034
|
+
content_b64: Buffer.from(css, 'utf8').toString('base64'),
|
|
15035
|
+
content_type: 'text/css',
|
|
15036
|
+
};
|
|
15037
|
+
const files = [css_file, ...html_files, ...image_files, ...seo_files, ...ingestion.file_entries];
|
|
15038
|
+
|
|
15039
|
+
return {
|
|
15040
|
+
code: 1,
|
|
15041
|
+
data: {
|
|
15042
|
+
files,
|
|
15043
|
+
plan,
|
|
15044
|
+
manifest: ingestion.manifest,
|
|
15045
|
+
stats: {
|
|
15046
|
+
page_count: plan.pages.length,
|
|
15047
|
+
image_count: image_files.length,
|
|
15048
|
+
attachment_count: ingestion.file_entries.length,
|
|
15049
|
+
},
|
|
15050
|
+
},
|
|
15051
|
+
};
|
|
15052
|
+
} catch (err) {
|
|
15053
|
+
return { code: -400, data: err.message };
|
|
15054
|
+
}
|
|
15055
|
+
};
|
|
15056
|
+
|
|
15057
|
+
// ── Modify orchestrator ────────────────────────────────────────────────
|
|
15058
|
+
//
|
|
15059
|
+
// Loads current files, asks LLM what to change, writes back only the deltas.
|
|
15060
|
+
// Image source stays pinned to whatever was chosen at creation - modify does
|
|
15061
|
+
// NOT silently upgrade Pexels -> AI (that would surprise free users).
|
|
15062
|
+
|
|
15063
|
+
const _modify_prompt = (user_prompt, current_files_index, attachments_manifest) => `You are editing an existing static website. Output STRICT JSON only, no prose, no markdown fences.
|
|
15064
|
+
|
|
15065
|
+
User request:
|
|
15066
|
+
${user_prompt}
|
|
15067
|
+
|
|
15068
|
+
Current file index (path + size + content-type — NOT full content; ask the user to re-attach if you need to read a file's body):
|
|
15069
|
+
${JSON.stringify(current_files_index, null, 2)}
|
|
15070
|
+
|
|
15071
|
+
New attachments uploaded with this request:
|
|
15072
|
+
${JSON.stringify(attachments_manifest, null, 2)}
|
|
15073
|
+
|
|
15074
|
+
For each change you want to make, emit one entry in "changes":
|
|
15075
|
+
{ "path": "/index.html", "action": "replace", "new_content": "...full new file content..." }
|
|
15076
|
+
{ "path": "/about/index.html", "action": "create", "new_content": "..." }
|
|
15077
|
+
{ "path": "/old-page/index.html", "action": "delete" }
|
|
15078
|
+
|
|
15079
|
+
Image regeneration: emit { "image_change": { "path": "/img/hero.png", "new_prompt": "..." } } and the orchestrator will re-render via the locked image source.
|
|
15080
|
+
|
|
15081
|
+
Hard rules:
|
|
15082
|
+
- Touch only files you actually need to change. Do NOT rewrite the whole site.
|
|
15083
|
+
- new_content for HTML files: full document, DOCTYPE through </html>. Link /style.css unchanged unless explicitly told to restyle.
|
|
15084
|
+
- Do NOT change file paths for unchanged pages.
|
|
15085
|
+
|
|
15086
|
+
Schema:
|
|
15087
|
+
{ "changes": [...], "summary": "one sentence" }
|
|
15088
|
+
|
|
15089
|
+
Respond with ONLY the JSON object.`;
|
|
15090
|
+
|
|
15091
|
+
export const modify_site_files = async (req) => {
|
|
15092
|
+
try {
|
|
15093
|
+
const { uid, current_files, prompt, attachments, plan_tier, image_source, account_profile_info } = req;
|
|
15094
|
+
if (!uid || !prompt) return { code: -1, data: 'uid + prompt required' };
|
|
15095
|
+
if (!Array.isArray(current_files) || !current_files.length) return { code: -1, data: 'current_files required' };
|
|
15096
|
+
|
|
15097
|
+
const ingestion = await ingest_attachments(attachments || []);
|
|
15098
|
+
const index = current_files.map((f) => ({
|
|
15099
|
+
path: f.path,
|
|
15100
|
+
content_type: f.content_type,
|
|
15101
|
+
size_b64: (f.content_b64 || '').length,
|
|
15102
|
+
}));
|
|
15103
|
+
|
|
15104
|
+
const diff_text = await _chat(uid, _modify_prompt(prompt, index, ingestion.manifest), account_profile_info, { stage: 'static_website_modify', plan_tier });
|
|
15105
|
+
const diff = _safe_json_parse(diff_text);
|
|
15106
|
+
if (!diff || !Array.isArray(diff.changes)) {
|
|
15107
|
+
return { code: -2, data: 'modify JSON malformed', diff_text };
|
|
15108
|
+
}
|
|
15109
|
+
|
|
15110
|
+
const changed_paths = [];
|
|
15111
|
+
const updated_files = [...current_files];
|
|
15112
|
+
|
|
15113
|
+
const _find = (p) => updated_files.findIndex((f) => f.path === p);
|
|
15114
|
+
|
|
15115
|
+
for (const ch of diff.changes) {
|
|
15116
|
+
// Re-rendered image
|
|
15117
|
+
if (ch.image_change && ch.image_change.path && ch.image_change.new_prompt) {
|
|
15118
|
+
const ip = ch.image_change.path;
|
|
15119
|
+
try {
|
|
15120
|
+
if (image_source === 'pexels') {
|
|
15121
|
+
const [img] = await pexels_search(ch.image_change.new_prompt, 1);
|
|
15122
|
+
if (img) {
|
|
15123
|
+
const entry = { path: ip, content_b64: img.content_b64, content_type: 'image/jpeg' };
|
|
15124
|
+
const idx = _find(ip);
|
|
15125
|
+
if (idx >= 0) updated_files[idx] = entry;
|
|
15126
|
+
else updated_files.push(entry);
|
|
15127
|
+
changed_paths.push(ip);
|
|
15128
|
+
}
|
|
15129
|
+
} else {
|
|
15130
|
+
const b64 = await create_image(uid, ch.image_change.new_prompt, image_source, '1024x1024', 1, 1024, 1024, { kind: 'static_website_modify' }, account_profile_info);
|
|
15131
|
+
if (b64) {
|
|
15132
|
+
const entry = { path: ip, content_b64: b64, content_type: 'image/png' };
|
|
15133
|
+
const idx = _find(ip);
|
|
15134
|
+
if (idx >= 0) updated_files[idx] = entry;
|
|
15135
|
+
else updated_files.push(entry);
|
|
15136
|
+
changed_paths.push(ip);
|
|
15137
|
+
}
|
|
15138
|
+
}
|
|
15139
|
+
} catch (err) {
|
|
15140
|
+
console.error('[modify_site_files] image regen fail', ip, err.message);
|
|
15141
|
+
}
|
|
15142
|
+
continue;
|
|
15143
|
+
}
|
|
15144
|
+
|
|
15145
|
+
if (!ch.path || !ch.action) continue;
|
|
15146
|
+
const idx = _find(ch.path);
|
|
15147
|
+
|
|
15148
|
+
if (ch.action === 'delete') {
|
|
15149
|
+
if (idx >= 0) {
|
|
15150
|
+
updated_files.splice(idx, 1);
|
|
15151
|
+
changed_paths.push(ch.path);
|
|
15152
|
+
}
|
|
15153
|
+
continue;
|
|
15154
|
+
}
|
|
15155
|
+
|
|
15156
|
+
if ((ch.action === 'replace' || ch.action === 'create') && typeof ch.new_content === 'string') {
|
|
15157
|
+
const ext = (ch.path.match(/\.([a-z0-9]+)$/i) || [, ''])[1].toLowerCase();
|
|
15158
|
+
const content_type = ext === 'html' ? 'text/html' : ext === 'css' ? 'text/css' : ext === 'js' ? 'application/javascript' : ext === 'json' ? 'application/json' : ext === 'xml' ? 'application/xml' : ext === 'txt' ? 'text/plain' : 'text/plain';
|
|
15159
|
+
const entry = {
|
|
15160
|
+
path: ch.path,
|
|
15161
|
+
content_b64: Buffer.from(ch.new_content, 'utf8').toString('base64'),
|
|
15162
|
+
content_type,
|
|
15163
|
+
};
|
|
15164
|
+
if (idx >= 0) updated_files[idx] = entry;
|
|
15165
|
+
else updated_files.push(entry);
|
|
15166
|
+
changed_paths.push(ch.path);
|
|
15167
|
+
}
|
|
15168
|
+
}
|
|
15169
|
+
|
|
15170
|
+
// Any new attachments uploaded this round join the file set verbatim.
|
|
15171
|
+
for (const entry of ingestion.file_entries) {
|
|
15172
|
+
const idx = _find(entry.path);
|
|
15173
|
+
if (idx >= 0) updated_files[idx] = entry;
|
|
15174
|
+
else updated_files.push(entry);
|
|
15175
|
+
changed_paths.push(entry.path);
|
|
15176
|
+
}
|
|
15177
|
+
|
|
15178
|
+
return {
|
|
15179
|
+
code: 1,
|
|
15180
|
+
data: {
|
|
15181
|
+
updated_files,
|
|
15182
|
+
changed_paths,
|
|
15183
|
+
summary: diff.summary || '',
|
|
15184
|
+
},
|
|
15185
|
+
};
|
|
15186
|
+
} catch (err) {
|
|
15187
|
+
return { code: -400, data: err.message };
|
|
15188
|
+
}
|
|
15189
|
+
};
|
package/index_ms.mjs
CHANGED
|
@@ -17,6 +17,30 @@
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
export const pick_image_source = async function (...args) {
|
|
21
|
+
return await broker.send_to_queue("pick_image_source", ...args);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const pexels_search = async function (...args) {
|
|
25
|
+
return await broker.send_to_queue("pexels_search", ...args);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const estimate_static_website_cost = async function (...args) {
|
|
29
|
+
return await broker.send_to_queue("estimate_static_website_cost", ...args);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const ingest_attachments = async function (...args) {
|
|
33
|
+
return await broker.send_to_queue("ingest_attachments", ...args);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const generate_site_files = async function (...args) {
|
|
37
|
+
return await broker.send_to_queue("generate_site_files", ...args);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const modify_site_files = async function (...args) {
|
|
41
|
+
return await broker.send_to_queue("modify_site_files", ...args);
|
|
42
|
+
};
|
|
43
|
+
|
|
20
44
|
export const execute_codex_request = async function (...args) {
|
|
21
45
|
return await broker.send_to_queue("execute_codex_request", ...args);
|
|
22
46
|
};
|
|
@@ -193,6 +217,10 @@ export const is_spam_email = async function (...args) {
|
|
|
193
217
|
return await broker.send_to_queue("is_spam_email", ...args);
|
|
194
218
|
};
|
|
195
219
|
|
|
220
|
+
export const marketplace_review_item = async function (...args) {
|
|
221
|
+
return await broker.send_to_queue("marketplace_review_item", ...args);
|
|
222
|
+
};
|
|
223
|
+
|
|
196
224
|
export const is_business_contact = async function (...args) {
|
|
197
225
|
return await broker.send_to_queue("is_business_contact", ...args);
|
|
198
226
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -17,6 +17,30 @@
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
export const pick_image_source = function (...args) {
|
|
21
|
+
broker.send_to_queue_async("pick_image_source", ...args);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const pexels_search = function (...args) {
|
|
25
|
+
broker.send_to_queue_async("pexels_search", ...args);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const estimate_static_website_cost = function (...args) {
|
|
29
|
+
broker.send_to_queue_async("estimate_static_website_cost", ...args);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const ingest_attachments = function (...args) {
|
|
33
|
+
broker.send_to_queue_async("ingest_attachments", ...args);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const generate_site_files = function (...args) {
|
|
37
|
+
broker.send_to_queue_async("generate_site_files", ...args);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const modify_site_files = function (...args) {
|
|
41
|
+
broker.send_to_queue_async("modify_site_files", ...args);
|
|
42
|
+
};
|
|
43
|
+
|
|
20
44
|
export const execute_codex_request = function (...args) {
|
|
21
45
|
broker.send_to_queue_async("execute_codex_request", ...args);
|
|
22
46
|
};
|
|
@@ -193,6 +217,10 @@ export const is_spam_email = function (...args) {
|
|
|
193
217
|
broker.send_to_queue_async("is_spam_email", ...args);
|
|
194
218
|
};
|
|
195
219
|
|
|
220
|
+
export const marketplace_review_item = function (...args) {
|
|
221
|
+
broker.send_to_queue_async("marketplace_review_item", ...args);
|
|
222
|
+
};
|
|
223
|
+
|
|
196
224
|
export const is_business_contact = function (...args) {
|
|
197
225
|
broker.send_to_queue_async("is_business_contact", ...args);
|
|
198
226
|
};
|