@xuda.io/account_module 1.2.2257 → 1.2.2259
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 +231 -85
- package/index_ms.mjs +28 -14
- package/index_msa.mjs +24 -16
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -34,6 +34,7 @@ const account_info_properties = [
|
|
|
34
34
|
'network_lang',
|
|
35
35
|
'network_country_code',
|
|
36
36
|
'network_city_slug',
|
|
37
|
+
'public_profile_disabled',
|
|
37
38
|
];
|
|
38
39
|
|
|
39
40
|
global._conf = (
|
|
@@ -579,42 +580,9 @@ export const increment_account_usage = async function (req) {
|
|
|
579
580
|
// app type?".
|
|
580
581
|
const BILLABLE_APP_TYPES = ['vps', 'datacenter', 'instance', 'balancer'];
|
|
581
582
|
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
// without delay.
|
|
586
|
-
//
|
|
587
|
-
// Why we need this: destroy_app runs with { job: true } — i.e. the
|
|
588
|
-
// HTTP call queues a background job and returns right away. The job
|
|
589
|
-
// worker eventually executes destroy_app server-side, which marks
|
|
590
|
-
// terminated_ts at the START of its own execution. But during the
|
|
591
|
-
// window between "request returns" and "job worker starts", the
|
|
592
|
-
// app doc still looks billable. The dashboard refresh in that
|
|
593
|
-
// window sees the old state — exactly the "GUI doesn't react /
|
|
594
|
-
// second refresh fixes it" symptom.
|
|
595
|
-
//
|
|
596
|
-
// This endpoint sidesteps the job queue entirely. It's a synchronous
|
|
597
|
-
// 2-line write — no DO API calls, no destroy chain. The dashboard
|
|
598
|
-
// calls it BEFORE the destroy_app job, returns can render the new
|
|
599
|
-
// billing state without waiting on infra teardown.
|
|
600
|
-
//
|
|
601
|
-
// Idempotent: if terminated_ts is already set, returns immediately.
|
|
602
|
-
export const mark_app_terminated = async function (req) {
|
|
603
|
-
const { app_id } = req || {};
|
|
604
|
-
if (!app_id) return { code: -1, data: 'app_id required' };
|
|
605
|
-
try {
|
|
606
|
-
const ret = await db_module.get_couch_doc('xuda_master', app_id);
|
|
607
|
-
if (ret.code < 0) return { code: -1, data: 'app not found' };
|
|
608
|
-
const app_doc = ret.data;
|
|
609
|
-
if (!app_doc.app_cost) return { code: 1, data: 'no app_cost — nothing to mark' };
|
|
610
|
-
if (app_doc.app_cost.terminated_ts) return { code: 1, data: 'already terminated' };
|
|
611
|
-
app_doc.app_cost.terminated_ts = Date.now();
|
|
612
|
-
await db_module.save_couch_doc('xuda_master', app_doc);
|
|
613
|
-
return { code: 1, data: { terminated_ts: app_doc.app_cost.terminated_ts } };
|
|
614
|
-
} catch (err) {
|
|
615
|
-
return { code: -1, data: err.message };
|
|
616
|
-
}
|
|
617
|
-
};
|
|
583
|
+
// `mark_app_terminated` was removed — its job is now done by
|
|
584
|
+
// `pre_destroy_app` in app_module, wired up via http_module's
|
|
585
|
+
// pre_dispatch hook on `destroy_app`. See cpi/app_module/index.mjs.
|
|
618
586
|
|
|
619
587
|
// -------------------------------------------------------------------
|
|
620
588
|
// Abuse detection — see the "How can we detect abuse?" plan.
|
|
@@ -1022,8 +990,16 @@ export const get_account_data = async function (req) {
|
|
|
1022
990
|
support_plan_changed: acc_obj.support_plan_changed,
|
|
1023
991
|
ai_workspace_plan: acc_obj.ai_workspace_plan,
|
|
1024
992
|
storage_plan_changed: acc_obj.storage_plan_changed,
|
|
993
|
+
account_hosted_email: acc_obj.account_hosted_email || null,
|
|
1025
994
|
account_project_id: acc_obj.account_project_id,
|
|
1026
|
-
|
|
995
|
+
// Coerce to explicit boolean. New accounts have `isBoarded`
|
|
996
|
+
// undefined; without this, JSON serialization drops the field
|
|
997
|
+
// entirely. The dashboard's SAVE_user uses spread-merge which
|
|
998
|
+
// doesn't overwrite missing-from-response fields, so a stale
|
|
999
|
+
// `is_boarded: true` from a previous user's localStorage would
|
|
1000
|
+
// never get cleared. Always sending an explicit false keeps the
|
|
1001
|
+
// gate honest.
|
|
1002
|
+
is_boarded: !!acc_obj.isBoarded,
|
|
1027
1003
|
|
|
1028
1004
|
prices_info: acc_obj.prices_info,
|
|
1029
1005
|
activity_usage: acc_obj.activity_usage,
|
|
@@ -1092,6 +1068,52 @@ export const get_account_instances = async function (req) {
|
|
|
1092
1068
|
key: req.uid,
|
|
1093
1069
|
});
|
|
1094
1070
|
};
|
|
1071
|
+
// Standalone VPS list for the sidenav — lets the dashboard refresh DATA.vps
|
|
1072
|
+
// after a deploy without a full account reload (SideNavData calls this). Same
|
|
1073
|
+
// user_vps view that the initial account load uses.
|
|
1074
|
+
export const get_account_vps = async function (req) {
|
|
1075
|
+
return await db_module.get_couch_view('xuda_master', 'user_vps', {
|
|
1076
|
+
key: req.uid,
|
|
1077
|
+
});
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// Advertising campaigns are apps (app_type:'advertising'). A Mango query by
|
|
1081
|
+
// owner avoids needing a dedicated CouchDB view; shaped like get_couch_view
|
|
1082
|
+
// ({code, data:{rows:[{id,value}]}}) so SideNavData buckets them into
|
|
1083
|
+
// DATA.advertising exactly like deployments/vps.
|
|
1084
|
+
export const get_account_advertising = async function (req) {
|
|
1085
|
+
try {
|
|
1086
|
+
// Superuser sees EVERY campaign across all accounts; everyone else only their own.
|
|
1087
|
+
const isSuper = _conf.superuser_account_ids?.includes(req.uid);
|
|
1088
|
+
const selector = isSuper ? { app_type: 'advertising', docType: 'app' } : { app_uId: req.uid, app_type: 'advertising', docType: 'app' };
|
|
1089
|
+
const ret = await db_module.find_couch_query('xuda_master', {
|
|
1090
|
+
selector,
|
|
1091
|
+
limit: 1000,
|
|
1092
|
+
});
|
|
1093
|
+
const rows = (ret?.docs || []).map((d) => ({ id: d._id, value: d }));
|
|
1094
|
+
return { code: 1, data: { rows } };
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
return { code: -1, data: err.message };
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// Static websites are apps (app_type:'static_website') created via create_app, but
|
|
1101
|
+
// they aren't emitted by any user_* CouchDB view, so they never surface in a sidenav
|
|
1102
|
+
// bucket. Same Mango-query approach as get_account_advertising → DATA.static_website.
|
|
1103
|
+
export const get_account_static_websites = async function (req) {
|
|
1104
|
+
try {
|
|
1105
|
+
const isSuper = _conf.superuser_account_ids?.includes(req.uid);
|
|
1106
|
+
const selector = isSuper ? { app_type: 'static_website', docType: 'app' } : { app_uId: req.uid, app_type: 'static_website', docType: 'app' };
|
|
1107
|
+
const ret = await db_module.find_couch_query('xuda_master', {
|
|
1108
|
+
selector,
|
|
1109
|
+
limit: 1000,
|
|
1110
|
+
});
|
|
1111
|
+
const rows = (ret?.docs || []).map((d) => ({ id: d._id, value: d }));
|
|
1112
|
+
return { code: 1, data: { rows } };
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
return { code: -1, data: err.message };
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1095
1117
|
|
|
1096
1118
|
export const get_account_info = async function (req) {
|
|
1097
1119
|
return await get_account_data({
|
|
@@ -1263,7 +1285,7 @@ export const get_account_name = async function (req) {
|
|
|
1263
1285
|
if (data.code < 0) {
|
|
1264
1286
|
return data;
|
|
1265
1287
|
}
|
|
1266
|
-
const { membership_plan = '', support_plan = '', ai_workspace_plan = '', date_created_ts = '' } = data.data;
|
|
1288
|
+
const { membership_plan = '', support_plan = '', ai_workspace_plan = '', date_created_ts = '', stat = '' } = data.data;
|
|
1267
1289
|
var obj = {
|
|
1268
1290
|
first_name: '',
|
|
1269
1291
|
last_name: '',
|
|
@@ -1275,13 +1297,41 @@ export const get_account_name = async function (req) {
|
|
|
1275
1297
|
support_plan,
|
|
1276
1298
|
ai_workspace_plan,
|
|
1277
1299
|
date_created_ts,
|
|
1300
|
+
stat,
|
|
1278
1301
|
};
|
|
1279
1302
|
if (data.data.account_info) {
|
|
1280
|
-
const {
|
|
1281
|
-
|
|
1303
|
+
const {
|
|
1304
|
+
first_name,
|
|
1305
|
+
last_name,
|
|
1306
|
+
email,
|
|
1307
|
+
phone_number,
|
|
1308
|
+
profile_picture,
|
|
1309
|
+
profile_avatar,
|
|
1310
|
+
username,
|
|
1311
|
+
website,
|
|
1312
|
+
country,
|
|
1313
|
+
bio,
|
|
1314
|
+
industry,
|
|
1315
|
+
account_type,
|
|
1316
|
+
address,
|
|
1317
|
+
city,
|
|
1318
|
+
state,
|
|
1319
|
+
zip,
|
|
1320
|
+
business_name,
|
|
1321
|
+
auto_respond,
|
|
1322
|
+
auto_respond_mode,
|
|
1323
|
+
auto_respond_agents,
|
|
1324
|
+
avatar_source,
|
|
1325
|
+
active_account_profile_id,
|
|
1326
|
+
network_country_code,
|
|
1327
|
+
public_profile_disabled,
|
|
1328
|
+
} = data.data.account_info;
|
|
1282
1329
|
|
|
1283
1330
|
obj = {
|
|
1284
1331
|
_id: data.data._id,
|
|
1332
|
+
stat,
|
|
1333
|
+
network_country_code,
|
|
1334
|
+
public_profile_disabled,
|
|
1285
1335
|
first_name,
|
|
1286
1336
|
last_name,
|
|
1287
1337
|
email,
|
|
@@ -1364,6 +1414,11 @@ export const verify_account = async function (req) {
|
|
|
1364
1414
|
export const validate_user_plan = async function (req) {
|
|
1365
1415
|
const { account_id, app_obj } = req;
|
|
1366
1416
|
|
|
1417
|
+
// Advertising campaigns are charged per-campaign (Stripe), NOT plan-capped — they
|
|
1418
|
+
// (and their team sharing) are available on EVERY plan, free included. Exempt them
|
|
1419
|
+
// from the plan gate so free accounts can invite collaborators to a campaign.
|
|
1420
|
+
if (app_obj?.app_type === 'advertising') return { code: 1, data: { membership_plan: 'advertising_exempt' } };
|
|
1421
|
+
|
|
1367
1422
|
const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps_count', {
|
|
1368
1423
|
startkey: [account_id, ''],
|
|
1369
1424
|
endkey: [account_id, 'ZZZZZ'],
|
|
@@ -1391,8 +1446,13 @@ export const validate_user_plan = async function (req) {
|
|
|
1391
1446
|
return { code: -7, data: 'error - no plan defined' };
|
|
1392
1447
|
}
|
|
1393
1448
|
|
|
1394
|
-
// validate number of projects
|
|
1395
|
-
|
|
1449
|
+
// validate number of projects.
|
|
1450
|
+
// The system account-project (is_account_project) backs login + the user
|
|
1451
|
+
// drive and is created automatically by login_maintenance_fix — it must
|
|
1452
|
+
// NEVER be blocked by the plan's project cap, or an account at the limit can
|
|
1453
|
+
// never be provisioned and gets permanently locked out (get_gtp_token_info
|
|
1454
|
+
// throws "fix: account_project_id" -> 401). Exempt it from the count.
|
|
1455
|
+
if (app_obj.app_type === 'master' && !app_obj.is_account_project && ret_obj.master >= plan.features.projects) {
|
|
1396
1456
|
return {
|
|
1397
1457
|
code: -8,
|
|
1398
1458
|
data: `Number of projects (${ret.data.membership_plan}) exceeds to the ${plan.features.projects} plan limits`,
|
|
@@ -2260,52 +2320,34 @@ export const validate_account_topup = async function (uid, items = []) {
|
|
|
2260
2320
|
if (code < 0) {
|
|
2261
2321
|
throw new Error(data);
|
|
2262
2322
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2323
|
+
// POSTPAID billing: paid features (VPS, custom domain, xuda subdomain, add-ons)
|
|
2324
|
+
// now bill on the Stripe invoice at cycle close — we NO LONGER require a prepaid
|
|
2325
|
+
// credit balance up front, only a working payment method. `items` is kept for
|
|
2326
|
+
// call-site compatibility but is no longer used to gate on a prepaid amount.
|
|
2267
2327
|
|
|
2328
|
+
// Gate 1: there must be a card on file to bill against.
|
|
2268
2329
|
if (!data?.customer_obj?.default_source) {
|
|
2269
2330
|
let err = new Error('no card on file');
|
|
2270
2331
|
err.code = -90;
|
|
2271
2332
|
err.billing_problem = true;
|
|
2272
2333
|
throw err;
|
|
2273
|
-
// throw new Error('no card on file');
|
|
2274
2334
|
}
|
|
2275
2335
|
|
|
2336
|
+
// Gate 2: a prior invoice that failed to charge is a real billing problem —
|
|
2337
|
+
// block new paid usage until the card is fixed (otherwise unpaid usage runs
|
|
2338
|
+
// unbounded). This is "pay your overdue invoice", not a prepaid top-up.
|
|
2276
2339
|
if (data?.balance?.past_due) {
|
|
2277
2340
|
let err = new Error(`past due ${data.balance.past_due}`);
|
|
2278
2341
|
err.code = -91;
|
|
2279
2342
|
err.billing_problem = true;
|
|
2280
|
-
err.topup =
|
|
2343
|
+
err.topup = data.balance.past_due;
|
|
2281
2344
|
err.open_invoices = data?.open_invoices;
|
|
2282
2345
|
throw err;
|
|
2283
2346
|
}
|
|
2284
|
-
// if (data?.balance?.past_due) {
|
|
2285
|
-
// let err = new Error(`past_due ${data?.balance?.past_due}`);
|
|
2286
|
-
// err.code = -91;
|
|
2287
|
-
// err.billing_problem = true;
|
|
2288
|
-
// err.topup = topup;
|
|
2289
|
-
// throw err;
|
|
2290
|
-
// }
|
|
2291
|
-
|
|
2292
|
-
const balance = data?.balance?.account || 0; // minus balance represents positive balance
|
|
2293
|
-
|
|
2294
|
-
if (balance > 0) {
|
|
2295
|
-
let err = new Error(`negative balance ${-balance}`);
|
|
2296
|
-
err.code = -92;
|
|
2297
|
-
err.billing_problem = true;
|
|
2298
|
-
err.topup = topup - balance; // ask for whole balance
|
|
2299
|
-
throw err;
|
|
2300
|
-
}
|
|
2301
2347
|
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
err.billing_problem = true;
|
|
2306
|
-
err.topup = topup;
|
|
2307
|
-
throw err;
|
|
2308
|
-
}
|
|
2348
|
+
// No prepaid-credit / "not enough balance" gate anymore — usage accrues on the
|
|
2349
|
+
// app_cost ledger and is charged on the next invoice (Stripe applies any credit
|
|
2350
|
+
// balance first, then the card).
|
|
2309
2351
|
|
|
2310
2352
|
await release_account_billing_hold_if_current(uid);
|
|
2311
2353
|
|
|
@@ -2490,19 +2532,11 @@ export const onboarding_completed = async function (req, job_id, headers) {
|
|
|
2490
2532
|
account_doc.isBoarded = true;
|
|
2491
2533
|
const save_ret = await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
2492
2534
|
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
app_id: null,
|
|
2499
|
-
uid_arr: [uid],
|
|
2500
|
-
topic: 'welcome_aboard',
|
|
2501
|
-
params: { name: account_doc.account_info.first_name, email: account_doc.account_info.email, dashboard_url: 'https://xuda.ai/dashboard' },
|
|
2502
|
-
ref: null,
|
|
2503
|
-
email: null,
|
|
2504
|
-
});
|
|
2505
|
-
}
|
|
2535
|
+
// Welcome email is sent once per account by maybe_send_welcome_email, which
|
|
2536
|
+
// is also triggered when avatar generation completes (so it reaches every
|
|
2537
|
+
// signup surface, not only the dashboard onboarding form). It is idempotent
|
|
2538
|
+
// (welcome_email_sent_ts) and suppresses mentors/ambassadors.
|
|
2539
|
+
maybe_send_welcome_email(uid);
|
|
2506
2540
|
|
|
2507
2541
|
return save_ret;
|
|
2508
2542
|
} catch (err) {
|
|
@@ -2510,6 +2544,55 @@ export const onboarding_completed = async function (req, job_id, headers) {
|
|
|
2510
2544
|
}
|
|
2511
2545
|
};
|
|
2512
2546
|
|
|
2547
|
+
// Sends the welcome_aboard email exactly once per account. Safe to call from
|
|
2548
|
+
// multiple triggers (avatar-completion, onboarding_completed) and across every
|
|
2549
|
+
// signup surface (xuda.ai, Google OAuth, xuda.fashion, xuda.network, chat
|
|
2550
|
+
// widget, public profile). Skips mentors/ambassadors (is_xuda_network_ambassador).
|
|
2551
|
+
export const maybe_send_welcome_email = async function (uid) {
|
|
2552
|
+
try {
|
|
2553
|
+
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
2554
|
+
if (!account_doc || !account_doc.account_info) return { code: -1, data: 'no account' };
|
|
2555
|
+
if (account_doc.account_info.is_xuda_network_ambassador === true) {
|
|
2556
|
+
console.log('[maybe_send_welcome_email] suppressing for xuda.network ambassador/mentor', uid);
|
|
2557
|
+
return { code: 0, data: 'ambassador' };
|
|
2558
|
+
}
|
|
2559
|
+
if (account_doc.welcome_email_sent_ts) return { code: 0, data: 'already sent' };
|
|
2560
|
+
|
|
2561
|
+
const host = _conf.is_debug ? process.env.XUDA_HOSTNAME || _conf.domain : _conf.domain;
|
|
2562
|
+
const username = account_doc.account_info.username || uid;
|
|
2563
|
+
const base = `https://${host}`;
|
|
2564
|
+
const public_url = `${base}/public_profiles/${encodeURIComponent(username)}`;
|
|
2565
|
+
const params = {
|
|
2566
|
+
name: account_doc.account_info.first_name || 'there',
|
|
2567
|
+
dashboard_url: `${base}/dashboard`,
|
|
2568
|
+
public_url,
|
|
2569
|
+
public_page_label: `${host}/public_profiles/${username}`,
|
|
2570
|
+
qr: `${public_url}/qr.png`,
|
|
2571
|
+
gif_base: `${base}/dist/images/email`,
|
|
2572
|
+
};
|
|
2573
|
+
await notification_msa.submit_notification({ type: 'account', app_id: null, uid_arr: [uid], topic: 'welcome_aboard', params, ref: null, email: null });
|
|
2574
|
+
|
|
2575
|
+
account_doc.welcome_email_sent_ts = Date.now();
|
|
2576
|
+
await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
2577
|
+
return { code: 1, data: 'sent' };
|
|
2578
|
+
} catch (err) {
|
|
2579
|
+
console.error('[maybe_send_welcome_email]', err.message);
|
|
2580
|
+
return { code: -1, data: err.message };
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
|
|
2584
|
+
// Resolve an account uid from a public username (account_info.username).
|
|
2585
|
+
// Used by the public-profile route so /public_profiles/<username> resolves.
|
|
2586
|
+
export const get_uid_by_username = async function (username) {
|
|
2587
|
+
if (!username) return null;
|
|
2588
|
+
const ret = await db_module.find_couch_query('xuda_accounts', {
|
|
2589
|
+
selector: { 'account_info.username': username },
|
|
2590
|
+
fields: ['_id'],
|
|
2591
|
+
limit: 1,
|
|
2592
|
+
});
|
|
2593
|
+
return ret?.docs?.[0]?._id || null;
|
|
2594
|
+
};
|
|
2595
|
+
|
|
2513
2596
|
export const ts_contact = async function (uid, contact_id) {
|
|
2514
2597
|
try {
|
|
2515
2598
|
const contact_doc = await get_contact(uid, contact_id);
|
|
@@ -2603,12 +2686,44 @@ const set_account_profile_picture = async function (uid, account_uid, metadata,
|
|
|
2603
2686
|
|
|
2604
2687
|
const account_save_ret = await db_module.save_couch_doc('xuda_accounts', account_obj);
|
|
2605
2688
|
await update_account_profile_picture_status(account_uid, 3);
|
|
2689
|
+
// Avatar finished generating -> the account is fully set up; send the
|
|
2690
|
+
// one-time welcome email (idempotent, skips mentors/ambassadors).
|
|
2691
|
+
maybe_send_welcome_email(account_uid);
|
|
2606
2692
|
}
|
|
2607
2693
|
} catch (err) {
|
|
2608
2694
|
await update_account_profile_picture_status(account_uid, 1, err.message);
|
|
2609
2695
|
}
|
|
2610
2696
|
};
|
|
2611
2697
|
|
|
2698
|
+
// Internal trigger used by the widget Google-signup flow: when a freshly-created
|
|
2699
|
+
// visitor account already has a profile_picture (their Google photo) but no
|
|
2700
|
+
// generated avatar yet, kick off avatar generation. set_account_profile_picture
|
|
2701
|
+
// maintains profile_avatar_stat (1 → 2 → 3); the caller polls that. Mirrors the
|
|
2702
|
+
// opportunistic trigger in update_account_info (profile_avatar_stat !== 2 guards
|
|
2703
|
+
// against re-triggering while a generation is mid-flight).
|
|
2704
|
+
export const ensure_profile_avatar = async function (req, job_id, headers) {
|
|
2705
|
+
try {
|
|
2706
|
+
const { uid } = req || {};
|
|
2707
|
+
if (!uid) return { code: -1, data: 'missing uid' };
|
|
2708
|
+
const { code, data: account_obj } = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
2709
|
+
if (code < 0 || !account_obj) return { code: -1, data: 'account not found' };
|
|
2710
|
+
const info = account_obj.account_info || {};
|
|
2711
|
+
if (info.profile_picture && !info.profile_avatar && info.profile_avatar_stat !== 2) {
|
|
2712
|
+
// Best-effort profile context for AI-usage attribution. A freshly-created
|
|
2713
|
+
// widget visitor may have no profile/project yet — tolerate that.
|
|
2714
|
+
let account_profile_info;
|
|
2715
|
+
try {
|
|
2716
|
+
account_profile_info = await get_active_account_profile_info(uid);
|
|
2717
|
+
} catch (e) {}
|
|
2718
|
+
// fire-and-forget inside this worker; generation is heavy (vision + image ops)
|
|
2719
|
+
set_account_profile_picture(uid, uid, info, job_id, headers, account_profile_info);
|
|
2720
|
+
}
|
|
2721
|
+
return { code: 1, data: { profile_avatar_stat: info.profile_avatar_stat || 0 } };
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
return { code: -1, data: err.message };
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2726
|
+
|
|
2612
2727
|
setTimeout(async () => {
|
|
2613
2728
|
const app_id = 'prj712ffdf5aa8adce6cedef988f9c12392'; //'prj3937cb6f9a31c8c7dea25055bba845b1'; //
|
|
2614
2729
|
const uid = 'd39126e0e2c51ffbd1aad10709fc8335';
|
|
@@ -4015,6 +4130,7 @@ export const update_entity_account_profiles = async function (req, job_id, heade
|
|
|
4015
4130
|
};
|
|
4016
4131
|
|
|
4017
4132
|
export const get_contact = async function (uid, contact_id) {
|
|
4133
|
+
if (!contact_id) return null;
|
|
4018
4134
|
const account_profile_info = await get_active_account_profile_info(uid);
|
|
4019
4135
|
|
|
4020
4136
|
const contact_ret = await db_module.get_app_couch_doc_native(account_profile_info.app_id, contact_id);
|
|
@@ -4291,6 +4407,34 @@ export const get_account_ai_usage_old = async function (req, job_id, headers) {
|
|
|
4291
4407
|
}
|
|
4292
4408
|
};
|
|
4293
4409
|
|
|
4410
|
+
// Push the account's live AI-credit balance to its dashboard WS room, so every
|
|
4411
|
+
// meter (the AiPrompt meter, the sidenav ring, the usage popup) updates the
|
|
4412
|
+
// instant credits are spent or topped up — no refresh. Debounced per-uid so a
|
|
4413
|
+
// burst of usage records (a streaming chat writing many chunks) collapses into a
|
|
4414
|
+
// single balance recompute + emit. Reuses get_account_ai_usage (the ledger read)
|
|
4415
|
+
// and emit_message_to_dashboard (the same uid-room push used for account updates).
|
|
4416
|
+
const _credits_broadcast_timers = {};
|
|
4417
|
+
export const broadcast_credits = function (uid) {
|
|
4418
|
+
if (!uid || uid === 'system' || uid === 'webhook') return;
|
|
4419
|
+
clearTimeout(_credits_broadcast_timers[uid]);
|
|
4420
|
+
_credits_broadcast_timers[uid] = setTimeout(async () => {
|
|
4421
|
+
delete _credits_broadcast_timers[uid];
|
|
4422
|
+
try {
|
|
4423
|
+
const ret = await get_account_ai_usage({ uid });
|
|
4424
|
+
if (!ret || ret.code !== 55) return;
|
|
4425
|
+
const max = Math.round((ret.data?.credits?.total || 0) * 100) / 100;
|
|
4426
|
+
const used = Math.round((ret.data?.usage?.total || 0) * 100) / 100;
|
|
4427
|
+
ws_dashboard_msa.emit_message_to_dashboard({
|
|
4428
|
+
service: 'credits_update',
|
|
4429
|
+
to: [uid],
|
|
4430
|
+
data: { used, max, remaining: Math.round((max - used) * 100) / 100, by_source: ret.data?.credits || {} },
|
|
4431
|
+
});
|
|
4432
|
+
} catch (e) {
|
|
4433
|
+
console.error('[broadcast_credits]', e?.message || e);
|
|
4434
|
+
}
|
|
4435
|
+
}, 800);
|
|
4436
|
+
};
|
|
4437
|
+
|
|
4294
4438
|
export const record_ai_usage = async function (uid, input_tokens, output_tokens, source, prompt, model, metadata = {}, account_profile_info, tools) {
|
|
4295
4439
|
try {
|
|
4296
4440
|
if (!account_profile_info) throw new Error('missing account_profile_info');
|
|
@@ -4319,6 +4463,7 @@ export const record_ai_usage = async function (uid, input_tokens, output_tokens,
|
|
|
4319
4463
|
};
|
|
4320
4464
|
const save_ret = await db_module.save_couch_doc('xuda_usage', usage_doc);
|
|
4321
4465
|
// console.log(save_ret);
|
|
4466
|
+
broadcast_credits(uid); // live meter: this account just spent credits
|
|
4322
4467
|
} catch (err) {
|
|
4323
4468
|
console.error(err);
|
|
4324
4469
|
}
|
|
@@ -4354,6 +4499,7 @@ export const record_ai_credit = async function (uid, credits = 0, source, detail
|
|
|
4354
4499
|
};
|
|
4355
4500
|
const save_ret = await db_module.save_couch_doc('xuda_billing', credit_doc);
|
|
4356
4501
|
// console.log(save_ret);
|
|
4502
|
+
broadcast_credits(credited_uid); // live meter: this account just gained credits
|
|
4357
4503
|
return save_ret;
|
|
4358
4504
|
} catch (err) {
|
|
4359
4505
|
if (!err?.message?.includes('already credited!')) {
|
package/index_ms.mjs
CHANGED
|
@@ -33,18 +33,8 @@ export const increment_account_usage = async function (...args) {
|
|
|
33
33
|
return await broker.send_to_queue("increment_account_usage", ...args);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// deploy_module/cost.mjs and the get_billing_metrics +
|
|
39
|
-
// flush_deployment_usage_to_stripe entry points in stripe_module.
|
|
40
|
-
|
|
41
|
-
export const backfill_app_costs = async function (...args) {
|
|
42
|
-
return await broker.send_to_queue("backfill_app_costs", ...args);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const mark_app_terminated = async function (...args) {
|
|
46
|
-
return await broker.send_to_queue("mark_app_terminated", ...args);
|
|
47
|
-
};
|
|
36
|
+
// mark_app_terminated removed — replaced by app_module.pre_destroy_app
|
|
37
|
+
// invoked via http_module's pre_dispatch hook on destroy_app.
|
|
48
38
|
|
|
49
39
|
export const recompute_abuse_signals = async function (...args) {
|
|
50
40
|
return await broker.send_to_queue("recompute_abuse_signals", ...args);
|
|
@@ -54,12 +44,16 @@ export const get_flagged_accounts = async function (...args) {
|
|
|
54
44
|
return await broker.send_to_queue("get_flagged_accounts", ...args);
|
|
55
45
|
};
|
|
56
46
|
|
|
47
|
+
export const sweep_abuse_signals = async function (...args) {
|
|
48
|
+
return await broker.send_to_queue("sweep_abuse_signals", ...args);
|
|
49
|
+
};
|
|
50
|
+
|
|
57
51
|
export const mark_account_reviewed = async function (...args) {
|
|
58
52
|
return await broker.send_to_queue("mark_account_reviewed", ...args);
|
|
59
53
|
};
|
|
60
54
|
|
|
61
|
-
export const
|
|
62
|
-
return await broker.send_to_queue("
|
|
55
|
+
export const backfill_app_costs = async function (...args) {
|
|
56
|
+
return await broker.send_to_queue("backfill_app_costs", ...args);
|
|
63
57
|
};
|
|
64
58
|
|
|
65
59
|
export const get_account_data = async function (...args) {
|
|
@@ -102,6 +96,10 @@ export const get_account_name = async function (...args) {
|
|
|
102
96
|
return await broker.send_to_queue("get_account_name", ...args);
|
|
103
97
|
};
|
|
104
98
|
|
|
99
|
+
export const get_uid_by_username = async function (...args) {
|
|
100
|
+
return await broker.send_to_queue("get_uid_by_username", ...args);
|
|
101
|
+
};
|
|
102
|
+
|
|
105
103
|
export const account_validate_username = async function (...args) {
|
|
106
104
|
return await broker.send_to_queue("account_validate_username", ...args);
|
|
107
105
|
};
|
|
@@ -206,6 +204,10 @@ export const ts_contact = async function (...args) {
|
|
|
206
204
|
return await broker.send_to_queue("ts_contact", ...args);
|
|
207
205
|
};
|
|
208
206
|
|
|
207
|
+
export const ensure_profile_avatar = async function (...args) {
|
|
208
|
+
return await broker.send_to_queue("ensure_profile_avatar", ...args);
|
|
209
|
+
};
|
|
210
|
+
|
|
209
211
|
export const add_contact = async function (...args) {
|
|
210
212
|
return await broker.send_to_queue("add_contact", ...args);
|
|
211
213
|
};
|
|
@@ -353,3 +355,15 @@ export const read_accounts_emails = async function (...args) {
|
|
|
353
355
|
export const process_accounts_emails = async function (...args) {
|
|
354
356
|
return await broker.send_to_queue("process_accounts_emails", ...args);
|
|
355
357
|
};
|
|
358
|
+
|
|
359
|
+
export const get_account_vps = async function (...args) {
|
|
360
|
+
return await broker.send_to_queue("get_account_vps", ...args);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export const get_account_advertising = async function (...args) {
|
|
364
|
+
return await broker.send_to_queue("get_account_advertising", ...args);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export const get_account_static_websites = async function (...args) {
|
|
368
|
+
return await broker.send_to_queue("get_account_static_websites", ...args);
|
|
369
|
+
};
|
package/index_msa.mjs
CHANGED
|
@@ -33,31 +33,27 @@ export const increment_account_usage = function (...args) {
|
|
|
33
33
|
broker.send_to_queue_async("increment_account_usage", ...args);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// mark_app_terminated removed — replaced by app_module.pre_destroy_app
|
|
37
|
+
// invoked via http_module's pre_dispatch hook on destroy_app.
|
|
38
38
|
|
|
39
|
-
export const
|
|
40
|
-
|
|
39
|
+
export const recompute_abuse_signals = function (...args) {
|
|
40
|
+
broker.send_to_queue_async("recompute_abuse_signals", ...args);
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
export const
|
|
44
|
-
|
|
43
|
+
export const get_flagged_accounts = function (...args) {
|
|
44
|
+
broker.send_to_queue_async("get_flagged_accounts", ...args);
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
export const
|
|
48
|
-
|
|
47
|
+
export const sweep_abuse_signals = function (...args) {
|
|
48
|
+
broker.send_to_queue_async("sweep_abuse_signals", ...args);
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
export const
|
|
52
|
-
|
|
51
|
+
export const mark_account_reviewed = function (...args) {
|
|
52
|
+
broker.send_to_queue_async("mark_account_reviewed", ...args);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
export const
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const sweep_abuse_signals = function (...args) {
|
|
60
|
-
broker.send_to_queue_async("sweep_abuse_signals", ...args);
|
|
55
|
+
export const backfill_app_costs = function (...args) {
|
|
56
|
+
broker.send_to_queue_async("backfill_app_costs", ...args);
|
|
61
57
|
};
|
|
62
58
|
|
|
63
59
|
export const get_account_data = function (...args) {
|
|
@@ -76,6 +72,14 @@ export const get_account_deployments = function (...args) {
|
|
|
76
72
|
broker.send_to_queue_async("get_account_deployments", ...args);
|
|
77
73
|
};
|
|
78
74
|
|
|
75
|
+
export const get_account_advertising = function (...args) {
|
|
76
|
+
broker.send_to_queue_async("get_account_advertising", ...args);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const get_account_static_websites = function (...args) {
|
|
80
|
+
broker.send_to_queue_async("get_account_static_websites", ...args);
|
|
81
|
+
};
|
|
82
|
+
|
|
79
83
|
export const get_account_instances = function (...args) {
|
|
80
84
|
broker.send_to_queue_async("get_account_instances", ...args);
|
|
81
85
|
};
|
|
@@ -204,6 +208,10 @@ export const ts_contact = function (...args) {
|
|
|
204
208
|
broker.send_to_queue_async("ts_contact", ...args);
|
|
205
209
|
};
|
|
206
210
|
|
|
211
|
+
export const ensure_profile_avatar = function (...args) {
|
|
212
|
+
broker.send_to_queue_async("ensure_profile_avatar", ...args);
|
|
213
|
+
};
|
|
214
|
+
|
|
207
215
|
export const add_contact = function (...args) {
|
|
208
216
|
broker.send_to_queue_async("add_contact", ...args);
|
|
209
217
|
};
|