@xuda.io/ai_module 1.1.5614 → 1.1.5616

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 CHANGED
@@ -2076,7 +2076,7 @@ export const unarchive_ai_chat = async function (req) {
2076
2076
  // }
2077
2077
  // };
2078
2078
 
2079
- const create_image = async function (uid, prompt, model = 'gpt-image-1-mini', size = '1024x1024', n = 1, width = 256, height = 256, metadata = {}, account_profile_info) {
2079
+ const create_image = async function (uid, prompt, model = 'gpt-image-1-mini', size = '1024x1024', n = 1, width = 256, height = 256, metadata = {}, account_profile_info, quality) {
2080
2080
  try {
2081
2081
  let response;
2082
2082
  try {
@@ -2085,6 +2085,11 @@ const create_image = async function (uid, prompt, model = 'gpt-image-1-mini', si
2085
2085
  prompt,
2086
2086
  size,
2087
2087
  n,
2088
+ // Chat-image output is downscaled to width×height (default 256²) right
2089
+ // below, so a high-quality 1024² render is wasted there → default 'low'.
2090
+ // Callers that need a real avatar (e.g. the factual avatar) pass an explicit
2091
+ // `quality` ('medium'). Tunable via _conf.chat_image_quality for the default.
2092
+ quality: quality || _conf.chat_image_quality || 'low',
2088
2093
  });
2089
2094
  report_ai_status(model);
2090
2095
  } catch (err) {
@@ -8334,7 +8339,9 @@ export const get_profile_picture = async function (uid, account_type = 'business
8334
8339
  }
8335
8340
 
8336
8341
  if (factual_image_mode) {
8337
- imageBase64 = await create_image(uid, factual_prompt, undefined, undefined, 1, undefined, undefined, {}, account_profile_info);
8342
+ // Factual avatar is a real user-facing profile image, not a chat thumbnail —
8343
+ // generate at medium (overridable via _conf.factual_avatar_quality), not the low default.
8344
+ imageBase64 = await create_image(uid, factual_prompt, undefined, undefined, 1, undefined, undefined, {}, account_profile_info, _conf.factual_avatar_quality || 'medium');
8338
8345
  } else {
8339
8346
  const model = 'gpt-image-1-mini'; // 'chatgpt-image-latest';
8340
8347
  const prompt = `
@@ -9475,9 +9482,9 @@ async function restoreFaceWithOpenAI(base64Image, ctx = {}) {
9475
9482
  response = await client.images.edit({
9476
9483
  model,
9477
9484
  image: imageFile,
9478
- input_fidelity: 'high',
9479
- quality: 'high',
9480
- size: 'auto',
9485
+ input_fidelity: 'high', // identity preservation — keep high (don't degrade)
9486
+ 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.
9487
+ size: _conf.avatar_image_size || '1024x1024', // was 'auto' (could pick 1536 = ~1.5x output tokens); avatars are square
9481
9488
  prompt:
9482
9489
  '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.',
9483
9490
  });
@@ -14131,7 +14138,7 @@ const _widget_verify_google_id_token = async function (id_token) {
14131
14138
  return payload;
14132
14139
  };
14133
14140
 
14134
- const _widget_find_or_create_visitor = async function (email, name, google_sub) {
14141
+ const _widget_find_or_create_visitor = async function (email, name, google_sub, picture) {
14135
14142
  const ret = await db_module.find_couch_query('xuda_accounts', {
14136
14143
  selector: { 'account_info.email': email },
14137
14144
  fields: ['_id'],
@@ -14139,14 +14146,26 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub)
14139
14146
  });
14140
14147
  if (ret.docs && ret.docs.length) {
14141
14148
  // If we have a google_sub and the existing account lacks one, attach it.
14142
- if (google_sub) {
14149
+ // Also seed the Google photo as the profile_picture when the account has no
14150
+ // avatar yet, so avatar generation has a source to work from.
14151
+ if (google_sub || picture) {
14143
14152
  try {
14144
14153
  const acct_ret = await db_module.get_couch_doc('xuda_accounts', ret.docs[0]._id);
14145
14154
  if (acct_ret.code > -1) {
14146
14155
  const acct = acct_ret.data;
14147
- if (!acct.google_sub) {
14156
+ let dirty = false;
14157
+ if (google_sub && !acct.google_sub) {
14148
14158
  acct.google_sub = google_sub;
14149
14159
  acct.google_linked = true;
14160
+ dirty = true;
14161
+ }
14162
+ acct.account_info = acct.account_info || {};
14163
+ if (picture && !acct.account_info.profile_picture && !acct.account_info.profile_avatar) {
14164
+ acct.account_info.profile_picture = picture;
14165
+ acct.account_info.profile_picture_source = 'google';
14166
+ dirty = true;
14167
+ }
14168
+ if (dirty) {
14150
14169
  acct.ts = Date.now();
14151
14170
  await db_module.save_couch_doc('xuda_accounts', acct);
14152
14171
  }
@@ -14160,6 +14179,13 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub)
14160
14179
  const secret = _utils.get_id('tmp_pass', 5);
14161
14180
  const hash = _utils.hash(secret);
14162
14181
  const { first_name, last_name } = _widget_split_name(name);
14182
+ const account_info = { email, username: _utils.get_id('xu'), first_name, last_name, full_name: name };
14183
+ // Seed the Google profile photo so the avatar-generation step has a source.
14184
+ if (picture) {
14185
+ account_info.profile_picture = picture;
14186
+ account_info.profile_picture_source = 'google';
14187
+ account_info.profile_avatar_stat = 0;
14188
+ }
14163
14189
  const doc = {
14164
14190
  _id: await _common.xuda_get_uuid('account'),
14165
14191
  stat: 1,
@@ -14167,7 +14193,7 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub)
14167
14193
  source: 'widget',
14168
14194
  password: hash,
14169
14195
  email,
14170
- account_info: { email, username: _utils.get_id('xu'), first_name, last_name, full_name: name },
14196
+ account_info,
14171
14197
  };
14172
14198
  if (google_sub) {
14173
14199
  doc.google_sub = google_sub;
@@ -14328,7 +14354,7 @@ export const get_widget_embed_snippet = async function (req, job_id, headers) {
14328
14354
  };
14329
14355
 
14330
14356
  const _widget_signup_core = async function (req, job_id, headers, opts) {
14331
- const { profile_id, email, name, widget_origin, google_sub, signup_method, visitor_lang } = req;
14357
+ const { profile_id, email, name, widget_origin, google_sub, picture, signup_method, visitor_lang } = req;
14332
14358
  const [owner_uid_part, pid_part] = String(profile_id || '').split('.');
14333
14359
  if (!owner_uid_part || !pid_part) return { code: -404, data: 'invalid_profile' };
14334
14360
 
@@ -14339,8 +14365,17 @@ const _widget_signup_core = async function (req, job_id, headers, opts) {
14339
14365
  const ap_doc = account_profile_info.account_profile_obj || {};
14340
14366
  if (ap_doc.widget_enabled === false) return { code: -404, data: 'widget_disabled' };
14341
14367
 
14342
- const visitor_uid = await _widget_find_or_create_visitor(email, name, google_sub);
14343
- 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);
14368
+ const visitor_uid = await _widget_find_or_create_visitor(email, name, google_sub, picture);
14369
+
14370
+ // Kick off persona-avatar generation for the visitor from their Google photo.
14371
+ // Fire-and-forget in the account_module worker; the client polls widget_get_avatar.
14372
+ if (picture) {
14373
+ try {
14374
+ account_msa.ensure_profile_avatar({ uid: visitor_uid }, job_id, headers);
14375
+ } catch (err) {}
14376
+ }
14377
+
14378
+ 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, picture);
14344
14379
  };
14345
14380
 
14346
14381
  export const widget_signup = async function (req, job_id, headers) {
@@ -14375,14 +14410,177 @@ export const widget_signup_google = async function (req, job_id, headers) {
14375
14410
  .trim();
14376
14411
  const name = String(payload.name || payload.email?.split('@')[0] || 'Friend').trim();
14377
14412
  if (!email) return { code: -1, data: 'no_email_in_token' };
14413
+ const picture = typeof payload.picture === 'string' ? payload.picture : '';
14414
+
14415
+ return await _widget_signup_core({ profile_id, email, name, widget_origin, visitor_lang, google_sub: payload.sub, picture, signup_method: 'google' }, job_id, headers);
14416
+ } catch (err) {
14417
+ return { code: -1, data: err.message || String(err) };
14418
+ }
14419
+ };
14420
+
14421
+ // Site-level Google sign-in (e.g. the xuda.network topbar). Unlike
14422
+ // widget_signup_google this is NOT tied to a persona/chat — it performs the
14423
+ // same "complete setup of the user in xuda" the widget does (verify the Google
14424
+ // token, then find-or-create + link the visitor's free xuda account by
14425
+ // google_sub, and kick off avatar generation from their Google photo), so a
14426
+ // visitor is fully provisioned at site login, before they ever open a chat.
14427
+ // The chat widget later just links the already-provisioned account to a persona.
14428
+ export const site_signup_google = async function (req, job_id, headers) {
14429
+ try {
14430
+ const { id_token } = req;
14431
+ if (!id_token || typeof id_token !== 'string') return { code: -1, data: 'missing_id_token' };
14432
+
14433
+ let payload;
14434
+ try {
14435
+ payload = await _widget_verify_google_id_token(id_token);
14436
+ } catch (err) {
14437
+ return { code: -401, data: err.message || 'google_token_invalid' };
14438
+ }
14439
+
14440
+ const email = String(payload.email || '')
14441
+ .toLowerCase()
14442
+ .trim();
14443
+ const name = String(payload.name || payload.email?.split('@')[0] || 'Friend').trim();
14444
+ if (!email) return { code: -1, data: 'no_email_in_token' };
14445
+ const picture = typeof payload.picture === 'string' ? payload.picture : '';
14446
+
14447
+ const visitor_uid = await _widget_find_or_create_visitor(email, name, payload.sub, picture);
14448
+
14449
+ // Fire-and-forget avatar generation from the Google photo, same as the widget.
14450
+ if (picture) {
14451
+ try {
14452
+ account_msa.ensure_profile_avatar({ uid: visitor_uid }, job_id, headers);
14453
+ } catch (err) {}
14454
+ }
14455
+
14456
+ const { first_name, last_name } = _widget_split_name(name);
14457
+ return {
14458
+ code: 1,
14459
+ data: {
14460
+ uid: visitor_uid,
14461
+ email,
14462
+ name,
14463
+ first_name,
14464
+ last_name,
14465
+ picture,
14466
+ account_active: true,
14467
+ signin_url: `https://${_conf.domain || 'xuda.ai'}/dashboard/login`,
14468
+ },
14469
+ };
14470
+ } catch (err) {
14471
+ return { code: -1, data: err.message || String(err) };
14472
+ }
14473
+ };
14474
+
14475
+ // Public-profile "Ask Friendship". A visitor on /public_profiles/<target_uid>
14476
+ // signs in with Google (same find-or-create as site_signup_google), then we send
14477
+ // a contact_connection team_request to the profile owner. Auto-accepts (canonical
14478
+ // bidirectional contacts) when the owner has auto_respond enabled; otherwise the
14479
+ // owner gets an invitation email and the request stays pending. Never direct
14480
+ // add_contact — always the team_request invitation flow.
14481
+ export const public_profile_ask_friendship = async function (req, job_id, headers) {
14482
+ try {
14483
+ const { id_token, target_uid } = req;
14484
+ if (!id_token || typeof id_token !== 'string') return { code: -1, data: 'missing_id_token' };
14485
+ if (!target_uid || typeof target_uid !== 'string') return { code: -1, data: 'missing_target_uid' };
14486
+
14487
+ let payload;
14488
+ try {
14489
+ payload = await _widget_verify_google_id_token(id_token);
14490
+ } catch (err) {
14491
+ return { code: -401, data: err.message || 'google_token_invalid' };
14492
+ }
14493
+ const email = String(payload.email || '')
14494
+ .toLowerCase()
14495
+ .trim();
14496
+ const name = String(payload.name || payload.email?.split('@')[0] || 'Friend').trim();
14497
+ if (!email) return { code: -1, data: 'no_email_in_token' };
14498
+ const picture = typeof payload.picture === 'string' ? payload.picture : '';
14499
+
14500
+ // Target must be an active, non-opted-out account.
14501
+ const target_ret = await account_ms.get_account_name({ uid_query: target_uid });
14502
+ if (!target_ret || target_ret.code < 0) return { code: -404, data: 'target_not_found' };
14503
+ const target = target_ret.data || {};
14504
+ if (target.stat !== 3 || target.public_profile_disabled === true) return { code: -404, data: 'target_unavailable' };
14505
+ const target_name = [target.first_name, target.last_name].filter(Boolean).join(' ') || target.username || 'Xuda Member';
14506
+
14507
+ // Find or create the visitor's xuda account (same setup as site_signup_google).
14508
+ const visitor_uid = await _widget_find_or_create_visitor(email, name, payload.sub, picture);
14509
+ if (visitor_uid === target_uid) return { code: -1, data: 'cannot_friend_self' };
14510
+ if (picture) {
14511
+ try {
14512
+ account_msa.ensure_profile_avatar({ uid: visitor_uid }, job_id, headers);
14513
+ } catch (err) {}
14514
+ }
14515
+
14516
+ const { first_name, last_name } = _widget_split_name(name);
14517
+ const visitor_first_name = first_name || '';
14518
+
14519
+ // Dedupe: reuse an existing request from this visitor to this target.
14520
+ const existing_ret = await db_module.find_couch_query('xuda_team', {
14521
+ selector: {
14522
+ docType: 'team_request',
14523
+ access_type: 'contact_connection',
14524
+ team_req_from_uid: visitor_uid,
14525
+ team_req_to_uid: target_uid,
14526
+ contact_source: 'public_profile',
14527
+ },
14528
+ limit: 1,
14529
+ });
14530
+ let team_req_doc = existing_ret?.docs?.[0] || null;
14378
14531
 
14379
- return await _widget_signup_core({ profile_id, email, name, widget_origin, visitor_lang, google_sub: payload.sub, signup_method: 'google' }, job_id, headers);
14532
+ if (!team_req_doc) {
14533
+ const team_req_id = await _common.xuda_get_uuid('team_req');
14534
+ team_req_doc = {
14535
+ _id: team_req_id,
14536
+ team_req_id,
14537
+ team_req_date: Date.now(),
14538
+ team_req_status_date: null,
14539
+ team_req_to_email: target.email || '',
14540
+ team_req_to_uid: target_uid,
14541
+ team_req_stat: 2,
14542
+ team_req_stat_desc: 'public profile friend request',
14543
+ team_req_from_uid: visitor_uid,
14544
+ docType: 'team_request',
14545
+ team_req_app_id: null,
14546
+ access_type: 'contact_connection',
14547
+ uid: visitor_uid,
14548
+ contact_source: 'public_profile',
14549
+ share_item_id: target_uid,
14550
+ sender_account_info: { email, first_name, last_name, full_name: name },
14551
+ };
14552
+ const tr_save = await db_module.save_couch_doc('xuda_team', team_req_doc);
14553
+ if (tr_save.code < 0) return { code: -1, data: tr_save.data || 'team_request_create_failed' };
14554
+
14555
+ // fire-and-forget — the invitation email shouldn't block the response
14556
+ team_ms.send_access_invitation({ team_req_id: team_req_doc._id }).catch(() => {});
14557
+ }
14558
+
14559
+ let state = team_req_doc.team_req_stat === 3 ? 'open' : team_req_doc.team_req_stat === 4 ? 'declined' : 'pending';
14560
+
14561
+ // Auto-accept when the owner's active profile has auto_respond — performs the
14562
+ // canonical bidirectional contact creation via confirm_team_request.
14563
+ if (state === 'pending') {
14564
+ let auto_accept = false;
14565
+ try {
14566
+ const tgt_ap = await get_active_account_profile_info(target_uid);
14567
+ auto_accept = !!tgt_ap?.account_profile_obj?.auto_respond;
14568
+ } catch (err) {}
14569
+ if (auto_accept) {
14570
+ try {
14571
+ const cret = await team_ms.confirm_team_request({ team_req_id: team_req_doc._id });
14572
+ if (cret && cret.code > -1) state = 'open';
14573
+ } catch (err) {}
14574
+ }
14575
+ }
14576
+
14577
+ return { code: 1, data: { state, visitor_first_name, target_name } };
14380
14578
  } catch (err) {
14381
14579
  return { code: -1, data: err.message || String(err) };
14382
14580
  }
14383
14581
  };
14384
14582
 
14385
- 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) {
14583
+ 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, picture) {
14386
14584
  try {
14387
14585
  let team_req_doc = await _widget_find_existing_team_req(visitor_uid, canonical_profile_id);
14388
14586
  let assigned_uid;
@@ -14494,6 +14692,9 @@ const _widget_signup_finalize = async function (visitor_uid, email, name, widget
14494
14692
  greeting,
14495
14693
  conversation_id,
14496
14694
  visitor_first_name,
14695
+ visitor_uid,
14696
+ // 1 = avatar generation kicked off (poll widget_get_avatar); 0 = none in flight
14697
+ avatar_stat: picture ? 1 : 0,
14497
14698
  signup_method: signup_method || 'email',
14498
14699
  account_active: signup_method === 'google',
14499
14700
  signin_url: signup_method === 'google' ? `https://${_conf.domain || 'xuda.ai'}/dashboard/login` : undefined,
@@ -14504,6 +14705,59 @@ const _widget_signup_finalize = async function (visitor_uid, email, name, widget
14504
14705
  }
14505
14706
  };
14506
14707
 
14708
+ // Build the avatar-poll payload for an account: generation status plus the
14709
+ // avatar as base64 so an embedding page can draw it to a <canvas> without a
14710
+ // cross-origin taint (drive URLs may not send CORS headers).
14711
+ const _account_avatar_payload = async function (uid) {
14712
+ const acct_ret = await db_module.get_couch_doc('xuda_accounts', uid);
14713
+ if (acct_ret.code < 0 || !acct_ret.data) return null;
14714
+ const info = acct_ret.data.account_info || {};
14715
+ let avatar_b64 = null;
14716
+ if (info.profile_avatar) {
14717
+ try {
14718
+ const blob_ret = await get_image_blob_from_downloaded_image(info.profile_avatar);
14719
+ avatar_b64 = Buffer.from(await blob_ret.image_blob.arrayBuffer()).toString('base64');
14720
+ } catch (err) {}
14721
+ }
14722
+ return {
14723
+ // 0 none · 1 started · 2 generating · 3 done (profile_avatar set)
14724
+ profile_avatar_stat: info.profile_avatar_stat || 0,
14725
+ profile_avatar: info.profile_avatar || null,
14726
+ profile_avatar_error: info.profile_avatar_error || null,
14727
+ avatar_b64,
14728
+ };
14729
+ };
14730
+
14731
+ // Poll the visitor's generated persona avatar after a widget Google signup.
14732
+ // Auth is the widget_token (the session resolves the visitor uid).
14733
+ export const widget_get_avatar = async function (req) {
14734
+ try {
14735
+ const { widget_token } = req || {};
14736
+ const session = await _widget_load_session(widget_token);
14737
+ if (!session) return { code: -401, data: 'invalid_session' };
14738
+ const payload = await _account_avatar_payload(session.visitor_uid);
14739
+ if (!payload) return { code: -1, data: 'visitor_not_found' };
14740
+ return { code: 1, data: payload };
14741
+ } catch (err) {
14742
+ return { code: -1, data: err.message || String(err) };
14743
+ }
14744
+ };
14745
+
14746
+ // Poll the generated avatar after a site_signup_google (no widget session). The
14747
+ // uid is the long random account id returned by site_signup_google — not
14748
+ // enumerable — and only non-sensitive avatar fields are returned.
14749
+ export const site_get_avatar = async function (req) {
14750
+ try {
14751
+ const { uid } = req || {};
14752
+ if (!uid || typeof uid !== 'string') return { code: -1, data: 'missing_uid' };
14753
+ const payload = await _account_avatar_payload(uid);
14754
+ if (!payload) return { code: -1, data: 'account_not_found' };
14755
+ return { code: 1, data: payload };
14756
+ } catch (err) {
14757
+ return { code: -1, data: err.message || String(err) };
14758
+ }
14759
+ };
14760
+
14507
14761
  export const widget_send_message = async function (req, job_id, headers) {
14508
14762
  try {
14509
14763
  const { widget_token, prompt = '', attachments = [] } = req;
@@ -14629,3 +14883,557 @@ export const widget_get_messages = async function (req, job_id, headers) {
14629
14883
  return { code: -1, data: err.message || String(err) };
14630
14884
  }
14631
14885
  };
14886
+
14887
+ // ============================================================================
14888
+ // Static-website generation pipeline (helpers + orchestrator)
14889
+ // ============================================================================
14890
+ //
14891
+ // pick_image_source - three-bucket router (Pexels / gpt-image-1 / chatgpt-image-latest)
14892
+ // pexels_search - first consumer of _conf.pexels.api_key
14893
+ // estimate_static_website_cost - credit pre-check for create + modify
14894
+ // ingest_attachments - categorize user-uploaded files into the output tree + manifest
14895
+ // generate_site_files - planning prompt + per-page HTML + per-image source + SEO files
14896
+ // modify_site_files - modify-by-prompt over existing file set
14897
+ //
14898
+ // Image-source bucket order (cheapest -> most expensive): Pexels (free) ->
14899
+ // gpt-image-1 (cheap AI) -> chatgpt-image-latest (premium AI). Decision is
14900
+ // driven by membership/workspace tier and credit balance so a paid user
14901
+ // running low auto-degrades from premium to cheap rather than failing.
14902
+
14903
+ const LOW_CREDIT_THRESHOLD = _conf.static_website?.low_credit_threshold ?? 10;
14904
+ const HIGH_CREDIT_THRESHOLD = _conf.static_website?.high_credit_threshold ?? 100;
14905
+
14906
+ export const pick_image_source = (account_doc, credit_balance) => {
14907
+ const free_mem = (account_doc?.membership_plan || 'free') === 'free';
14908
+ const free_ws = (account_doc?.ai_workspace_plan || 'free_ai_workspace') === 'free_ai_workspace';
14909
+ const credits = Number(credit_balance ?? 0);
14910
+ const paid_tier = !free_mem || !free_ws;
14911
+
14912
+ // Bucket 1: no AI option (fully-free + no credits)
14913
+ if (!paid_tier && credits < LOW_CREDIT_THRESHOLD) return 'pexels';
14914
+ // Bucket 3: premium, only for paid users with a comfortable buffer
14915
+ if (paid_tier && credits >= HIGH_CREDIT_THRESHOLD) return 'chatgpt-image-latest';
14916
+ // Bucket 2: cheaper AI - free users with credits, paid users running low (auto-degrade)
14917
+ return 'gpt-image-1';
14918
+ };
14919
+
14920
+ export const pexels_search = async (query, n = 1) => {
14921
+ const api_key = _conf.pexels?.api_key;
14922
+ if (!api_key) throw new Error('pexels.api_key not configured');
14923
+ const url = `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=${Math.max(1, Math.min(80, Number(n) || 1))}`;
14924
+ const r = await fetch(url, { headers: { Authorization: api_key } });
14925
+ if (!r.ok) throw new Error(`pexels search failed: ${r.status}`);
14926
+ const j = await r.json();
14927
+ const photos = (j.photos || []).slice(0, n);
14928
+ return Promise.all(
14929
+ photos.map(async (p) => {
14930
+ const img = await fetch(p.src.large || p.src.medium || p.src.original);
14931
+ if (!img.ok) throw new Error(`pexels fetch failed: ${img.status}`);
14932
+ const buf = Buffer.from(await img.arrayBuffer());
14933
+ return {
14934
+ content_b64: buf.toString('base64'),
14935
+ content_type: 'image/jpeg',
14936
+ src_url: p.url,
14937
+ photographer: p.photographer,
14938
+ photographer_url: p.photographer_url,
14939
+ };
14940
+ }),
14941
+ );
14942
+ };
14943
+
14944
+ // Expected per-tier work for the credit estimator. Real generated count is
14945
+ // usually a fraction of the hard cap (page_count_limit in PLAN_OBJ).
14946
+ const EXPECTED_STATIC_WEBSITE_WORK = {
14947
+ landing: { pages: 1, imgs_per_page: 2, seo: 0, css: 1 },
14948
+ full: { pages: 8, imgs_per_page: 3, seo: 0, css: 1 },
14949
+ seo: { pages: 25, imgs_per_page: 3, seo: 2, css: 1 },
14950
+ enterprise: { pages: 100, imgs_per_page: 3, seo: 2, css: 1 },
14951
+ };
14952
+
14953
+ const _image_price = (image_source) => {
14954
+ if (image_source === 'pexels') return 0;
14955
+ const m = _conf.ai_models?.[image_source];
14956
+ return m?.price ?? Math.max(m?.input ?? 0, m?.output ?? 0) ?? 5;
14957
+ };
14958
+
14959
+ const _text_price = () => {
14960
+ const k = _conf.default_ai_model || 'gpt-5.4-mini';
14961
+ const m = _conf.ai_models?.[k];
14962
+ return m?.price ?? 1;
14963
+ };
14964
+
14965
+ export const estimate_static_website_cost = (req) => {
14966
+ const plan_tier = req?.plan_tier;
14967
+ const image_source = req?.image_source || 'gpt-image-1';
14968
+ const mode = req?.mode || 'create';
14969
+ const current_page_count = Number(req?.current_page_count || 0);
14970
+
14971
+ const W = EXPECTED_STATIC_WEBSITE_WORK[plan_tier] || EXPECTED_STATIC_WEBSITE_WORK.landing;
14972
+ const pages =
14973
+ mode === 'modify'
14974
+ ? Math.max(1, Math.ceil(current_page_count * 0.3)) // modify touches ~30% by default
14975
+ : W.pages;
14976
+ const text_calls = 1 /* site plan */ + W.css + pages /* per-page HTML */ + W.seo;
14977
+ const image_calls = pages * W.imgs_per_page;
14978
+
14979
+ const text_unit = _text_price();
14980
+ const image_unit = _image_price(image_source);
14981
+ const credits_needed = text_calls * text_unit + image_calls * image_unit;
14982
+
14983
+ return {
14984
+ credits_needed,
14985
+ breakdown: { text_calls, text_unit, image_calls, image_unit, pages, mode },
14986
+ image_source,
14987
+ };
14988
+ };
14989
+
14990
+ // ── Attachment ingestion ────────────────────────────────────────────────
14991
+ //
14992
+ // Shape: [{ filename, mime_type, drive_path? | content_b64? | url? }]
14993
+ // At least one source field must be present. Files land in the deployed site
14994
+ // at a stable /uploads/{img,docs,text,data,files}/{slug} path so the LLM can
14995
+ // reference them by URL in generated HTML.
14996
+
14997
+ const _categorize_attachment = (att) => {
14998
+ const mt = (att?.mime_type || '').toLowerCase();
14999
+ if (mt.startsWith('image/')) return 'image';
15000
+ if (mt === 'application/pdf') return 'pdf';
15001
+ if (mt === 'text/csv' || mt === 'application/vnd.ms-excel') return 'data';
15002
+ if (mt === 'application/json' || mt === 'text/markdown' || mt.startsWith('text/')) return 'text';
15003
+ return 'binary';
15004
+ };
15005
+
15006
+ const _load_attachment_bytes = async (att) => {
15007
+ if (att.content_b64) return Buffer.from(att.content_b64, 'base64');
15008
+ if (att.drive_path) {
15009
+ const abs = path.join(_conf.studio_drive_path, att.drive_path);
15010
+ return await readFile(abs);
15011
+ }
15012
+ if (att.url) {
15013
+ const r = await fetch(att.url);
15014
+ if (!r.ok) throw new Error(`attachment fetch ${att.url}: ${r.status}`);
15015
+ return Buffer.from(await r.arrayBuffer());
15016
+ }
15017
+ throw new Error(`attachment ${att.filename} has no source (drive_path | content_b64 | url)`);
15018
+ };
15019
+
15020
+ export const ingest_attachments = async (attachments = []) => {
15021
+ const out = { manifest: [], file_entries: [], text_corpus: [] };
15022
+ for (const att of attachments) {
15023
+ if (!att || !att.filename) continue;
15024
+ const kind = _categorize_attachment(att);
15025
+ let bytes;
15026
+ try {
15027
+ bytes = await _load_attachment_bytes(att);
15028
+ } catch (err) {
15029
+ console.error('[ingest_attachments] skip', att.filename, err.message);
15030
+ continue;
15031
+ }
15032
+ const slug = String(att.filename)
15033
+ .replace(/[^a-z0-9.-]/gi, '_')
15034
+ .toLowerCase();
15035
+ let public_path;
15036
+ if (kind === 'image') public_path = `/uploads/img/${slug}`;
15037
+ else if (kind === 'pdf') public_path = `/uploads/docs/${slug}`;
15038
+ else if (kind === 'text') public_path = `/uploads/text/${slug}`;
15039
+ else if (kind === 'data') public_path = `/uploads/data/${slug}`;
15040
+ else public_path = `/uploads/files/${slug}`;
15041
+
15042
+ out.file_entries.push({
15043
+ path: public_path,
15044
+ content_b64: bytes.toString('base64'),
15045
+ content_type: att.mime_type || 'application/octet-stream',
15046
+ });
15047
+
15048
+ const manifest_entry = { filename: att.filename, kind, public_path };
15049
+ if (kind === 'text' || kind === 'data') {
15050
+ const text = bytes.toString('utf8').slice(0, 8000);
15051
+ manifest_entry.excerpt = text;
15052
+ out.text_corpus.push({ filename: att.filename, text });
15053
+ }
15054
+ if (kind === 'image') {
15055
+ manifest_entry.usage = `Embed via <img src="${public_path}" alt="..."> on relevant pages.`;
15056
+ }
15057
+ out.manifest.push(manifest_entry);
15058
+ }
15059
+ return out;
15060
+ };
15061
+
15062
+ // ── Generation orchestrator ─────────────────────────────────────────────
15063
+ //
15064
+ // generate_site_files runs the prompt -> plan -> pages -> images -> SEO chain.
15065
+ // Returns a flat [{ path, content_b64, content_type }] list ready for
15066
+ // domains_module.deploy_site.
15067
+
15068
+ const _safe_json_parse = (s) => {
15069
+ if (!s || typeof s !== 'string') return null;
15070
+ // The LLM sometimes wraps JSON in ```json fences despite instructions.
15071
+ const cleaned = s
15072
+ .trim()
15073
+ .replace(/^```(?:json)?\s*/i, '')
15074
+ .replace(/\s*```\s*$/i, '');
15075
+ try {
15076
+ return JSON.parse(cleaned);
15077
+ } catch (_) {
15078
+ return null;
15079
+ }
15080
+ };
15081
+
15082
+ const _chat = async (uid, prompt, account_profile_info, metadata) => {
15083
+ const ret = await submit_chat_gpt_prompt({
15084
+ uid,
15085
+ prompt,
15086
+ model: _conf.default_ai_model,
15087
+ metadata: metadata || {},
15088
+ account_profile_info,
15089
+ });
15090
+ if (ret.code < 0) throw new Error(`LLM call failed: ${ret.data}`);
15091
+ return ret.data || '';
15092
+ };
15093
+
15094
+ const _site_planning_prompt = (user_prompt, attachments_manifest, plan_tier, page_count_limit) => {
15095
+ const W = EXPECTED_STATIC_WEBSITE_WORK[plan_tier] || EXPECTED_STATIC_WEBSITE_WORK.landing;
15096
+ const default_pages = Math.min(W.pages, page_count_limit);
15097
+ return `You are planning a static website. Output STRICT JSON only, no prose, no markdown fences.
15098
+
15099
+ User brief:
15100
+ ${user_prompt}
15101
+
15102
+ 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.
15103
+ ${JSON.stringify(attachments_manifest, null, 2)}
15104
+
15105
+ Hard rules:
15106
+ - Hosting tier: ${plan_tier}. Maximum ${page_count_limit} pages total. Aim for ${default_pages} pages unless the brief clearly implies more.
15107
+ - Each page has a slug (kebab-case path component, "" for home), title, and an ordered list of sections.
15108
+ - 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).
15109
+ - For 'generate' slots, write a concise visual prompt AND a Pexels-friendly query string.
15110
+ - For 'attachment' slots, set attachment_path to one of the public_paths from the manifest.
15111
+ - style_directive is one paragraph describing the desired visual style (palette, typography, density) used to generate the global stylesheet.
15112
+
15113
+ Schema:
15114
+ {
15115
+ "pages": [
15116
+ {
15117
+ "slug": "string (empty for home)",
15118
+ "title": "string",
15119
+ "sections": [{"kind": "prose|image|quote|data_table|callout", "...": "..."}],
15120
+ "image_specs": [
15121
+ {"slot": "kebab-slug", "source": "generate|attachment", "prompt": "...", "query": "...", "attachment_path": "/uploads/...", "aspect": "16/10|1/1|3/2"}
15122
+ ]
15123
+ }
15124
+ ],
15125
+ "style_directive": "string"
15126
+ }
15127
+
15128
+ Respond with ONLY the JSON object.`;
15129
+ };
15130
+
15131
+ const _page_html_prompt = (page, style_directive, attachments_manifest) => `Generate a single static HTML page. Output ONLY the HTML, no markdown fences, no commentary.
15132
+
15133
+ Page brief:
15134
+ ${JSON.stringify(page, null, 2)}
15135
+
15136
+ Global style directive (matches /style.css that's already written):
15137
+ ${style_directive}
15138
+
15139
+ Attachments available (use these public_paths verbatim when referenced):
15140
+ ${JSON.stringify(attachments_manifest, null, 2)}
15141
+
15142
+ Hard rules:
15143
+ - DOCTYPE html, lang="en", responsive viewport meta.
15144
+ - Link to /style.css.
15145
+ - Reference images from /img/{slot}.{ext} for 'generate' slots and from their public_path for 'attachment' slots, exactly as listed in image_specs.
15146
+ - Semantic HTML5 (header, main, section, footer). No inline styles unless absolutely necessary.
15147
+ - No external CDNs, no JavaScript.
15148
+ - Title from page.title.
15149
+
15150
+ Respond with ONLY the raw HTML document.`;
15151
+
15152
+ 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.
15153
+
15154
+ Style directive:
15155
+ ${style_directive}
15156
+
15157
+ Hard rules:
15158
+ - Modern CSS only (no preprocessors). Use CSS variables for palette.
15159
+ - Responsive: mobile-first, breakpoints at 640px and 980px.
15160
+ - Typography: choose a serif heading + sans-serif body OR all sans, consistent with the directive.
15161
+ - Include base resets, layout containers, header, footer, basic image styling (max-width 100%, rounded), button styles.
15162
+ - Keep under 6 KB.
15163
+
15164
+ Respond with ONLY the raw CSS text.`;
15165
+
15166
+ const _sitemap_xml = (pages, hostname) => {
15167
+ const base = `https://${hostname}`;
15168
+ const urls = pages
15169
+ .map((p) => {
15170
+ const loc = p.slug ? `${base}/${p.slug}/` : `${base}/`;
15171
+ return ` <url><loc>${loc}</loc></url>`;
15172
+ })
15173
+ .join('\n');
15174
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
15175
+ };
15176
+
15177
+ const _robots_txt = (hostname) => `User-agent: *\nAllow: /\nSitemap: https://${hostname}/sitemap.xml\n`;
15178
+
15179
+ // Picks an extension for a generated image filename from its content-type.
15180
+ const _image_ext = (content_type) => {
15181
+ if (!content_type) return 'png';
15182
+ if (/webp/i.test(content_type)) return 'webp';
15183
+ if (/jpe?g/i.test(content_type)) return 'jpg';
15184
+ if (/png/i.test(content_type)) return 'png';
15185
+ if (/svg/i.test(content_type)) return 'svg';
15186
+ return 'png';
15187
+ };
15188
+
15189
+ export const generate_site_files = async (req) => {
15190
+ try {
15191
+ const { uid, prompt, attachments, plan_tier, page_count_limit, image_source, account_profile_info, hostname = 'sites.xuda.io' } = req;
15192
+ if (!uid || !prompt) return { code: -1, data: 'uid + prompt required' };
15193
+ if (!plan_tier) return { code: -1, data: 'plan_tier required' };
15194
+
15195
+ // 1. Ingest attachments (copies them into the output file list verbatim)
15196
+ const ingestion = await ingest_attachments(attachments || []);
15197
+
15198
+ // 2. Plan the site
15199
+ 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 });
15200
+ const plan = _safe_json_parse(plan_text);
15201
+ if (!plan || !Array.isArray(plan.pages) || !plan.pages.length) {
15202
+ return { code: -2, data: 'planning JSON malformed', plan_text };
15203
+ }
15204
+
15205
+ // 3. Hard-cap page count
15206
+ plan.pages = plan.pages.slice(0, Math.max(1, Math.min(page_count_limit, plan.pages.length)));
15207
+
15208
+ // 4. Global CSS
15209
+ const css = await _chat(uid, _css_prompt(plan.style_directive || '', plan_tier), account_profile_info, { stage: 'static_website_css', plan_tier });
15210
+
15211
+ // 5. Per-page HTML
15212
+ const html_files = [];
15213
+ for (const page of plan.pages) {
15214
+ 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 || '' });
15215
+ const rel = page.slug ? `/${page.slug}/index.html` : '/index.html';
15216
+ html_files.push({
15217
+ path: rel,
15218
+ content_b64: Buffer.from(html, 'utf8').toString('base64'),
15219
+ content_type: 'text/html',
15220
+ });
15221
+ }
15222
+
15223
+ // 6. Per-image generation
15224
+ const image_files = [];
15225
+ for (const page of plan.pages) {
15226
+ const specs = Array.isArray(page.image_specs) ? page.image_specs : [];
15227
+ for (const spec of specs) {
15228
+ try {
15229
+ if (spec.source === 'attachment') {
15230
+ // already in ingestion.file_entries — nothing to add
15231
+ continue;
15232
+ }
15233
+ if (image_source === 'pexels') {
15234
+ const [img] = await pexels_search(spec.query || spec.prompt || page.title, 1);
15235
+ if (!img) continue;
15236
+ image_files.push({
15237
+ path: `/img/${spec.slot}.jpg`,
15238
+ content_b64: img.content_b64,
15239
+ content_type: 'image/jpeg',
15240
+ });
15241
+ } else {
15242
+ const b64 = await create_image(
15243
+ uid,
15244
+ spec.prompt || spec.query || page.title,
15245
+ image_source, // 'gpt-image-1' | 'chatgpt-image-latest'
15246
+ '1024x1024',
15247
+ 1,
15248
+ 1024,
15249
+ 1024, // full-resolution output
15250
+ { kind: 'static_website', slot: spec.slot },
15251
+ account_profile_info,
15252
+ );
15253
+ if (!b64) continue;
15254
+ image_files.push({
15255
+ path: `/img/${spec.slot}.png`,
15256
+ content_b64: b64,
15257
+ content_type: 'image/png',
15258
+ });
15259
+ }
15260
+ } catch (err) {
15261
+ console.error('[generate_site_files] image fail', spec.slot, err.message);
15262
+ }
15263
+ }
15264
+ }
15265
+
15266
+ // 7. SEO files (seo + enterprise tiers only)
15267
+ const seo_files = [];
15268
+ if (plan_tier === 'seo' || plan_tier === 'enterprise') {
15269
+ seo_files.push({
15270
+ path: '/sitemap.xml',
15271
+ content_b64: Buffer.from(_sitemap_xml(plan.pages, hostname), 'utf8').toString('base64'),
15272
+ content_type: 'application/xml',
15273
+ });
15274
+ seo_files.push({
15275
+ path: '/robots.txt',
15276
+ content_b64: Buffer.from(_robots_txt(hostname), 'utf8').toString('base64'),
15277
+ content_type: 'text/plain',
15278
+ });
15279
+ }
15280
+
15281
+ // 8. Compose final flat file list
15282
+ const css_file = {
15283
+ path: '/style.css',
15284
+ content_b64: Buffer.from(css, 'utf8').toString('base64'),
15285
+ content_type: 'text/css',
15286
+ };
15287
+ const files = [css_file, ...html_files, ...image_files, ...seo_files, ...ingestion.file_entries];
15288
+
15289
+ return {
15290
+ code: 1,
15291
+ data: {
15292
+ files,
15293
+ plan,
15294
+ manifest: ingestion.manifest,
15295
+ stats: {
15296
+ page_count: plan.pages.length,
15297
+ image_count: image_files.length,
15298
+ attachment_count: ingestion.file_entries.length,
15299
+ },
15300
+ },
15301
+ };
15302
+ } catch (err) {
15303
+ return { code: -400, data: err.message };
15304
+ }
15305
+ };
15306
+
15307
+ // ── Modify orchestrator ────────────────────────────────────────────────
15308
+ //
15309
+ // Loads current files, asks LLM what to change, writes back only the deltas.
15310
+ // Image source stays pinned to whatever was chosen at creation - modify does
15311
+ // NOT silently upgrade Pexels -> AI (that would surprise free users).
15312
+
15313
+ 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.
15314
+
15315
+ User request:
15316
+ ${user_prompt}
15317
+
15318
+ 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):
15319
+ ${JSON.stringify(current_files_index, null, 2)}
15320
+
15321
+ New attachments uploaded with this request:
15322
+ ${JSON.stringify(attachments_manifest, null, 2)}
15323
+
15324
+ For each change you want to make, emit one entry in "changes":
15325
+ { "path": "/index.html", "action": "replace", "new_content": "...full new file content..." }
15326
+ { "path": "/about/index.html", "action": "create", "new_content": "..." }
15327
+ { "path": "/old-page/index.html", "action": "delete" }
15328
+
15329
+ Image regeneration: emit { "image_change": { "path": "/img/hero.png", "new_prompt": "..." } } and the orchestrator will re-render via the locked image source.
15330
+
15331
+ Hard rules:
15332
+ - Touch only files you actually need to change. Do NOT rewrite the whole site.
15333
+ - new_content for HTML files: full document, DOCTYPE through </html>. Link /style.css unchanged unless explicitly told to restyle.
15334
+ - Do NOT change file paths for unchanged pages.
15335
+
15336
+ Schema:
15337
+ { "changes": [...], "summary": "one sentence" }
15338
+
15339
+ Respond with ONLY the JSON object.`;
15340
+
15341
+ export const modify_site_files = async (req) => {
15342
+ try {
15343
+ const { uid, current_files, prompt, attachments, plan_tier, image_source, account_profile_info } = req;
15344
+ if (!uid || !prompt) return { code: -1, data: 'uid + prompt required' };
15345
+ if (!Array.isArray(current_files) || !current_files.length) return { code: -1, data: 'current_files required' };
15346
+
15347
+ const ingestion = await ingest_attachments(attachments || []);
15348
+ const index = current_files.map((f) => ({
15349
+ path: f.path,
15350
+ content_type: f.content_type,
15351
+ size_b64: (f.content_b64 || '').length,
15352
+ }));
15353
+
15354
+ const diff_text = await _chat(uid, _modify_prompt(prompt, index, ingestion.manifest), account_profile_info, { stage: 'static_website_modify', plan_tier });
15355
+ const diff = _safe_json_parse(diff_text);
15356
+ if (!diff || !Array.isArray(diff.changes)) {
15357
+ return { code: -2, data: 'modify JSON malformed', diff_text };
15358
+ }
15359
+
15360
+ const changed_paths = [];
15361
+ const updated_files = [...current_files];
15362
+
15363
+ const _find = (p) => updated_files.findIndex((f) => f.path === p);
15364
+
15365
+ for (const ch of diff.changes) {
15366
+ // Re-rendered image
15367
+ if (ch.image_change && ch.image_change.path && ch.image_change.new_prompt) {
15368
+ const ip = ch.image_change.path;
15369
+ try {
15370
+ if (image_source === 'pexels') {
15371
+ const [img] = await pexels_search(ch.image_change.new_prompt, 1);
15372
+ if (img) {
15373
+ const entry = { path: ip, content_b64: img.content_b64, content_type: 'image/jpeg' };
15374
+ const idx = _find(ip);
15375
+ if (idx >= 0) updated_files[idx] = entry;
15376
+ else updated_files.push(entry);
15377
+ changed_paths.push(ip);
15378
+ }
15379
+ } else {
15380
+ const b64 = await create_image(uid, ch.image_change.new_prompt, image_source, '1024x1024', 1, 1024, 1024, { kind: 'static_website_modify' }, account_profile_info);
15381
+ if (b64) {
15382
+ const entry = { path: ip, content_b64: b64, content_type: 'image/png' };
15383
+ const idx = _find(ip);
15384
+ if (idx >= 0) updated_files[idx] = entry;
15385
+ else updated_files.push(entry);
15386
+ changed_paths.push(ip);
15387
+ }
15388
+ }
15389
+ } catch (err) {
15390
+ console.error('[modify_site_files] image regen fail', ip, err.message);
15391
+ }
15392
+ continue;
15393
+ }
15394
+
15395
+ if (!ch.path || !ch.action) continue;
15396
+ const idx = _find(ch.path);
15397
+
15398
+ if (ch.action === 'delete') {
15399
+ if (idx >= 0) {
15400
+ updated_files.splice(idx, 1);
15401
+ changed_paths.push(ch.path);
15402
+ }
15403
+ continue;
15404
+ }
15405
+
15406
+ if ((ch.action === 'replace' || ch.action === 'create') && typeof ch.new_content === 'string') {
15407
+ const ext = (ch.path.match(/\.([a-z0-9]+)$/i) || [, ''])[1].toLowerCase();
15408
+ 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';
15409
+ const entry = {
15410
+ path: ch.path,
15411
+ content_b64: Buffer.from(ch.new_content, 'utf8').toString('base64'),
15412
+ content_type,
15413
+ };
15414
+ if (idx >= 0) updated_files[idx] = entry;
15415
+ else updated_files.push(entry);
15416
+ changed_paths.push(ch.path);
15417
+ }
15418
+ }
15419
+
15420
+ // Any new attachments uploaded this round join the file set verbatim.
15421
+ for (const entry of ingestion.file_entries) {
15422
+ const idx = _find(entry.path);
15423
+ if (idx >= 0) updated_files[idx] = entry;
15424
+ else updated_files.push(entry);
15425
+ changed_paths.push(entry.path);
15426
+ }
15427
+
15428
+ return {
15429
+ code: 1,
15430
+ data: {
15431
+ updated_files,
15432
+ changed_paths,
15433
+ summary: diff.summary || '',
15434
+ },
15435
+ };
15436
+ } catch (err) {
15437
+ return { code: -400, data: err.message };
15438
+ }
15439
+ };
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
  };
@@ -253,6 +281,22 @@ export const widget_signup_google = async function (...args) {
253
281
  return await broker.send_to_queue("widget_signup_google", ...args);
254
282
  };
255
283
 
284
+ export const site_signup_google = async function (...args) {
285
+ return await broker.send_to_queue("site_signup_google", ...args);
286
+ };
287
+
288
+ export const public_profile_ask_friendship = async function (...args) {
289
+ return await broker.send_to_queue("public_profile_ask_friendship", ...args);
290
+ };
291
+
292
+ export const widget_get_avatar = async function (...args) {
293
+ return await broker.send_to_queue("widget_get_avatar", ...args);
294
+ };
295
+
296
+ export const site_get_avatar = async function (...args) {
297
+ return await broker.send_to_queue("site_get_avatar", ...args);
298
+ };
299
+
256
300
  export const widget_send_message = async function (...args) {
257
301
  return await broker.send_to_queue("widget_send_message", ...args);
258
302
  };
@@ -260,7 +304,3 @@ export const widget_send_message = async function (...args) {
260
304
  export const widget_get_messages = async function (...args) {
261
305
  return await broker.send_to_queue("widget_get_messages", ...args);
262
306
  };
263
-
264
- export const marketplace_review_item = async function (...args) {
265
- return await broker.send_to_queue("marketplace_review_item", ...args);
266
- };
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
  };
@@ -253,6 +281,22 @@ export const widget_signup_google = function (...args) {
253
281
  broker.send_to_queue_async("widget_signup_google", ...args);
254
282
  };
255
283
 
284
+ export const site_signup_google = function (...args) {
285
+ broker.send_to_queue_async("site_signup_google", ...args);
286
+ };
287
+
288
+ export const public_profile_ask_friendship = function (...args) {
289
+ broker.send_to_queue_async("public_profile_ask_friendship", ...args);
290
+ };
291
+
292
+ export const widget_get_avatar = function (...args) {
293
+ broker.send_to_queue_async("widget_get_avatar", ...args);
294
+ };
295
+
296
+ export const site_get_avatar = function (...args) {
297
+ broker.send_to_queue_async("site_get_avatar", ...args);
298
+ };
299
+
256
300
  export const widget_send_message = function (...args) {
257
301
  broker.send_to_queue_async("widget_send_message", ...args);
258
302
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5614",
3
+ "version": "1.1.5616",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",