@xuda.io/account_module 1.2.2309 → 1.2.2310
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 +187 -2
- package/index_ms.mjs +4 -0
- package/index_msa.mjs +4 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -4247,6 +4247,7 @@ export const merge_contact = async function (req) {
|
|
|
4247
4247
|
}
|
|
4248
4248
|
|
|
4249
4249
|
const primary_contact_save_ret = await save_contact(uid, primary_contact_doc);
|
|
4250
|
+
log_contact_activity(uid, contact_to_merge, 'merged_in', { count: contact_to_delete.length, emails: primary_contact_doc.email, note: 'kept as the primary of a duplicate set' });
|
|
4250
4251
|
|
|
4251
4252
|
for (let contact_id of contact_to_delete) {
|
|
4252
4253
|
try {
|
|
@@ -4255,6 +4256,7 @@ export const merge_contact = async function (req) {
|
|
|
4255
4256
|
contact_doc.stat_ts = Date.now();
|
|
4256
4257
|
contact_doc.stat_reason = 'merge';
|
|
4257
4258
|
await save_contact(uid, contact_doc);
|
|
4259
|
+
log_contact_activity(uid, contact_id, 'merged_away', { into: contact_to_merge, note: 'folded into the primary duplicate and closed' });
|
|
4258
4260
|
} catch (error) {}
|
|
4259
4261
|
}
|
|
4260
4262
|
|
|
@@ -4753,7 +4755,9 @@ export const update_contact = async function (req) {
|
|
|
4753
4755
|
doc.profile_avatar = account_obj.account_info.profile_avatar;
|
|
4754
4756
|
}
|
|
4755
4757
|
|
|
4756
|
-
|
|
4758
|
+
const save_ret = await save_contact(uid, doc);
|
|
4759
|
+
log_contact_activity(uid, doc._id, 'updated', { stat, team_req_id, linked_account: contact_uid, note: contact_uid ? 'picture and avatar taken from the linked Xuda account' : undefined });
|
|
4760
|
+
return save_ret;
|
|
4757
4761
|
}
|
|
4758
4762
|
return { code: -1, data: 'contact not found' };
|
|
4759
4763
|
};
|
|
@@ -5663,6 +5667,16 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5663
5667
|
|
|
5664
5668
|
/////////////////
|
|
5665
5669
|
|
|
5670
|
+
// Minted up front rather than at save time, so every classification step
|
|
5671
|
+
// below can file its verdict under the contact it is deciding about. The
|
|
5672
|
+
// steps run BEFORE the doc exists, which is exactly why their reasoning
|
|
5673
|
+
// used to be unrecoverable.
|
|
5674
|
+
const contact_id = await _common.xuda_get_uuid('contact');
|
|
5675
|
+
const app_id = account_profile_info.app_id;
|
|
5676
|
+
const activity = (event, detail) => log_contact_activity(uid, contact_id, event, detail, app_id);
|
|
5677
|
+
|
|
5678
|
+
await activity('created', { source: source || 'unknown', email: email.toLowerCase(), name: name || '', from_account: !!contact_uid, subject: metadata.subject, profile_id: account_profile_obj?._id });
|
|
5679
|
+
|
|
5666
5680
|
let is_spam = false;
|
|
5667
5681
|
// let email_type_info;
|
|
5668
5682
|
// let account_type_info;
|
|
@@ -5679,27 +5693,42 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5679
5693
|
}
|
|
5680
5694
|
|
|
5681
5695
|
set_account_profile_picture(uid, contact_uid, metadata, job_id, headers, account_profile_info);
|
|
5696
|
+
await activity('linked_account', { contact_uid });
|
|
5682
5697
|
} else {
|
|
5683
5698
|
is_spam = await isLikelySpamEmail(email);
|
|
5699
|
+
if (is_spam) await activity('spam_check', { by: 'pattern', is_spam: true, note: 'address matched the local spam pattern list, no AI step ran' });
|
|
5684
5700
|
|
|
5685
5701
|
// contact without use account
|
|
5686
5702
|
cached_contact = await get_xuda_cache(uid, 'contact', email, 'metadata');
|
|
5703
|
+
if (!_.isEmpty(cached_contact)) {
|
|
5704
|
+
// Every classification below is SKIPPED on a cache hit, which is the single
|
|
5705
|
+
// most confusing thing about the result: the contact inherits a verdict that
|
|
5706
|
+
// was reached for an earlier contact on the same address or domain.
|
|
5707
|
+
await activity('classification_reused', { from_cache: true, key: email.toLowerCase(), account_type: cached_contact.account_type, business_name: cached_contact?.business_info?.business_name, is_spam: cached_contact.is_spam });
|
|
5708
|
+
}
|
|
5687
5709
|
|
|
5688
5710
|
if (_.isEmpty(cached_contact)) {
|
|
5689
5711
|
if (!is_spam && metadata.subject && !metadata.is_sent && !metadata.is_answered && !metadata.not_junk) {
|
|
5690
5712
|
// deep search for spam
|
|
5691
5713
|
is_spam = await ai_ms.is_spam_email(uid, email, metadata.subject, account_profile_info);
|
|
5714
|
+
await activity('spam_check', { by: 'ai', is_spam: !!is_spam, subject: metadata.subject });
|
|
5692
5715
|
}
|
|
5693
5716
|
if (!is_spam) {
|
|
5694
5717
|
const is_business = await ai_ms.is_business_contact(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
5695
5718
|
account_type = is_business ? 'business' : 'personal';
|
|
5719
|
+
await activity('classified', { by: 'ai', account_type, inputs: { email: email.toLowerCase(), name: name || '', subject: metadata.subject || '' } });
|
|
5696
5720
|
if (is_business) {
|
|
5697
5721
|
business_info = await ai_ms.get_business_info(uid, '', email, account_profile_info, { light: true });
|
|
5698
5722
|
if (business_info.business_name !== 'Not available') {
|
|
5723
|
+
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 });
|
|
5699
5724
|
business_has_person = await ai_ms.is_business_contact_has_person(uid, email, name, metadata.subject, metadata.summarized_body, account_profile_info);
|
|
5725
|
+
await activity('business_has_person', { by: 'ai', has_person: !!business_has_person });
|
|
5700
5726
|
} else {
|
|
5701
5727
|
account_type = 'personal';
|
|
5702
5728
|
business_info = undefined;
|
|
5729
|
+
// The reversal matters more than the original verdict: the card says
|
|
5730
|
+
// 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' });
|
|
5703
5732
|
}
|
|
5704
5733
|
}
|
|
5705
5734
|
if (_.isEmpty(cached_contact) && account_type === 'business' && !business_has_person) {
|
|
@@ -5708,6 +5737,9 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5708
5737
|
} else {
|
|
5709
5738
|
cached_contact = await get_xuda_cache(uid, 'contact', business_info.business_name, 'metadata');
|
|
5710
5739
|
}
|
|
5740
|
+
if (!_.isEmpty(cached_contact)) {
|
|
5741
|
+
await activity('classification_reused', { from_cache: true, key: business_info.business_domain || business_info.business_name, account_type: cached_contact.account_type, business_name: cached_contact?.business_info?.business_name });
|
|
5742
|
+
}
|
|
5711
5743
|
}
|
|
5712
5744
|
}
|
|
5713
5745
|
}
|
|
@@ -5715,7 +5747,7 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5715
5747
|
|
|
5716
5748
|
const d = Date.now();
|
|
5717
5749
|
const contact_obj = {
|
|
5718
|
-
_id:
|
|
5750
|
+
_id: contact_id,
|
|
5719
5751
|
email: email.toLowerCase(),
|
|
5720
5752
|
name: cached_contact?.name || name,
|
|
5721
5753
|
stat: stat || 3,
|
|
@@ -5742,13 +5774,16 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5742
5774
|
if (!is_spam) {
|
|
5743
5775
|
if (account_type === 'personal' || contact_obj?.business_has_person) {
|
|
5744
5776
|
contact_obj.person_info = await ai_ms.get_person_info(uid, contact_obj.name, contact_obj.email, account_profile_info, metadata?.summarized_body, { light: true });
|
|
5777
|
+
await activity('person_info', { by: 'ai', depth: 'light', full_name: contact_obj.person_info?.full_name, job_title: contact_obj.person_info?.job_title, company: contact_obj.person_info?.company_name });
|
|
5745
5778
|
if (!contact_obj.name) {
|
|
5746
5779
|
contact_obj.name = await ai_ms.get_name_from_email_addr(uid, contact_obj.email, account_profile_info);
|
|
5780
|
+
await activity('name_resolved', { by: 'ai', from: 'email address', name: contact_obj.name });
|
|
5747
5781
|
}
|
|
5748
5782
|
}
|
|
5749
5783
|
|
|
5750
5784
|
const conversation_obj = await ai_ms.create_openai_conversation();
|
|
5751
5785
|
contact_obj.contact_reference_conversation_id = conversation_obj.id;
|
|
5786
|
+
await activity('ai_thread_opened', { conversation_id: conversation_obj.id });
|
|
5752
5787
|
} else {
|
|
5753
5788
|
if (!contact_obj.name) {
|
|
5754
5789
|
contact_obj.name = '';
|
|
@@ -5758,6 +5793,10 @@ export const add_contact = async function (req, job_id, headers) {
|
|
|
5758
5793
|
const save_ret = await save_contact(uid, contact_obj);
|
|
5759
5794
|
save_xuda_cache(uid, 'contact', contact_obj.email, null, contact_obj);
|
|
5760
5795
|
|
|
5796
|
+
// The saved verdict, in one row, so the trail ends on the answer the card
|
|
5797
|
+
// actually shows rather than making the reader replay the steps above.
|
|
5798
|
+
await activity('saved', { account_type: contact_obj.account_type || 'unset', is_spam: !!contact_obj.is_spam, business_name: contact_obj?.business_info?.business_name, name: contact_obj.name || '' });
|
|
5799
|
+
|
|
5761
5800
|
if (!contact_obj.is_spam && contact_obj.name) {
|
|
5762
5801
|
set_contact_profile_picture(uid, contact_obj._id, metadata, job_id, headers, account_profile_info, false);
|
|
5763
5802
|
}
|
|
@@ -5821,6 +5860,7 @@ const set_contact_profile_picture = async function (uid, contact_id, metadata, j
|
|
|
5821
5860
|
contact_obj.profile_picture_source = file_ret.profile_picture_source;
|
|
5822
5861
|
|
|
5823
5862
|
const contact_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_obj);
|
|
5863
|
+
log_contact_activity(uid, contact_id, 'picture_found', { by: cache ? 'cache' : 'ai', source: file_ret.profile_picture_source, account_type: contact_obj.account_type || 'unset' });
|
|
5824
5864
|
// profile_picture = contact_obj.profile_picture;
|
|
5825
5865
|
}
|
|
5826
5866
|
}
|
|
@@ -5905,10 +5945,14 @@ const set_contact_profile_picture = async function (uid, contact_id, metadata, j
|
|
|
5905
5945
|
contact_obj.profile_avatar = contact_obj?.profile_avatar_obj?.file_url;
|
|
5906
5946
|
contact_obj.avatar_source = file_ret?.data?.avatar_source;
|
|
5907
5947
|
const contact_save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_obj);
|
|
5948
|
+
log_contact_activity(uid, contact_id, 'avatar_ready', { by: cache ? 'cache' : 'ai', source: contact_obj.avatar_source, account_type: contact_obj.account_type || 'unset' });
|
|
5908
5949
|
}
|
|
5909
5950
|
await update_contact_profile_picture_status(uid, contact_id, 3);
|
|
5910
5951
|
} catch (err) {
|
|
5911
5952
|
await update_contact_profile_picture_status(uid, contact_id, 1, err.message);
|
|
5953
|
+
// Worth a row of its own: a contact stuck on the shop / silhouette placeholder
|
|
5954
|
+
// looks like a classification result, and this is the line that says otherwise.
|
|
5955
|
+
log_contact_activity(uid, contact_id, 'picture_failed', { error: err?.message });
|
|
5912
5956
|
delete_xuda_cache(contact_obj);
|
|
5913
5957
|
}
|
|
5914
5958
|
};
|
|
@@ -6141,6 +6185,7 @@ export const archive_contact = async function (req) {
|
|
|
6141
6185
|
contact_doc.stat_reason = 'archived by the user';
|
|
6142
6186
|
|
|
6143
6187
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6188
|
+
log_contact_activity(uid, contact_id, 'archived', { by: 'user', reason: contact_doc.stat_reason });
|
|
6144
6189
|
|
|
6145
6190
|
return contact_save_ret;
|
|
6146
6191
|
} catch (err) {
|
|
@@ -6163,6 +6208,7 @@ export const delete_contact = async function (req, job_id, headers) {
|
|
|
6163
6208
|
contact_doc.stat_reason = 'deleted by the user';
|
|
6164
6209
|
|
|
6165
6210
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6211
|
+
log_contact_activity(uid, contact_id, 'deleted', { by: 'user', reason: contact_doc.stat_reason, note: 'conversations attached to this contact were deleted with it' });
|
|
6166
6212
|
|
|
6167
6213
|
ai_msa.delete_depended_chats(uid, contact_id);
|
|
6168
6214
|
return contact_save_ret;
|
|
@@ -6188,6 +6234,7 @@ export const unarchive_contact = async function (req, job_id, headers) {
|
|
|
6188
6234
|
contact_doc.stat_reason = 'unarchive by the user';
|
|
6189
6235
|
|
|
6190
6236
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6237
|
+
log_contact_activity(uid, contact_id, 'unarchived', { by: 'user', reason: contact_doc.stat_reason });
|
|
6191
6238
|
|
|
6192
6239
|
if (!contact_doc.is_spam) {
|
|
6193
6240
|
not_spam_contact(req, job_id, headers);
|
|
@@ -6217,6 +6264,7 @@ export const unfriend_contact = async function (req) {
|
|
|
6217
6264
|
|
|
6218
6265
|
contact_doc.team_req_id = null;
|
|
6219
6266
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6267
|
+
log_contact_activity(uid, contact_id, 'unfriended', { by: 'user' });
|
|
6220
6268
|
|
|
6221
6269
|
const req_save_ret = await db_module.save_couch_doc('xuda_team', req_doc);
|
|
6222
6270
|
|
|
@@ -6236,6 +6284,7 @@ export const pin_contact = async function (req) {
|
|
|
6236
6284
|
contact_doc.pinned = true;
|
|
6237
6285
|
|
|
6238
6286
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6287
|
+
log_contact_activity(uid, contact_id, 'pinned', { by: 'user' });
|
|
6239
6288
|
|
|
6240
6289
|
ws_dashboard_msa.emit_message_to_dashboard({
|
|
6241
6290
|
service: 'contact_pinned',
|
|
@@ -6280,6 +6329,7 @@ export const unpin_contact = async function (req) {
|
|
|
6280
6329
|
contact_doc.pinned = false;
|
|
6281
6330
|
|
|
6282
6331
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6332
|
+
log_contact_activity(uid, contact_id, 'unpinned', { by: 'user' });
|
|
6283
6333
|
|
|
6284
6334
|
ws_dashboard_msa.emit_message_to_dashboard({
|
|
6285
6335
|
service: 'contact_unpinned',
|
|
@@ -6311,22 +6361,30 @@ export const not_spam_contact = async function (req, job_id, headers) {
|
|
|
6311
6361
|
stat: 3,
|
|
6312
6362
|
});
|
|
6313
6363
|
}
|
|
6364
|
+
const was_spam = !!contact_doc.is_spam;
|
|
6314
6365
|
contact_doc.is_spam = false;
|
|
6366
|
+
log_contact_activity(uid, contact_id, 'marked_not_spam', { by: 'user', was_spam, note: was_spam ? 'address added to the spam whitelist, classification is being redone' : 'classification is being redone' });
|
|
6315
6367
|
|
|
6316
6368
|
if (!contact_doc.contact_reference_conversation_id) {
|
|
6317
6369
|
const conversation_obj = await ai_ms.create_openai_conversation();
|
|
6318
6370
|
contact_doc.contact_reference_conversation_id = conversation_obj.id;
|
|
6371
|
+
await log_contact_activity(uid, contact_id, 'ai_thread_opened', { conversation_id: conversation_obj.id });
|
|
6319
6372
|
}
|
|
6320
6373
|
|
|
6374
|
+
const previous_account_type = contact_doc.account_type;
|
|
6321
6375
|
const is_business = 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);
|
|
6322
6376
|
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' });
|
|
6323
6378
|
if (is_business) {
|
|
6324
6379
|
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info, { light: true });
|
|
6325
6380
|
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
6381
|
+
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 });
|
|
6326
6382
|
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
|
+
await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
|
|
6327
6384
|
} else {
|
|
6328
6385
|
contact_doc.account_type = 'personal';
|
|
6329
6386
|
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' });
|
|
6330
6388
|
}
|
|
6331
6389
|
}
|
|
6332
6390
|
|
|
@@ -6362,6 +6420,7 @@ export const not_spam_contact = async function (req, job_id, headers) {
|
|
|
6362
6420
|
req.email_id = email._id;
|
|
6363
6421
|
await email_ms.process_pending_email(req, job_id, headers);
|
|
6364
6422
|
}
|
|
6423
|
+
if (emails.docs.length) log_contact_activity(uid, contact_id, 'emails_reprocessed', { count: emails.docs.length, trigger: 'not spam', note: 'partially processed emails were re-read in full' });
|
|
6365
6424
|
|
|
6366
6425
|
return contact_save_ret;
|
|
6367
6426
|
} catch (err) {
|
|
@@ -6390,6 +6449,7 @@ export const generate_contact_avatar = async function (req, job_id, headers) {
|
|
|
6390
6449
|
|
|
6391
6450
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6392
6451
|
await delete_xuda_cache(contact_doc);
|
|
6452
|
+
log_contact_activity(uid, contact_id, 'avatar_regenerate_requested', { by: 'user', note: 'existing picture and avatar cleared, a new avatar is being generated' });
|
|
6393
6453
|
set_contact_profile_picture(uid, contact_doc._id, {}, job_id, headers, account_profile_info, true);
|
|
6394
6454
|
|
|
6395
6455
|
return contact_save_ret;
|
|
@@ -6407,21 +6467,27 @@ export const set_deep_research_contact = async function (req, job_id, headers) {
|
|
|
6407
6467
|
try {
|
|
6408
6468
|
var contact_doc = await get_contact(uid, contact_id);
|
|
6409
6469
|
contact_doc.deep_research = true;
|
|
6470
|
+
await log_contact_activity(uid, contact_id, 'deep_research_started', { by: 'user', account_type: contact_doc.account_type || 'unset' });
|
|
6410
6471
|
|
|
6411
6472
|
if (contact_doc.account_type === 'business') {
|
|
6412
6473
|
contact_doc.business_info = await ai_ms.get_business_info(uid, '', contact_doc.email, account_profile_info);
|
|
6413
6474
|
if (contact_doc.business_info.business_name !== 'Not available') {
|
|
6475
|
+
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 });
|
|
6414
6476
|
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
|
+
await log_contact_activity(uid, contact_id, 'business_has_person', { by: 'ai', has_person: !!contact_doc.business_has_person });
|
|
6415
6478
|
} else {
|
|
6416
6479
|
contact_doc.account_type = 'personal';
|
|
6417
6480
|
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' });
|
|
6418
6482
|
}
|
|
6419
6483
|
}
|
|
6420
6484
|
|
|
6421
6485
|
if (contact_doc.account_type === 'personal' || contact_doc?.business_has_person) {
|
|
6422
6486
|
contact_doc.person_info = await ai_ms.get_person_info(uid, contact_doc.name, contact_doc.email, account_profile_info, contact_doc?.metadata?.summarized_body);
|
|
6487
|
+
await log_contact_activity(uid, contact_id, 'person_info', { by: 'ai', depth: 'deep', full_name: contact_doc.person_info?.full_name, job_title: contact_doc.person_info?.job_title, company: contact_doc.person_info?.company_name });
|
|
6423
6488
|
if (!contact_doc.name) {
|
|
6424
6489
|
contact_doc.name = await ai_ms.get_name_from_email_addr(uid, contact_doc.email, account_profile_info);
|
|
6490
|
+
await log_contact_activity(uid, contact_id, 'name_resolved', { by: 'ai', from: 'email address', name: contact_doc.name });
|
|
6425
6491
|
}
|
|
6426
6492
|
}
|
|
6427
6493
|
|
|
@@ -6456,6 +6522,8 @@ export const set_deep_research_contact = async function (req, job_id, headers) {
|
|
|
6456
6522
|
throw err;
|
|
6457
6523
|
}
|
|
6458
6524
|
}
|
|
6525
|
+
if (conversation_items.docs.length) log_contact_activity(uid, contact_id, 'attachments_transcribed', { count: conversation_items.docs.length, trigger: 'deep research' });
|
|
6526
|
+
log_contact_activity(uid, contact_id, 'deep_research_finished', { account_type: contact_doc.account_type || 'unset', business_name: contact_doc?.business_info?.business_name });
|
|
6459
6527
|
|
|
6460
6528
|
return contact_save_ret;
|
|
6461
6529
|
} catch (err) {
|
|
@@ -6475,6 +6543,7 @@ export const unset_deep_research_contact = async function (req, job_id, headers)
|
|
|
6475
6543
|
|
|
6476
6544
|
const contact_save_ret = await save_contact(uid, contact_doc);
|
|
6477
6545
|
await delete_xuda_cache(contact_doc);
|
|
6546
|
+
log_contact_activity(uid, contact_id, 'deep_research_turned_off', { by: 'user' });
|
|
6478
6547
|
|
|
6479
6548
|
return contact_save_ret;
|
|
6480
6549
|
} catch (err) {
|
|
@@ -6484,6 +6553,80 @@ export const unset_deep_research_contact = async function (req, job_id, headers)
|
|
|
6484
6553
|
};
|
|
6485
6554
|
}
|
|
6486
6555
|
};
|
|
6556
|
+
|
|
6557
|
+
// The trail for one contact, newest first.
|
|
6558
|
+
//
|
|
6559
|
+
// Recorded rows are only half the answer: every contact that existed before the
|
|
6560
|
+
// trail did has none, and those are most of them. So the contact doc is read back
|
|
6561
|
+
// into the same shape first (derived: true). The doc keeps one timestamp per
|
|
6562
|
+
// FIELD, not per step, so a derived row is stamped with the closest timestamp it
|
|
6563
|
+
// honestly has (date_created for the creation and the classification that ran
|
|
6564
|
+
// inside it, profile_avatar_stat_ts for the picture, stat_ts for the archive) and
|
|
6565
|
+
// says derived so the UI can mark it as reconstructed rather than observed.
|
|
6566
|
+
export const get_contact_activity = async function (req) {
|
|
6567
|
+
const { uid, contact_id } = req;
|
|
6568
|
+
try {
|
|
6569
|
+
if (!contact_id) throw new Error('contact_id is missing');
|
|
6570
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
6571
|
+
const contact_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
|
|
6572
|
+
if (!contact_doc || contact_doc.docType !== 'contact') throw new Error(`contact ${contact_id} not found`);
|
|
6573
|
+
if (contact_doc.uid !== uid && contact_doc?.uid_created !== uid) throw new Error('Operation not allowed');
|
|
6574
|
+
|
|
6575
|
+
const recorded_ret = await db_module.find_app_couch_query(account_profile_info.app_id, {
|
|
6576
|
+
selector: { docType: 'contact_activity', contact_id },
|
|
6577
|
+
limit: 500,
|
|
6578
|
+
});
|
|
6579
|
+
const recorded = (recorded_ret?.docs || []).map((d) => ({ event: d.event, detail: d.detail || {}, ts: d.ts, derived: false }));
|
|
6580
|
+
|
|
6581
|
+
const derived = [];
|
|
6582
|
+
const add = (event, ts, detail) => {
|
|
6583
|
+
if (!ts) return;
|
|
6584
|
+
// A recorded row always wins: it has the real timestamp and the real inputs.
|
|
6585
|
+
if (recorded.some((r) => r.event === event)) return;
|
|
6586
|
+
derived.push({ event, detail, ts, derived: true });
|
|
6587
|
+
};
|
|
6588
|
+
|
|
6589
|
+
const created_ts = contact_doc.date_created || contact_doc.ts;
|
|
6590
|
+
add('created', created_ts, { source: contact_doc.source || 'unknown', email: contact_doc.email, name: contact_doc.name || '', from_account: !!contact_doc.contact_uid, subject: contact_doc?.metadata?.subject });
|
|
6591
|
+
if (contact_doc.is_spam) add('spam_check', created_ts, { is_spam: true });
|
|
6592
|
+
if (contact_doc.account_type) add('classified', created_ts, { account_type: contact_doc.account_type });
|
|
6593
|
+
if (contact_doc?.business_info?.business_name) add('business_info', created_ts, { business_name: contact_doc.business_info.business_name, business_domain: contact_doc.business_info.business_domain, business_category: contact_doc.business_info.business_category });
|
|
6594
|
+
if (contact_doc.business_has_person !== undefined && contact_doc.business_has_person !== null) add('business_has_person', created_ts, { has_person: !!contact_doc.business_has_person });
|
|
6595
|
+
if (contact_doc?.person_info) add('person_info', created_ts, { full_name: contact_doc.person_info.full_name || contact_doc.person_info.person_full_name, job_title: contact_doc.person_info.job_title, company: contact_doc.person_info.company_name });
|
|
6596
|
+
if (contact_doc.contact_reference_conversation_id) add('ai_thread_opened', created_ts, { conversation_id: contact_doc.contact_reference_conversation_id });
|
|
6597
|
+
if (contact_doc.profile_picture) add('picture_found', contact_doc.profile_avatar_stat_ts || created_ts, { source: contact_doc.profile_picture_source });
|
|
6598
|
+
if (contact_doc.profile_avatar) add('avatar_ready', contact_doc.profile_avatar_stat_ts || created_ts, { source: contact_doc.avatar_source });
|
|
6599
|
+
if (contact_doc.profile_avatar_stat === 1 && contact_doc.profile_avatar_error) add('picture_failed', contact_doc.profile_avatar_stat_ts, { error: contact_doc.profile_avatar_error });
|
|
6600
|
+
if (contact_doc.deep_research) add('deep_research_started', contact_doc.stat_ts || created_ts, {});
|
|
6601
|
+
if (contact_doc.pinned) add('pinned', contact_doc.stat_ts || created_ts, {});
|
|
6602
|
+
// stat carries only the LAST transition, which is why an archive/unarchive
|
|
6603
|
+
// history is exactly what the recorded trail adds from here on.
|
|
6604
|
+
if (contact_doc.stat === 5) add('archived', contact_doc.stat_ts, { reason: contact_doc.stat_reason });
|
|
6605
|
+
if (contact_doc.stat === 4) add('deleted', contact_doc.stat_ts, { reason: contact_doc.stat_reason });
|
|
6606
|
+
|
|
6607
|
+
const rows = [...recorded, ...derived].sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
|
6608
|
+
|
|
6609
|
+
return {
|
|
6610
|
+
code: 1,
|
|
6611
|
+
data: {
|
|
6612
|
+
contact_id,
|
|
6613
|
+
// What the card shows today, so the UI can head the trail with the outcome.
|
|
6614
|
+
current: {
|
|
6615
|
+
account_type: contact_doc.account_type || null,
|
|
6616
|
+
is_spam: !!contact_doc.is_spam,
|
|
6617
|
+
stat: contact_doc.stat,
|
|
6618
|
+
source: contact_doc.source || null,
|
|
6619
|
+
deep_research: !!contact_doc.deep_research,
|
|
6620
|
+
business_name: contact_doc?.business_info?.business_name || null,
|
|
6621
|
+
profile_avatar_stat: contact_doc.profile_avatar_stat,
|
|
6622
|
+
},
|
|
6623
|
+
rows,
|
|
6624
|
+
},
|
|
6625
|
+
};
|
|
6626
|
+
} catch (err) {
|
|
6627
|
+
return { code: -25, data: err.message };
|
|
6628
|
+
}
|
|
6629
|
+
};
|
|
6487
6630
|
//////// PROFILES //////////////
|
|
6488
6631
|
|
|
6489
6632
|
export const get_pending_share_profile_in = async function (uid, _id) {
|
|
@@ -7030,6 +7173,48 @@ export const get_contact = async function (uid, contact_id) {
|
|
|
7030
7173
|
return contact_ret;
|
|
7031
7174
|
};
|
|
7032
7175
|
|
|
7176
|
+
// ---------------------------------------------------------------------------
|
|
7177
|
+
// Contact activity trail
|
|
7178
|
+
//
|
|
7179
|
+
// A contact accumulates a lot of history that was previously only visible as
|
|
7180
|
+
// the END STATE on its own doc: it was created from somewhere, several AI steps
|
|
7181
|
+
// ran on it and each decided something (spam, business or personal, the business
|
|
7182
|
+
// profile, the person profile, the name, the avatar), and it may since have been
|
|
7183
|
+
// archived, unarchived, pinned, re-classified or sent to deep research. The doc
|
|
7184
|
+
// keeps the last answer of each and nothing about when, in what order, or what
|
|
7185
|
+
// the previous answer was, so "why is this contact a business" had no trace to
|
|
7186
|
+
// read back.
|
|
7187
|
+
//
|
|
7188
|
+
// One `contact_activity` doc per event, in the same app db as the contact.
|
|
7189
|
+
// Deliberately append-only and deliberately silent: a failure to WRITE the trail
|
|
7190
|
+
// must never fail the action being recorded, so every call is wrapped and logged
|
|
7191
|
+
// to the console instead of thrown. Callers do not await it for that reason
|
|
7192
|
+
// either, except where the next line already awaits something else anyway.
|
|
7193
|
+
// ---------------------------------------------------------------------------
|
|
7194
|
+
const log_contact_activity = async function (uid, contact_id, event, detail = {}, app_id) {
|
|
7195
|
+
try {
|
|
7196
|
+
if (!uid || !contact_id || !event) return null;
|
|
7197
|
+
const app = app_id || (await get_active_account_profile_info(uid))?.app_id;
|
|
7198
|
+
if (!app) return null;
|
|
7199
|
+
|
|
7200
|
+
return await db_module.save_app_couch_doc_native(app, {
|
|
7201
|
+
_id: await _common.xuda_get_uuid('contact_activity'),
|
|
7202
|
+
docType: 'contact_activity',
|
|
7203
|
+
contact_id,
|
|
7204
|
+
uid,
|
|
7205
|
+
event,
|
|
7206
|
+
// Values the UI prints back verbatim, so keep them short and human. Never
|
|
7207
|
+
// put a full AI response or an email body here: the trail is a summary.
|
|
7208
|
+
detail,
|
|
7209
|
+
ts: Date.now(),
|
|
7210
|
+
stat: 3,
|
|
7211
|
+
});
|
|
7212
|
+
} catch (err) {
|
|
7213
|
+
console.error('[account_module] contact activity not recorded:', event, contact_id, err?.message);
|
|
7214
|
+
return null;
|
|
7215
|
+
}
|
|
7216
|
+
};
|
|
7217
|
+
|
|
7033
7218
|
export const save_contact = async function (uid, contact_doc) {
|
|
7034
7219
|
const account_profile_info = await get_active_account_profile_info(uid);
|
|
7035
7220
|
const contact_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_doc);
|
package/index_ms.mjs
CHANGED
|
@@ -449,6 +449,10 @@ export const unset_deep_research_contact = async function (...args) {
|
|
|
449
449
|
return await broker.send_to_queue("unset_deep_research_contact", ...args);
|
|
450
450
|
};
|
|
451
451
|
|
|
452
|
+
export const get_contact_activity = async function (...args) {
|
|
453
|
+
return await broker.send_to_queue("get_contact_activity", ...args);
|
|
454
|
+
};
|
|
455
|
+
|
|
452
456
|
export const get_pending_share_profile_in = async function (...args) {
|
|
453
457
|
return await broker.send_to_queue("get_pending_share_profile_in", ...args);
|
|
454
458
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -449,6 +449,10 @@ export const unset_deep_research_contact = function (...args) {
|
|
|
449
449
|
broker.send_to_queue_async("unset_deep_research_contact", ...args);
|
|
450
450
|
};
|
|
451
451
|
|
|
452
|
+
export const get_contact_activity = function (...args) {
|
|
453
|
+
broker.send_to_queue_async("get_contact_activity", ...args);
|
|
454
|
+
};
|
|
455
|
+
|
|
452
456
|
export const get_pending_share_profile_in = function (...args) {
|
|
453
457
|
broker.send_to_queue_async("get_pending_share_profile_in", ...args);
|
|
454
458
|
};
|