@xuda.io/account_module 1.2.2310 → 1.2.2312
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 +222 -134
- package/index_ms.mjs +4 -0
- package/index_msa.mjs +4 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -3535,6 +3535,7 @@ export const get_account_name = async function (req) {
|
|
|
3535
3535
|
active_account_profile_id,
|
|
3536
3536
|
network_country_code,
|
|
3537
3537
|
public_profile_disabled,
|
|
3538
|
+
verify_level,
|
|
3538
3539
|
} = data.data.account_info;
|
|
3539
3540
|
|
|
3540
3541
|
obj = {
|
|
@@ -3570,6 +3571,11 @@ export const get_account_name = async function (req) {
|
|
|
3570
3571
|
// email,// email?.[0] || '',
|
|
3571
3572
|
avatar_source,
|
|
3572
3573
|
active_account_profile_id,
|
|
3574
|
+
// The verification level of the account behind a card. Every caller that
|
|
3575
|
+
// builds a contact card off another account copies this onto the card doc
|
|
3576
|
+
// so the background tile can say what the level is; without it a card for
|
|
3577
|
+
// a verified person read as L0, which is a false statement about them.
|
|
3578
|
+
verify_level,
|
|
3573
3579
|
};
|
|
3574
3580
|
}
|
|
3575
3581
|
return { code: 1, data: obj }; //is_online: data?.data?.socket_id ? true : false
|
|
@@ -4305,6 +4311,13 @@ const level_pattern = function (doc) {
|
|
|
4305
4311
|
return `level-${level}-pattern.png`;
|
|
4306
4312
|
};
|
|
4307
4313
|
|
|
4314
|
+
// The level tile is the DEFAULT for every contact card, not a fallback. It used
|
|
4315
|
+
// to be computed first and then overwritten by a tile keyed off where the
|
|
4316
|
+
// contact came from (xu / authentic / fictional / email / web), so a card for a
|
|
4317
|
+
// verified person showed the generic XU tile and the level never appeared on any
|
|
4318
|
+
// card that had a contact_uid, which is every card for a real account. Only the
|
|
4319
|
+
// states that say the card cannot show a level at all still win: a shared card
|
|
4320
|
+
// wears its owner's avatar, an avatar being processed says so, and spam says so.
|
|
4308
4321
|
const get_contact_pattern = async function (doc) {
|
|
4309
4322
|
let ret = level_pattern(doc);
|
|
4310
4323
|
|
|
@@ -4325,84 +4338,12 @@ const get_contact_pattern = async function (doc) {
|
|
|
4325
4338
|
}
|
|
4326
4339
|
} else if (doc.is_spam) {
|
|
4327
4340
|
ret = `spam-pattern.png`;
|
|
4328
|
-
} else {
|
|
4329
|
-
// if (doc.account_type === 'business') {
|
|
4330
|
-
// // if (doc?.is_real_person) {
|
|
4331
|
-
// ret = doc.profile_picture;
|
|
4332
|
-
// // }
|
|
4333
|
-
// } else {
|
|
4334
|
-
// if (doc.my_contact) {
|
|
4335
|
-
// if (doc.account_type === 'business') {
|
|
4336
|
-
// ret = doc.profile_picture;
|
|
4337
|
-
// } else {
|
|
4338
|
-
// switch (doc.avatar_source) {
|
|
4339
|
-
// case 'fictional':
|
|
4340
|
-
// ret = `fictional-avatar-pattern.png`;
|
|
4341
|
-
// break;
|
|
4342
|
-
|
|
4343
|
-
// case 'ai profile':
|
|
4344
|
-
// ret = `fictional-avatar-pattern.png`;
|
|
4345
|
-
// break;
|
|
4346
|
-
|
|
4347
|
-
// case 'authentic profile':
|
|
4348
|
-
// ret = `authentic-avatar-pattern.png`;
|
|
4349
|
-
// break;
|
|
4350
|
-
|
|
4351
|
-
// default:
|
|
4352
|
-
// ret = `xu-pattern.png`;
|
|
4353
|
-
// break;
|
|
4354
|
-
// }
|
|
4355
|
-
// }
|
|
4356
|
-
// } else
|
|
4357
|
-
|
|
4358
|
-
// if (doc?.shared_from_uid) {
|
|
4359
|
-
// //&& (doc.pending || (doc.team_req_id && doc.contact_share_copy))) {
|
|
4360
|
-
// // pending or active share request
|
|
4361
|
-
// const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
|
|
4362
|
-
// ret = shared_from_uid_ret.data.profile_avatar;
|
|
4363
|
-
// }
|
|
4364
|
-
|
|
4365
|
-
// else if (doc?.reference_doc?.shared_from_uid) {
|
|
4366
|
-
// //active share
|
|
4367
|
-
// const shared_from_uid_ret = await get_account_name({ uid_query: doc.reference_doc.shared_from_uid });
|
|
4368
|
-
// ret = shared_from_uid_ret.data.profile_avatar;
|
|
4369
|
-
// }
|
|
4370
|
-
// else
|
|
4371
|
-
if (doc.contact_uid) {
|
|
4372
|
-
switch (doc.avatar_source) {
|
|
4373
|
-
case 'fictional':
|
|
4374
|
-
ret = `fictional-avatar-pattern.png`;
|
|
4375
|
-
break;
|
|
4376
|
-
|
|
4377
|
-
case 'ai profile':
|
|
4378
|
-
ret = `fictional-avatar-pattern.png`;
|
|
4379
|
-
break;
|
|
4380
|
-
|
|
4381
|
-
case 'authentic profile':
|
|
4382
|
-
ret = `authentic-avatar-pattern.png`;
|
|
4383
|
-
break;
|
|
4384
|
-
|
|
4385
|
-
default:
|
|
4386
|
-
ret = `xu-pattern.png`;
|
|
4387
|
-
break;
|
|
4388
|
-
}
|
|
4389
|
-
} else {
|
|
4390
|
-
switch (doc.source) {
|
|
4391
|
-
case 'read emails': {
|
|
4392
|
-
ret = `email-pattern.png`;
|
|
4393
|
-
break;
|
|
4394
|
-
}
|
|
4395
|
-
case 'web': {
|
|
4396
|
-
ret = `web-pattern.png`;
|
|
4397
|
-
break;
|
|
4398
|
-
}
|
|
4399
|
-
|
|
4400
|
-
default:
|
|
4401
|
-
break;
|
|
4402
|
-
}
|
|
4403
|
-
}
|
|
4404
|
-
// }
|
|
4405
4341
|
}
|
|
4342
|
+
// Everything else keeps the level tile. The old where-it-came-from tiles
|
|
4343
|
+
// (xu / authentic / fictional for an account, email / web for a scraped
|
|
4344
|
+
// contact) are gone on purpose: they occupied the one surface that now
|
|
4345
|
+
// carries the verification level, and a contact with no account behind it
|
|
4346
|
+
// has no level to show, which is exactly what L0 says.
|
|
4406
4347
|
return ret;
|
|
4407
4348
|
};
|
|
4408
4349
|
|
|
@@ -4570,6 +4511,10 @@ export const get_contact_info = async function (uid, contact_doc, _id) {
|
|
|
4570
4511
|
}
|
|
4571
4512
|
|
|
4572
4513
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
4514
|
+
// The card's background tile is the contact's verification level, and the
|
|
4515
|
+
// level lives on the account, not on my copy of them. Carry it over here,
|
|
4516
|
+
// where the account is already in hand, so the tile costs no extra read.
|
|
4517
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
4573
4518
|
// doc.icon_pattern = await get_contact_pattern(doc);
|
|
4574
4519
|
doc.username = account_info_ret.data.username;
|
|
4575
4520
|
doc.business_name = account_info_ret.data.business_name;
|
|
@@ -4634,6 +4579,7 @@ export const get_pending_contact_out = async function (uid, _id) {
|
|
|
4634
4579
|
doc.account_type = account_info_ret.data.account_type;
|
|
4635
4580
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
4636
4581
|
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
4582
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
4637
4583
|
doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
4638
4584
|
}
|
|
4639
4585
|
doc.icon_pattern = await get_contact_pattern(doc);
|
|
@@ -4665,6 +4611,7 @@ export const get_pending_contact_in = async function (uid, _id) {
|
|
|
4665
4611
|
doc.account_type = account_info_ret.data.account_type;
|
|
4666
4612
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
4667
4613
|
doc.profile_avatar = account_info_ret.data.profile_avatar;
|
|
4614
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
4668
4615
|
doc.name = `${account_info_ret.data.first_name} ${account_info_ret.data.last_name}`;
|
|
4669
4616
|
}
|
|
4670
4617
|
doc.card_background = get_contact_background({ pending: true });
|
|
@@ -4704,6 +4651,7 @@ const get_user_contact = async function (uid, uid_query) {
|
|
|
4704
4651
|
doc.profile_avatar = account_info.profile_avatar;
|
|
4705
4652
|
doc.profile_picture = account_info.profile_picture;
|
|
4706
4653
|
doc.avatar_source = account_info.avatar_source;
|
|
4654
|
+
doc.verify_level = account_info.verify_level;
|
|
4707
4655
|
doc.card_background = get_contact_background(doc);
|
|
4708
4656
|
doc.icon_pattern = await get_contact_pattern(doc);
|
|
4709
4657
|
doc.border = get_contact_border(doc);
|
|
@@ -5618,28 +5566,18 @@ setTimeout(async () => {
|
|
|
5618
5566
|
|
|
5619
5567
|
//////// CONTACTS //////////////
|
|
5620
5568
|
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
'yahoo.com': 'Yahoo Mail',
|
|
5629
|
-
'ymail.com': 'Yahoo Mail',
|
|
5630
|
-
'aol.com': 'AOL Mail',
|
|
5631
|
-
'icloud.com': 'Apple iCloud',
|
|
5632
|
-
'me.com': 'Apple iCloud',
|
|
5633
|
-
'mac.com': 'Apple iCloud',
|
|
5634
|
-
'protonmail.com': 'ProtonMail',
|
|
5635
|
-
'proton.me': 'ProtonMail',
|
|
5636
|
-
'zoho.com': 'Zoho Mail',
|
|
5637
|
-
'yandex.com': 'Yandex Mail',
|
|
5638
|
-
'mail.com': 'Mail.com',
|
|
5639
|
-
'gmx.com': 'GMX Mail',
|
|
5640
|
-
};
|
|
5569
|
+
// A "business" that resolves to the address's own mail provider is not the contact's
|
|
5570
|
+
// employer, it is Gmail. get_business_info, handed a bare gmail.com address, answers
|
|
5571
|
+
// "Gmail / gmail.com / Technology" rather than "Not available", so the only escape hatch the
|
|
5572
|
+
// classification had (business_name === 'Not available') never fired and every personal
|
|
5573
|
+
// Gmail contact came out as a business, storefront placeholder and all. This is the second
|
|
5574
|
+
// escape hatch, built on the provider list that already exists directly below.
|
|
5575
|
+
const businessIsJustTheMailProvider = (business_info) => !!identifyPersonalProvider(business_info?.business_domain || '');
|
|
5641
5576
|
|
|
5642
|
-
|
|
5577
|
+
function identifyPersonalProvider(domain) {
|
|
5578
|
+
// The list moved to common so ai_module's contact classifier reads the same one. It used to
|
|
5579
|
+
// live here alone, which is exactly why the classifier could call gmail.com a business.
|
|
5580
|
+
return _common.personal_email_provider(domain);
|
|
5643
5581
|
}
|
|
5644
5582
|
|
|
5645
5583
|
export const add_contact = async function (req, job_id, headers) {
|
|
@@ -5714,21 +5652,24 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5714
5652
|
await activity('spam_check', { by: 'ai', is_spam: !!is_spam, subject: metadata.subject });
|
|
5715
5653
|
}
|
|
5716
5654
|
if (!is_spam) {
|
|
5717
|
-
const
|
|
5655
|
+
const verdict = await ai_ms.is_business_contact(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
5656
|
+
// Tolerates the old bare boolean, in case a stale ai_module answers this call.
|
|
5657
|
+
const is_business = typeof verdict === 'object' && verdict !== null ? verdict.is_business : !!verdict;
|
|
5718
5658
|
account_type = is_business ? 'business' : 'personal';
|
|
5719
|
-
await activity('classified', { by: 'ai', account_type, inputs: { email: email.toLowerCase(), name: name || '', subject: metadata.subject || '' } });
|
|
5659
|
+
await activity('classified', { by: 'ai', account_type, confidence: verdict?.confidence, why: verdict?.reason, inputs: { email: email.toLowerCase(), name: name || '', subject: metadata.subject || '' } });
|
|
5720
5660
|
if (is_business) {
|
|
5721
5661
|
business_info = await ai_ms.get_business_info(uid, '', email, account_profile_info, { light: true });
|
|
5722
|
-
if (business_info.business_name !== 'Not available') {
|
|
5662
|
+
if (business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(business_info)) {
|
|
5723
5663
|
await activity('business_info', { by: 'ai', depth: 'light', business_name: business_info.business_name, business_domain: business_info.business_domain, business_category: business_info.business_category });
|
|
5724
5664
|
business_has_person = await ai_ms.is_business_contact_has_person(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
5725
5665
|
await activity('business_has_person', { by: 'ai', has_person: !!business_has_person });
|
|
5726
5666
|
} else {
|
|
5667
|
+
const provider = identifyPersonalProvider(business_info?.business_domain || '');
|
|
5727
5668
|
account_type = 'personal';
|
|
5728
5669
|
business_info = undefined;
|
|
5729
5670
|
// The reversal matters more than the original verdict: the card says
|
|
5730
5671
|
// personal even though the business call answered yes.
|
|
5731
|
-
await activity('classified', { by: 'ai', account_type, reverted_from: 'business', reason: 'no business could be identified behind the address' });
|
|
5672
|
+
await activity('classified', { by: 'ai', account_type, reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'no business could be identified behind the address' });
|
|
5732
5673
|
}
|
|
5733
5674
|
}
|
|
5734
5675
|
if (_.isEmpty(cached_contact) && account_type === 'business' && !business_has_person) {
|
|
@@ -6372,19 +6313,31 @@ export const not_spam_contact = async function (req, job_id, headers) {
|
|
|
6372
6313
|
}
|
|
6373
6314
|
|
|
6374
6315
|
const previous_account_type = contact_doc.account_type;
|
|
6375
|
-
const
|
|
6316
|
+
const verdict = await ai_ms.is_business_contact(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
|
|
6317
|
+
const is_business = typeof verdict === 'object' && verdict !== null ? verdict.is_business : !!verdict;
|
|
6376
6318
|
contact_doc.account_type = is_business ? 'business' : 'personal';
|
|
6377
|
-
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', trigger: 'not spam', account_type: contact_doc.account_type, previous_account_type: previous_account_type || 'unset' });
|
|
6319
|
+
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', trigger: 'not spam', account_type: contact_doc.account_type, confidence: verdict?.confidence, why: verdict?.reason, previous_account_type: previous_account_type || 'unset' });
|
|
6320
|
+
if (!is_business && (contact_doc.business_info || contact_doc.business_has_person)) {
|
|
6321
|
+
// A contact that reclassifies from business to person used to keep the company profile
|
|
6322
|
+
// the old verdict attached to it, because business_info is only ever written inside the
|
|
6323
|
+
// is_business branch and nothing clears it on the way out. The card then showed a person
|
|
6324
|
+
// with a company still on file.
|
|
6325
|
+
const dropped = contact_doc?.business_info?.business_name;
|
|
6326
|
+
contact_doc.business_info = undefined;
|
|
6327
|
+
contact_doc.business_has_person = undefined;
|
|
6328
|
+
await log_contact_activity(uid, contact_id, 'business_info_cleared', { reason: 'contact reclassified as a person', dropped });
|
|
6329
|
+
}
|
|
6378
6330
|
if (is_business) {
|
|
6379
6331
|
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info, { light: true });
|
|
6380
|
-
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
6332
|
+
if (contact_doc.business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(contact_doc.business_info)) {
|
|
6381
6333
|
await log_contact_activity(uid, contact_id, 'business_info', { by: 'ai', depth: 'light', business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
|
|
6382
6334
|
contact_doc.business_has_person = await ai_ms.is_business_contact_has_person(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
|
|
6383
6335
|
await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
|
|
6384
6336
|
} else {
|
|
6337
|
+
const provider = identifyPersonalProvider(contact_doc.business_info?.business_domain || '');
|
|
6385
6338
|
contact_doc.account_type = 'personal';
|
|
6386
6339
|
contact_doc.business_info = undefined;
|
|
6387
|
-
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: 'no business could be identified behind the address' });
|
|
6340
|
+
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'no business could be identified behind the address' });
|
|
6388
6341
|
}
|
|
6389
6342
|
}
|
|
6390
6343
|
|
|
@@ -6471,14 +6424,15 @@ export const set_deep_research_contact = async function (req, job_id, headers) {
|
|
|
6471
6424
|
|
|
6472
6425
|
if (contact_doc.account_type === 'business') {
|
|
6473
6426
|
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info);
|
|
6474
|
-
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
6427
|
+
if (contact_doc.business_info.business_name !== 'Not available' && !businessIsJustTheMailProvider(contact_doc.business_info)) {
|
|
6475
6428
|
await log_contact_activity(uid, contact_id, 'business_info', { by: 'ai', depth: 'deep', business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
|
|
6476
6429
|
contact_doc.business_has_person = await ai_ms.is_business_contact_has_person(uid, contact_doc.email, contact_doc.name, contact_doc?.metadata?.subject, contact_doc?.metadata?.summarized_body, account_profile_info);
|
|
6477
6430
|
await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
|
|
6478
6431
|
} else {
|
|
6432
|
+
const provider = identifyPersonalProvider(contact_doc.business_info?.business_domain || '');
|
|
6479
6433
|
contact_doc.account_type = 'personal';
|
|
6480
6434
|
contact_doc.business_info = undefined;
|
|
6481
|
-
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: 'deep research found no business behind the address' });
|
|
6435
|
+
await log_contact_activity(uid, contact_id, 'classified', { by: 'ai', account_type: 'personal', reverted_from: 'business', reason: provider ? `the only business behind the address is its mail provider (${provider})` : 'deep research found no business behind the address' });
|
|
6482
6436
|
}
|
|
6483
6437
|
}
|
|
6484
6438
|
|
|
@@ -6568,7 +6522,16 @@ export const get_contact_activity = async function (req) {
|
|
|
6568
6522
|
try {
|
|
6569
6523
|
if (!contact_id) throw new Error('contact_id is missing');
|
|
6570
6524
|
const account_profile_info = await get_active_account_profile_info(uid);
|
|
6571
|
-
|
|
6525
|
+
// get_app_couch_doc_native THROWS a bare couch 'missing' for an id that is not in
|
|
6526
|
+
// this account's app db (an id from another account is the common way to get here),
|
|
6527
|
+
// so the not-found check below never ran and the caller saw "missing" with nothing
|
|
6528
|
+
// to act on. Catch it and answer the question that was actually asked.
|
|
6529
|
+
let contact_doc;
|
|
6530
|
+
try {
|
|
6531
|
+
contact_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
|
|
6532
|
+
} catch (_) {
|
|
6533
|
+
contact_doc = null;
|
|
6534
|
+
}
|
|
6572
6535
|
if (!contact_doc || contact_doc.docType !== 'contact') throw new Error(`contact ${contact_id} not found`);
|
|
6573
6536
|
if (contact_doc.uid !== uid && contact_doc?.uid_created !== uid) throw new Error('Operation not allowed');
|
|
6574
6537
|
|
|
@@ -6688,7 +6651,11 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
6688
6651
|
|
|
6689
6652
|
const get_pattern = async function (doc) {
|
|
6690
6653
|
// Same level-based default as get_contact_pattern, so a card shows the same
|
|
6691
|
-
// verification tile whichever path built it
|
|
6654
|
+
// verification tile whichever path built it, and the same two states that
|
|
6655
|
+
// outrank a level: a shared profile wears its owner's avatar, spam says
|
|
6656
|
+
// spam. A business profile used to paint its own logo across the card face,
|
|
6657
|
+
// which is why a verified business showed no level at all; the logo is
|
|
6658
|
+
// already the avatar on the same card, so the tile is the level here too.
|
|
6692
6659
|
let ret = level_pattern(doc);
|
|
6693
6660
|
|
|
6694
6661
|
if (doc.shared_from_uid) {
|
|
@@ -6697,26 +6664,6 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
6697
6664
|
ret = shared_from_uid_ret.data.profile_avatar;
|
|
6698
6665
|
} else if (doc.is_spam) {
|
|
6699
6666
|
ret = `spam-pattern.png`;
|
|
6700
|
-
} else if (doc.account_type === 'business') {
|
|
6701
|
-
ret = doc.profile_picture;
|
|
6702
|
-
} else {
|
|
6703
|
-
switch (doc.avatar_source) {
|
|
6704
|
-
case 'fictional':
|
|
6705
|
-
ret = `fictional-avatar-pattern.png`;
|
|
6706
|
-
break;
|
|
6707
|
-
|
|
6708
|
-
case 'ai profile':
|
|
6709
|
-
ret = `fictional-avatar-pattern.png`;
|
|
6710
|
-
break;
|
|
6711
|
-
|
|
6712
|
-
case 'authentic profile':
|
|
6713
|
-
ret = `authentic-avatar-pattern.png`;
|
|
6714
|
-
break;
|
|
6715
|
-
|
|
6716
|
-
default:
|
|
6717
|
-
ret = `xu-pattern.png`;
|
|
6718
|
-
break;
|
|
6719
|
-
}
|
|
6720
6667
|
}
|
|
6721
6668
|
|
|
6722
6669
|
return ret;
|
|
@@ -6774,6 +6721,10 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
6774
6721
|
// console.log(stringToColour('Project Z')); // -> Consistent color for "Project Z"
|
|
6775
6722
|
|
|
6776
6723
|
doc.avatar_source = account_info_ret.data.avatar_source;
|
|
6724
|
+
// A profile inherits the verification level of the account that owns it (a
|
|
6725
|
+
// shared profile, the account that shared it), so its card reads the same
|
|
6726
|
+
// level as its owner's contact card instead of falling back to L0.
|
|
6727
|
+
doc.verify_level = account_info_ret.data.verify_level;
|
|
6777
6728
|
doc.icon_pattern = await get_pattern(doc);
|
|
6778
6729
|
doc.name = doc.profile_name;
|
|
6779
6730
|
doc.interactions = 0;
|
|
@@ -7888,9 +7839,11 @@ const _list_owner_members = async function (owner_uid, profiles) {
|
|
|
7888
7839
|
const acc = await db_module.get_couch_doc_native('xuda_accounts', u);
|
|
7889
7840
|
const ai = acc?.account_info || {};
|
|
7890
7841
|
const name = ai.account_type === 'business' ? ai.business_name || 'Member' : `${ai.first_name || ''} ${ai.last_name || ''}`.trim() || ai.email || 'Member';
|
|
7891
|
-
|
|
7842
|
+
// owner is reported (UI-163) so the member card can say so without the
|
|
7843
|
+
// dashboard having to infer it from the order of this list.
|
|
7844
|
+
out.push({ uid: u, name, owner: u === owner_uid });
|
|
7892
7845
|
} catch (e) {
|
|
7893
|
-
out.push({ uid: u, name: 'Member' });
|
|
7846
|
+
out.push({ uid: u, name: 'Member', owner: u === owner_uid });
|
|
7894
7847
|
}
|
|
7895
7848
|
}
|
|
7896
7849
|
return out;
|
|
@@ -7931,7 +7884,139 @@ export const get_credit_management = async function (req) {
|
|
|
7931
7884
|
const profiles = await _list_owner_profiles(uid, app_id, account_doc?.account_profile_id);
|
|
7932
7885
|
const members = await _list_owner_members(uid, profiles);
|
|
7933
7886
|
|
|
7934
|
-
|
|
7887
|
+
// main_profile_id is reported (UI-163) so the spend chart can name the
|
|
7888
|
+
// owner's own profile: it is deliberately absent from `profiles` (no member
|
|
7889
|
+
// to scope a rule to), and without the id its spend can only be labelled
|
|
7890
|
+
// "some other profile".
|
|
7891
|
+
return { code: 1, data: { rules, can_edit, plan, entitlements, usage_breakdown, model_catalog, profiles, members, main_profile_id: account_doc?.account_profile_id || '' } };
|
|
7892
|
+
} catch (err) {
|
|
7893
|
+
return { code: -1, data: err.message };
|
|
7894
|
+
}
|
|
7895
|
+
};
|
|
7896
|
+
|
|
7897
|
+
/////////////////////////////////////////////////////////////////////////
|
|
7898
|
+
// USAGE HISTORY (UI-163)
|
|
7899
|
+
//
|
|
7900
|
+
// The drill-down behind the "N credits used" line on a profile or member card:
|
|
7901
|
+
// day by day spend, which models it went on, and the calls themselves. It reads
|
|
7902
|
+
// the same raw ai_usage docs _get_scoped_usage aggregates, and calibrates them
|
|
7903
|
+
// the same way, so a card's total and its history cannot tell two stories.
|
|
7904
|
+
//
|
|
7905
|
+
// Calibration: raw token cost is not the credit unit the ledger reports, so the
|
|
7906
|
+
// scan also covers the CURRENT PERIOD and the two totals give the conversion
|
|
7907
|
+
// factor (authoritative period spend / raw period cost). That is a unit
|
|
7908
|
+
// conversion, so it holds for any window inside the scan.
|
|
7909
|
+
/////////////////////////////////////////////////////////////////////////
|
|
7910
|
+
const _CREDIT_HISTORY_MAX_DOCS = 20000;
|
|
7911
|
+
const _CREDIT_HISTORY_MAX_DAYS = 180;
|
|
7912
|
+
const _CREDIT_HISTORY_SCAN_DAYS = 120;
|
|
7913
|
+
const _DAY_MS = 86400000;
|
|
7914
|
+
const _utc_day = function (ts) {
|
|
7915
|
+
return new Date(ts).toISOString().slice(0, 10);
|
|
7916
|
+
};
|
|
7917
|
+
const _raw_usage_cost = function (doc) {
|
|
7918
|
+
const c = doc?.cost || { input: 1, output: 1 };
|
|
7919
|
+
return ((Number(doc?.input_tokens) || 0) * (Number(c.input) || 0) + (Number(doc?.output_tokens) || 0) * (Number(c.output) || 0)) / 1e6;
|
|
7920
|
+
};
|
|
7921
|
+
|
|
7922
|
+
export const get_credit_usage_history = async function (req) {
|
|
7923
|
+
try {
|
|
7924
|
+
const uid = req?.uid || req?.token_ret?.data?.uid;
|
|
7925
|
+
if (!uid) return { code: -1, data: 'not authorized' };
|
|
7926
|
+
|
|
7927
|
+
const scope = ['profile', 'user'].includes(req?.scope) ? req.scope : 'account';
|
|
7928
|
+
const id = typeof req?.id === 'string' ? req.id : '';
|
|
7929
|
+
if (scope !== 'account' && !id) return { code: -1, data: 'id is required for a profile or member history' };
|
|
7930
|
+
|
|
7931
|
+
let days = Math.round(Number(req?.days) || 30);
|
|
7932
|
+
if (!(days >= 1)) days = 30;
|
|
7933
|
+
days = Math.min(_CREDIT_HISTORY_MAX_DAYS, days);
|
|
7934
|
+
|
|
7935
|
+
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
7936
|
+
const period = _current_credit_period(account_doc);
|
|
7937
|
+
|
|
7938
|
+
// UTC day buckets, so the series is the same series wherever it is read.
|
|
7939
|
+
const today_start = Math.floor(Date.now() / _DAY_MS) * _DAY_MS;
|
|
7940
|
+
const window_from = today_start - (days - 1) * _DAY_MS;
|
|
7941
|
+
// Reach back to the period start even when the window is shorter (the factor
|
|
7942
|
+
// needs it), but never further than the scan bound: a yearly period must not
|
|
7943
|
+
// turn one drill-down into a full-history scan.
|
|
7944
|
+
const scan_from = Math.max(Math.min(window_from, period.start), today_start - _CREDIT_HISTORY_SCAN_DAYS * _DAY_MS);
|
|
7945
|
+
|
|
7946
|
+
// ignore_warning=true: same nested selector as _get_scoped_usage, and the
|
|
7947
|
+
// same index note applies (an index on account_profile_info.uid before any
|
|
7948
|
+
// prod rollout).
|
|
7949
|
+
const raw = await db_module.find_couch_query(
|
|
7950
|
+
'xuda_usage',
|
|
7951
|
+
{
|
|
7952
|
+
selector: { docType: 'ai_usage', 'account_profile_info.uid': uid, date_created_ts: { $gte: scan_from } },
|
|
7953
|
+
fields: ['date_created_ts', 'uid', 'model', 'source', 'prompt', 'input_tokens', 'output_tokens', 'cost', 'account_profile_info.account_profile_id'],
|
|
7954
|
+
limit: _CREDIT_HISTORY_MAX_DOCS,
|
|
7955
|
+
},
|
|
7956
|
+
true,
|
|
7957
|
+
);
|
|
7958
|
+
const docs = raw?.docs || [];
|
|
7959
|
+
|
|
7960
|
+
let raw_period_total = 0;
|
|
7961
|
+
for (const d of docs) if ((Number(d.date_created_ts) || 0) >= period.start) raw_period_total += _raw_usage_cost(d);
|
|
7962
|
+
const su = await _get_scoped_usage(uid, false);
|
|
7963
|
+
const factor = raw_period_total > 0 && su.total > 0 ? su.total / raw_period_total : 1;
|
|
7964
|
+
|
|
7965
|
+
const in_scope = function (d) {
|
|
7966
|
+
if (scope === 'profile') return d?.account_profile_info?.account_profile_id === id;
|
|
7967
|
+
if (scope === 'user') return d?.uid === id;
|
|
7968
|
+
return true;
|
|
7969
|
+
};
|
|
7970
|
+
|
|
7971
|
+
const series = {};
|
|
7972
|
+
for (let t = window_from; t <= today_start; t += _DAY_MS) series[_utc_day(t)] = 0;
|
|
7973
|
+
|
|
7974
|
+
const by_model = {};
|
|
7975
|
+
const events = [];
|
|
7976
|
+
let total = 0;
|
|
7977
|
+
for (const d of docs) {
|
|
7978
|
+
const ts = Number(d.date_created_ts) || 0;
|
|
7979
|
+
if (ts < window_from || !in_scope(d)) continue;
|
|
7980
|
+
const credits = _raw_usage_cost(d) * factor;
|
|
7981
|
+
total += credits;
|
|
7982
|
+
const day = _utc_day(ts);
|
|
7983
|
+
if (day in series) series[day] += credits;
|
|
7984
|
+
const code = _conf.ai_model_aliases?.[d.model] || d.model || 'unknown';
|
|
7985
|
+
by_model[code] = (by_model[code] || 0) + credits;
|
|
7986
|
+
events.push({
|
|
7987
|
+
ts,
|
|
7988
|
+
credits,
|
|
7989
|
+
model: code,
|
|
7990
|
+
model_name: _conf.ai_models?.[code]?.name || d.model || code,
|
|
7991
|
+
source: d.source || 'other',
|
|
7992
|
+
uid: d.uid || '',
|
|
7993
|
+
account_profile_id: d?.account_profile_info?.account_profile_id || '',
|
|
7994
|
+
prompt: typeof d.prompt === 'string' ? d.prompt : '',
|
|
7995
|
+
});
|
|
7996
|
+
}
|
|
7997
|
+
events.sort((a, b) => b.ts - a.ts);
|
|
7998
|
+
|
|
7999
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
8000
|
+
return {
|
|
8001
|
+
code: 1,
|
|
8002
|
+
data: {
|
|
8003
|
+
scope,
|
|
8004
|
+
id,
|
|
8005
|
+
days,
|
|
8006
|
+
total: r2(total),
|
|
8007
|
+
calls: events.length,
|
|
8008
|
+
// The scan is capped, so say so rather than quietly reporting a partial
|
|
8009
|
+
// history as the whole of it.
|
|
8010
|
+
truncated: docs.length >= _CREDIT_HISTORY_MAX_DOCS,
|
|
8011
|
+
series: Object.keys(series)
|
|
8012
|
+
.sort()
|
|
8013
|
+
.map((day) => ({ day, credits: r2(series[day]) })),
|
|
8014
|
+
by_model: Object.entries(by_model)
|
|
8015
|
+
.map(([code, credits]) => ({ code, name: _conf.ai_models?.[code]?.name || code, credits: r2(credits) }))
|
|
8016
|
+
.sort((a, b) => b.credits - a.credits),
|
|
8017
|
+
events: events.slice(0, 60).map((e) => ({ ...e, credits: Math.round(e.credits * 10000) / 10000 })),
|
|
8018
|
+
},
|
|
8019
|
+
};
|
|
7935
8020
|
} catch (err) {
|
|
7936
8021
|
return { code: -1, data: err.message };
|
|
7937
8022
|
}
|
|
@@ -9205,7 +9290,10 @@ const _newsletter_blast = async (issue, by_uid) => {
|
|
|
9205
9290
|
} catch (e) {
|
|
9206
9291
|
console.error('[newsletter] finalize failed:', e.message);
|
|
9207
9292
|
}
|
|
9208
|
-
|
|
9293
|
+
// No ops notification on completion: the "Publishing newsletter #N" alert
|
|
9294
|
+
// already announces the blast, and the result lands on the issue doc
|
|
9295
|
+
// (sent.count / sent.failed) plus the log line below.
|
|
9296
|
+
console.log(`[newsletter] #${issue.issue_number} published: sent=${sent} failed=${failed} recipients=${total}`);
|
|
9209
9297
|
};
|
|
9210
9298
|
|
|
9211
9299
|
export const newsletter_publish = async function (req = {}) {
|
package/index_ms.mjs
CHANGED
|
@@ -541,6 +541,10 @@ export const get_credit_management = async function (...args) {
|
|
|
541
541
|
return await broker.send_to_queue("get_credit_management", ...args);
|
|
542
542
|
};
|
|
543
543
|
|
|
544
|
+
export const get_credit_usage_history = async function (...args) {
|
|
545
|
+
return await broker.send_to_queue("get_credit_usage_history", ...args);
|
|
546
|
+
};
|
|
547
|
+
|
|
544
548
|
export const set_credit_rules = async function (...args) {
|
|
545
549
|
return await broker.send_to_queue("set_credit_rules", ...args);
|
|
546
550
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -541,6 +541,10 @@ export const get_credit_management = function (...args) {
|
|
|
541
541
|
broker.send_to_queue_async("get_credit_management", ...args);
|
|
542
542
|
};
|
|
543
543
|
|
|
544
|
+
export const get_credit_usage_history = function (...args) {
|
|
545
|
+
broker.send_to_queue_async("get_credit_usage_history", ...args);
|
|
546
|
+
};
|
|
547
|
+
|
|
544
548
|
export const set_credit_rules = function (...args) {
|
|
545
549
|
broker.send_to_queue_async("set_credit_rules", ...args);
|
|
546
550
|
};
|