@xuda.io/account_module 1.2.2296 → 1.2.2298
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/download_model.js +56 -0
- package/download_model.mjs +56 -0
- package/get_active_account_profile_info.mjs +21 -0
- package/get_user_info.mjs +126 -0
- package/index copy.js +1467 -0
- package/index.js +1467 -0
- package/index.mjs +81 -17
- package/index.mjs.premerge.bak +4725 -0
- package/package.json +1 -1
- package/scripts/run_backfill.mjs.premerge.bak +51 -0
- package/scripts/run_migrate_account.mjs.premerge.bak +54 -0
- package/scripts/run_migrate_all.mjs.premerge.bak +109 -0
package/index.mjs
CHANGED
|
@@ -1925,8 +1925,18 @@ export const ops_reinstate_account = async function (req = {}) {
|
|
|
1925
1925
|
export const ops_list_terminations = async function (req = {}) {
|
|
1926
1926
|
try {
|
|
1927
1927
|
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
1928
|
-
|
|
1929
|
-
|
|
1928
|
+
// Suspended OR terminated. CouchDB Mango uses one index per query, so it
|
|
1929
|
+
// cannot serve an $or across two different fields and would full-scan every
|
|
1930
|
+
// account (logs "documents examined is high"). Query each branch separately
|
|
1931
|
+
// (each hits its own [docType, account_*_status] index) and merge, deduping
|
|
1932
|
+
// accounts flagged as both.
|
|
1933
|
+
const [susp_ret, term_ret] = await Promise.all([
|
|
1934
|
+
db_module.find_couch_query('xuda_accounts', { selector: { docType: 'account', account_suspension_status: 1 }, limit: 9999 }, true),
|
|
1935
|
+
db_module.find_couch_query('xuda_accounts', { selector: { docType: 'account', account_termination_status: 1 }, limit: 9999 }, true),
|
|
1936
|
+
]);
|
|
1937
|
+
const by_id = new Map();
|
|
1938
|
+
for (const a of [...(susp_ret?.docs || []), ...(term_ret?.docs || [])]) by_id.set(a._id, a);
|
|
1939
|
+
const rows = [...by_id.values()].map((a) => ({
|
|
1930
1940
|
account_uid: a._id,
|
|
1931
1941
|
email: a.account_info?.email,
|
|
1932
1942
|
name: `${a.account_info?.first_name || ''} ${a.account_info?.last_name || ''}`.trim(),
|
|
@@ -1999,10 +2009,10 @@ export const ops_account_snapshot = async function (req = {}) {
|
|
|
1999
2009
|
const a = ar.data;
|
|
2000
2010
|
let credits = { max: 0, used: 0, remaining: 0 };
|
|
2001
2011
|
try {
|
|
2002
|
-
const cr = await get_account_ai_usage({ uid: account_uid });
|
|
2012
|
+
const cr = await get_account_ai_usage({ uid: account_uid, ..._LIFETIME_WINDOW });
|
|
2003
2013
|
if (cr && cr.code === 55) {
|
|
2004
2014
|
const max = Math.round((cr.data?.credits?.total || 0) * 100) / 100;
|
|
2005
|
-
const used = Math.round((cr.data?.usage
|
|
2015
|
+
const used = Math.round(_usage_total(cr.data?.usage) * 100) / 100;
|
|
2006
2016
|
credits = { max, used, remaining: Math.round((max - used) * 100) / 100, by_source: cr.data?.credits || {} };
|
|
2007
2017
|
}
|
|
2008
2018
|
} catch (e) { console.error('[ops snapshot credits]', e.message); }
|
|
@@ -6471,11 +6481,13 @@ export const save_cache_hit = async function (_id) {
|
|
|
6471
6481
|
///////////////////////////////
|
|
6472
6482
|
|
|
6473
6483
|
export const get_account_ai_usage = async function (req, job_id, headers) {
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6484
|
+
// Default to the LIFETIME window (all-time spend) — the credit model is a
|
|
6485
|
+
// remaining-pool ledger (see _LIFETIME_WINDOW + the Credit Management page).
|
|
6486
|
+
// Callers that want a specific range (the usage chart in WorkspaceUsage.vue)
|
|
6487
|
+
// pass explicit year/month/day. The nav meter + ring call with no window, so
|
|
6488
|
+
// this makes them read lifetime "used" and match the Credit Management page
|
|
6489
|
+
// instead of a current-month slice against a lifetime pool.
|
|
6490
|
+
const { uid, year_from = 2000, month_from = 1, day_from = 1, year_to = 2999, month_to = 12, day_to = 31 } = req;
|
|
6479
6491
|
try {
|
|
6480
6492
|
const app_id = await get_account_default_project_id(uid);
|
|
6481
6493
|
|
|
@@ -6499,8 +6511,13 @@ export const get_account_ai_usage = async function (req, job_id, headers) {
|
|
|
6499
6511
|
|
|
6500
6512
|
// console.log('ai_usage', ai_usage.rows);
|
|
6501
6513
|
|
|
6502
|
-
let total_usage = response.total_usage;
|
|
6503
6514
|
let profile = response.profiles;
|
|
6515
|
+
// The ai_usage2 list function emits per-profile spend but sometimes a null
|
|
6516
|
+
// rolled-up total. Roll it up HERE at the source so every consumer gets a real
|
|
6517
|
+
// usage.total — including the dashboard nav (shared-store fetchCredits reads
|
|
6518
|
+
// data.usage.total directly). Without this, Number(null) -> 0 hid all spend in
|
|
6519
|
+
// the meter/ring and disabled the account-wide credit cap.
|
|
6520
|
+
let total_usage = _usage_total({ total: response.total_usage, profile });
|
|
6504
6521
|
|
|
6505
6522
|
const ai_credits = await db_module.get_couch_view('xuda_billing', 'ai_credits', {
|
|
6506
6523
|
startkey: [uid, ''],
|
|
@@ -6610,6 +6627,23 @@ const _fold_usage = function (profile_map) {
|
|
|
6610
6627
|
return { by_profile, by_user };
|
|
6611
6628
|
};
|
|
6612
6629
|
|
|
6630
|
+
// Single source of truth for an account's rolled-up AI-credit usage. The usage
|
|
6631
|
+
// view reliably emits per-profile spend but sometimes returns a null rolled-up
|
|
6632
|
+
// total (usage.total); Number(null)||0 then collapsed "used" to 0 EVERYWHERE —
|
|
6633
|
+
// hiding spend in the nav/meter AND making the account-wide credit cap never
|
|
6634
|
+
// fire. Prefer the view's own total when finite, otherwise sum the per-profile
|
|
6635
|
+
// fold (same source, same credit unit).
|
|
6636
|
+
const _usage_total = function (usage_data) {
|
|
6637
|
+
// NOTE: Number(null) === 0 (and IS finite), which is exactly what collapsed
|
|
6638
|
+
// "used" to 0 — so guard on the RAW type, not the coerced number. Only trust
|
|
6639
|
+
// the view's own total when it is a real finite number; otherwise roll up the
|
|
6640
|
+
// per-profile fold.
|
|
6641
|
+
const t = usage_data?.total;
|
|
6642
|
+
if (typeof t === 'number' && Number.isFinite(t)) return t;
|
|
6643
|
+
const { by_profile } = _fold_usage(usage_data?.profile);
|
|
6644
|
+
return Object.values(by_profile).reduce((a, b) => a + (Number(b) || 0), 0);
|
|
6645
|
+
};
|
|
6646
|
+
|
|
6613
6647
|
// Wide window so "used" reads as all-time spend (the lifetime / remaining-pool model).
|
|
6614
6648
|
const _LIFETIME_WINDOW = { year_from: 2000, month_from: 1, day_from: 1, year_to: 2999, month_to: 12, day_to: 31 };
|
|
6615
6649
|
|
|
@@ -6625,8 +6659,8 @@ const _get_scoped_usage = async function (owner_uid, need_raw) {
|
|
|
6625
6659
|
if (cached && now - cached.ts < _SCOPED_USAGE_TTL && (!need_raw || cached.has_raw)) return cached.data;
|
|
6626
6660
|
|
|
6627
6661
|
const usage_ret = await get_account_ai_usage({ uid: owner_uid, ..._LIFETIME_WINDOW });
|
|
6628
|
-
const total = Number(usage_ret?.data?.usage?.total) || 0;
|
|
6629
6662
|
const { by_profile, by_user } = _fold_usage(usage_ret?.data?.usage?.profile);
|
|
6663
|
+
const total = _usage_total(usage_ret?.data?.usage);
|
|
6630
6664
|
|
|
6631
6665
|
let by_model = {};
|
|
6632
6666
|
let by_source = {};
|
|
@@ -6757,8 +6791,8 @@ export const evaluate_credit_gate = async function (req) {
|
|
|
6757
6791
|
|
|
6758
6792
|
// Back-compat: no rules or disabled => legacy single ledger cap, byte-for-byte.
|
|
6759
6793
|
if (!rules || rules.enabled === false) {
|
|
6760
|
-
const usage = await get_account_ai_usage({ uid: owner });
|
|
6761
|
-
const over = (Number(usage?.data?.credits?.total) || 0) - (
|
|
6794
|
+
const usage = await get_account_ai_usage({ uid: owner, ..._LIFETIME_WINDOW });
|
|
6795
|
+
const over = (Number(usage?.data?.credits?.total) || 0) - _usage_total(usage?.data?.usage) < -0.5;
|
|
6762
6796
|
return { block: over, scope: over ? 'pool' : null, reason: over ? _SCOPE_REASON.ledger : '', hard_breached: over ? [{ scope: 'pool', key: 'ledger' }] : [], soft_breached: [], account_id: owner };
|
|
6763
6797
|
}
|
|
6764
6798
|
|
|
@@ -7105,10 +7139,13 @@ export const broadcast_credits = function (uid) {
|
|
|
7105
7139
|
_credits_broadcast_timers[uid] = setTimeout(async () => {
|
|
7106
7140
|
delete _credits_broadcast_timers[uid];
|
|
7107
7141
|
try {
|
|
7108
|
-
|
|
7142
|
+
// Lifetime window (matches the Credit Management meter) + the same null-total
|
|
7143
|
+
// rollup as _get_scoped_usage — get_account_ai_usage can return a null
|
|
7144
|
+
// rolled-up usage.total, which otherwise broadcasts used:0 to the live nav.
|
|
7145
|
+
const ret = await get_account_ai_usage({ uid, ..._LIFETIME_WINDOW });
|
|
7109
7146
|
if (!ret || ret.code !== 55) return;
|
|
7110
7147
|
const max = Math.round((ret.data?.credits?.total || 0) * 100) / 100;
|
|
7111
|
-
const used = Math.round((ret.data?.usage
|
|
7148
|
+
const used = Math.round(_usage_total(ret.data?.usage) * 100) / 100;
|
|
7112
7149
|
ws_dashboard_msa.emit_message_to_dashboard({
|
|
7113
7150
|
service: 'credits_update',
|
|
7114
7151
|
to: [uid],
|
|
@@ -7547,6 +7584,26 @@ const _nl_one = async (db, docType) => {
|
|
|
7547
7584
|
}
|
|
7548
7585
|
})();
|
|
7549
7586
|
|
|
7587
|
+
// The abuse-review queue (the flagged-accounts list, see ~line 1014) selects on
|
|
7588
|
+
// `abuse_signals.flagged` with no index, so Couch full-scans xuda_accounts and
|
|
7589
|
+
// logs "No matching index found". xuda_accounts lives locally on dev + master;
|
|
7590
|
+
// ensure the index at module load, idempotent, on those content-home nodes only.
|
|
7591
|
+
(async () => {
|
|
7592
|
+
const host = process.env.XUDA_HOSTNAME;
|
|
7593
|
+
if (host !== 'dev.xuda.ai' && host !== 'master.xuda.ai') return;
|
|
7594
|
+
try {
|
|
7595
|
+
const ret = await db_module.create_couch_index('xuda_accounts', {
|
|
7596
|
+
index: { fields: ['abuse_signals.flagged'] },
|
|
7597
|
+
name: 'idx_abuse_signals_flagged',
|
|
7598
|
+
ddoc: 'idx_abuse_signals_flagged',
|
|
7599
|
+
type: 'json',
|
|
7600
|
+
});
|
|
7601
|
+
if (ret?.error) console.error('[abuse] xuda_accounts abuse_signals.flagged index create failed:', ret.error);
|
|
7602
|
+
} catch (err) {
|
|
7603
|
+
console.error('[abuse] xuda_accounts abuse_signals.flagged index init failed:', err?.message || err);
|
|
7604
|
+
}
|
|
7605
|
+
})();
|
|
7606
|
+
|
|
7550
7607
|
const _nl_fashion_url = (p) => {
|
|
7551
7608
|
const rel = p.custom_url || (p.slug ? '/' + p.slug : '');
|
|
7552
7609
|
return `https://xuda.fashion${rel}`;
|
|
@@ -7907,7 +7964,12 @@ export const newsletter_generate = async function (req = {}) {
|
|
|
7907
7964
|
if (!req.system && !_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
7908
7965
|
// Master-only (dev exempt for testing): the issue DB must never be created
|
|
7909
7966
|
// on a regional couch, and the weekly coupon/sample flow belongs to master.
|
|
7910
|
-
|
|
7967
|
+
// MASTER ONLY, dev included. Dev is is_debug but still sends real [SAMPLE]/
|
|
7968
|
+
// [OPS] mail and keeps its own issue counter, so a "Generate now" on dev
|
|
7969
|
+
// blasts live info@ inboxes and creates dev-only drafts that never appear in
|
|
7970
|
+
// the master Newsletter Manager (dev does not replicate to master). Preview /
|
|
7971
|
+
// Test still work on dev for reviewing render; only issuing is locked here.
|
|
7972
|
+
if (process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_generate runs on master only' };
|
|
7911
7973
|
await _nl_ensure_storage();
|
|
7912
7974
|
|
|
7913
7975
|
// Two DISTINCT tips: one for "Did you know?", one for "Tip of the week".
|
|
@@ -8068,7 +8130,9 @@ export const newsletter_publish = async function (req = {}) {
|
|
|
8068
8130
|
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
8069
8131
|
// Same master-only gate as generate: the blast must run where xuda_newsletter
|
|
8070
8132
|
// and the authoritative xuda_accounts live (dev exempt for testing).
|
|
8071
|
-
|
|
8133
|
+
// MASTER ONLY, dev included: a publish on dev would blast dev's opted-in
|
|
8134
|
+
// accounts with real mail. Locked so only the real master issues a newsletter.
|
|
8135
|
+
if (process.env.XUDA_HOSTNAME !== 'master.xuda.ai') return { code: -1, data: 'newsletter_publish runs on master only' };
|
|
8072
8136
|
const id = req.id || `nl_issue_${req.issue_number}`;
|
|
8073
8137
|
const gret = await db_module.get_couch_doc(NEWSLETTER_DB, id);
|
|
8074
8138
|
if (gret.code < 0 || !gret.data) return { code: -1, data: 'issue not found' };
|