@xuda.io/ai_module 1.1.5615 → 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,10 +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
- // 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
+ // 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',
2092
2093
  });
2093
2094
  report_ai_status(model);
2094
2095
  } catch (err) {
@@ -8338,7 +8339,9 @@ export const get_profile_picture = async function (uid, account_type = 'business
8338
8339
  }
8339
8340
 
8340
8341
  if (factual_image_mode) {
8341
- 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');
8342
8345
  } else {
8343
8346
  const model = 'gpt-image-1-mini'; // 'chatgpt-image-latest';
8344
8347
  const prompt = `
@@ -14135,7 +14138,7 @@ const _widget_verify_google_id_token = async function (id_token) {
14135
14138
  return payload;
14136
14139
  };
14137
14140
 
14138
- 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) {
14139
14142
  const ret = await db_module.find_couch_query('xuda_accounts', {
14140
14143
  selector: { 'account_info.email': email },
14141
14144
  fields: ['_id'],
@@ -14143,14 +14146,26 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub)
14143
14146
  });
14144
14147
  if (ret.docs && ret.docs.length) {
14145
14148
  // If we have a google_sub and the existing account lacks one, attach it.
14146
- 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) {
14147
14152
  try {
14148
14153
  const acct_ret = await db_module.get_couch_doc('xuda_accounts', ret.docs[0]._id);
14149
14154
  if (acct_ret.code > -1) {
14150
14155
  const acct = acct_ret.data;
14151
- if (!acct.google_sub) {
14156
+ let dirty = false;
14157
+ if (google_sub && !acct.google_sub) {
14152
14158
  acct.google_sub = google_sub;
14153
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) {
14154
14169
  acct.ts = Date.now();
14155
14170
  await db_module.save_couch_doc('xuda_accounts', acct);
14156
14171
  }
@@ -14164,6 +14179,13 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub)
14164
14179
  const secret = _utils.get_id('tmp_pass', 5);
14165
14180
  const hash = _utils.hash(secret);
14166
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
+ }
14167
14189
  const doc = {
14168
14190
  _id: await _common.xuda_get_uuid('account'),
14169
14191
  stat: 1,
@@ -14171,7 +14193,7 @@ const _widget_find_or_create_visitor = async function (email, name, google_sub)
14171
14193
  source: 'widget',
14172
14194
  password: hash,
14173
14195
  email,
14174
- account_info: { email, username: _utils.get_id('xu'), first_name, last_name, full_name: name },
14196
+ account_info,
14175
14197
  };
14176
14198
  if (google_sub) {
14177
14199
  doc.google_sub = google_sub;
@@ -14332,7 +14354,7 @@ export const get_widget_embed_snippet = async function (req, job_id, headers) {
14332
14354
  };
14333
14355
 
14334
14356
  const _widget_signup_core = async function (req, job_id, headers, opts) {
14335
- 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;
14336
14358
  const [owner_uid_part, pid_part] = String(profile_id || '').split('.');
14337
14359
  if (!owner_uid_part || !pid_part) return { code: -404, data: 'invalid_profile' };
14338
14360
 
@@ -14343,8 +14365,17 @@ const _widget_signup_core = async function (req, job_id, headers, opts) {
14343
14365
  const ap_doc = account_profile_info.account_profile_obj || {};
14344
14366
  if (ap_doc.widget_enabled === false) return { code: -404, data: 'widget_disabled' };
14345
14367
 
14346
- const visitor_uid = await _widget_find_or_create_visitor(email, name, google_sub);
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);
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);
14348
14379
  };
14349
14380
 
14350
14381
  export const widget_signup = async function (req, job_id, headers) {
@@ -14379,14 +14410,177 @@ export const widget_signup_google = async function (req, job_id, headers) {
14379
14410
  .trim();
14380
14411
  const name = String(payload.name || payload.email?.split('@')[0] || 'Friend').trim();
14381
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);
14382
14448
 
14383
- return await _widget_signup_core({ profile_id, email, name, widget_origin, visitor_lang, google_sub: payload.sub, signup_method: 'google' }, job_id, headers);
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
+ };
14384
14470
  } catch (err) {
14385
14471
  return { code: -1, data: err.message || String(err) };
14386
14472
  }
14387
14473
  };
14388
14474
 
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) {
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;
14531
+
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 } };
14578
+ } catch (err) {
14579
+ return { code: -1, data: err.message || String(err) };
14580
+ }
14581
+ };
14582
+
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) {
14390
14584
  try {
14391
14585
  let team_req_doc = await _widget_find_existing_team_req(visitor_uid, canonical_profile_id);
14392
14586
  let assigned_uid;
@@ -14498,6 +14692,9 @@ const _widget_signup_finalize = async function (visitor_uid, email, name, widget
14498
14692
  greeting,
14499
14693
  conversation_id,
14500
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,
14501
14698
  signup_method: signup_method || 'email',
14502
14699
  account_active: signup_method === 'google',
14503
14700
  signin_url: signup_method === 'google' ? `https://${_conf.domain || 'xuda.ai'}/dashboard/login` : undefined,
@@ -14508,6 +14705,59 @@ const _widget_signup_finalize = async function (visitor_uid, email, name, widget
14508
14705
  }
14509
14706
  };
14510
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
+
14511
14761
  export const widget_send_message = async function (req, job_id, headers) {
14512
14762
  try {
14513
14763
  const { widget_token, prompt = '', attachments = [] } = req;
package/index_ms.mjs CHANGED
@@ -281,6 +281,22 @@ export const widget_signup_google = async function (...args) {
281
281
  return await broker.send_to_queue("widget_signup_google", ...args);
282
282
  };
283
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
+
284
300
  export const widget_send_message = async function (...args) {
285
301
  return await broker.send_to_queue("widget_send_message", ...args);
286
302
  };
package/index_msa.mjs CHANGED
@@ -281,6 +281,22 @@ export const widget_signup_google = function (...args) {
281
281
  broker.send_to_queue_async("widget_signup_google", ...args);
282
282
  };
283
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
+
284
300
  export const widget_send_message = function (...args) {
285
301
  broker.send_to_queue_async("widget_send_message", ...args);
286
302
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5615",
3
+ "version": "1.1.5616",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",