@xuda.io/ai_module 1.1.5610 → 1.1.5612
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 +1120 -33
- package/index_ms.mjs +24 -0
- package/index_msa.mjs +24 -0
- package/package.json +4 -4
package/index.mjs
CHANGED
|
@@ -232,6 +232,7 @@ const email_ms = await import(`${module_path}/email_module/index_ms.mjs`);
|
|
|
232
232
|
const ws_dashboard_msa = await import(`${module_path}/ws_dashboard_module/index_msa.mjs`);
|
|
233
233
|
const account_msa = await import(`${module_path}/account_module/index_msa.mjs`);
|
|
234
234
|
const drive_msa = await import(`${module_path}/drive_module/index_msa.mjs`);
|
|
235
|
+
const misc_msa = await import(`${module_path}/misc_module/index_msa.mjs`);
|
|
235
236
|
|
|
236
237
|
var open_ai_status = {};
|
|
237
238
|
const report_ai_status = function (model, err) {
|
|
@@ -2150,7 +2151,7 @@ const get_studio_doc = async function (req) {
|
|
|
2150
2151
|
};
|
|
2151
2152
|
|
|
2152
2153
|
export const get_ai_agent_info = async function (uid, job_id, headers, doc, from_marketplace) {
|
|
2153
|
-
const account_profile_info = await get_active_account_profile_info(uid);
|
|
2154
|
+
const account_profile_info = await get_active_account_profile_info(uid, from_marketplace ? doc?.studio_meta?.account_profiles?.[0] : undefined);
|
|
2154
2155
|
|
|
2155
2156
|
const get_contact_border = function (doc) {
|
|
2156
2157
|
const disable_color = `#1a3557`;
|
|
@@ -2671,6 +2672,10 @@ export const delete_ai_agent = async function (req) {
|
|
|
2671
2672
|
agent_doc.ts = Date.now();
|
|
2672
2673
|
const save_ret = await db_module.save_app_couch_doc_native(account_profile_info.app_id, agent_doc);
|
|
2673
2674
|
|
|
2675
|
+
if (agent_doc?.agentConfig?.npm_package_name) {
|
|
2676
|
+
misc_msa.sync_agent_npm_state({ app_id: account_profile_info.app_id, prog_id: agent_id, event: 'deleted' });
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2674
2679
|
for (const [key, val] of Object.entries(agent_doc?.agentConfig?.agent_tools || [])) {
|
|
2675
2680
|
const file_id = val?.youtube?.file_id || val?.file?.file_id;
|
|
2676
2681
|
const vector_store_id = val?.youtube?.vector_store_id || val?.file?.vector_store_id;
|
|
@@ -2883,10 +2888,46 @@ export const delete_mini_app = async function (req) {
|
|
|
2883
2888
|
}
|
|
2884
2889
|
};
|
|
2885
2890
|
|
|
2891
|
+
export const start_cli_agent_conversation = async function (req) {
|
|
2892
|
+
const { uid, agent_id } = req;
|
|
2893
|
+
if (!uid) return { code: -1, data: 'uid required' };
|
|
2894
|
+
if (!agent_id) return { code: -1, data: 'agent_id required' };
|
|
2895
|
+
try {
|
|
2896
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
2897
|
+
const profile_id = account_profile_info.account_profile_id;
|
|
2898
|
+
const conversation_obj = await create_openai_conversation();
|
|
2899
|
+
const conversation_id = await _common.xuda_get_uuid('chat_conversation');
|
|
2900
|
+
const now = Date.now();
|
|
2901
|
+
const doc = {
|
|
2902
|
+
_id: conversation_id,
|
|
2903
|
+
docType: 'chat_conversation',
|
|
2904
|
+
title: 'CLI session',
|
|
2905
|
+
date_created_ts: now,
|
|
2906
|
+
ts: now,
|
|
2907
|
+
stat: 1,
|
|
2908
|
+
uid,
|
|
2909
|
+
messages: [],
|
|
2910
|
+
reference_type: 'ai_agents',
|
|
2911
|
+
reference_id: agent_id,
|
|
2912
|
+
conversation_obj,
|
|
2913
|
+
reference_conversation_id: conversation_obj.id,
|
|
2914
|
+
conversation_type: 'ai_chat',
|
|
2915
|
+
initiator_uid: uid,
|
|
2916
|
+
account_profiles: [profile_id],
|
|
2917
|
+
source: 'cli',
|
|
2918
|
+
};
|
|
2919
|
+
const save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, doc);
|
|
2920
|
+
if (save_ret.code < 0) return save_ret;
|
|
2921
|
+
return { code: 1, data: { conversation_id, profile_id, app_id: account_profile_info.app_id } };
|
|
2922
|
+
} catch (err) {
|
|
2923
|
+
return { code: -1, data: err.message };
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
|
|
2886
2927
|
export const update_ai_agent = async function (req, job_id, headers) {
|
|
2887
|
-
let { agent_id, agentConfig, uid } = req;
|
|
2928
|
+
let { agent_id, agentConfig, uid, profile_id } = req;
|
|
2888
2929
|
|
|
2889
|
-
const account_profile_info = await get_active_account_profile_info(uid);
|
|
2930
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
2890
2931
|
|
|
2891
2932
|
try {
|
|
2892
2933
|
let agent_doc = await await db_module.get_app_couch_doc_native(account_profile_info.app_id, agent_id);
|
|
@@ -3292,11 +3333,11 @@ const save_agent_status = async function (uid, agent_id, stat, agentConfig) {
|
|
|
3292
3333
|
};
|
|
3293
3334
|
|
|
3294
3335
|
export const create_ai_agent = async function (req, job_id, headers) {
|
|
3295
|
-
const { agentConfig, uid } = req;
|
|
3336
|
+
const { agentConfig, uid, profile_id } = req;
|
|
3296
3337
|
|
|
3297
3338
|
const { account_project_id: app_id, account_info } = await account_ms.get_default_project_account_doc(uid);
|
|
3298
3339
|
|
|
3299
|
-
const account_profile_info = await get_active_account_profile_info(uid);
|
|
3340
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
3300
3341
|
const { account_profile_obj } = account_profile_info;
|
|
3301
3342
|
try {
|
|
3302
3343
|
const param = { uid, properties: { menuName: agentConfig.agent_name, menuTitle: agentConfig.agent_name, menuType: 'ai_agent' }, checkedInUserName: account_info.first_name + ' ' + account_info.last_name, app_id: account_profile_info.app_id, parentId: 'ai_agents' };
|
|
@@ -3574,7 +3615,7 @@ export const update_thumbnail = async function (type, doc, app_id, uid, job_id,
|
|
|
3574
3615
|
|
|
3575
3616
|
switch (type) {
|
|
3576
3617
|
case 'ai_agent': {
|
|
3577
|
-
const images_arr = await create_ai_agent_image({ app_id, uid, ai_agent_id: doc._id, tags }, job_id, headers);
|
|
3618
|
+
const images_arr = await create_ai_agent_image({ app_id, uid, ai_agent_id: doc._id, tags, account_profile_info }, job_id, headers);
|
|
3578
3619
|
db_doc = await get_db_doc();
|
|
3579
3620
|
db_doc.studio_meta.thumbnail_request_ts = Date.now();
|
|
3580
3621
|
db_doc.studio_meta.agent_image = [images_arr.data];
|
|
@@ -5113,8 +5154,99 @@ ${conversation_history || `User (studio): ${prompt}`}
|
|
|
5113
5154
|
}
|
|
5114
5155
|
};
|
|
5115
5156
|
|
|
5157
|
+
// Clarifying-questions protocol shared by the vibe/codex path AND the dashboard
|
|
5158
|
+
// CPI agent path. Both emit plain prose by default, so to get a structured
|
|
5159
|
+
// "ask the user" payload (Claude-Code-style options stepper) we instruct them
|
|
5160
|
+
// to emit a JSON block at the end of the response when they would otherwise
|
|
5161
|
+
// ask follow-up questions in prose. The block is parsed off, prose is shipped
|
|
5162
|
+
// as the normal text, and the structured questions ride alongside on the
|
|
5163
|
+
// stream_end event + the persisted chat_conversation_item.
|
|
5164
|
+
const CLARIFYING_QUESTIONS_INSTRUCTION = `
|
|
5165
|
+
If you need clarifying input from the user before proceeding, do NOT ask in prose. Instead, emit a structured questions block at the END of your response:
|
|
5166
|
+
|
|
5167
|
+
<xuda-questions>
|
|
5168
|
+
[
|
|
5169
|
+
{
|
|
5170
|
+
"question": "<full question text>",
|
|
5171
|
+
"options": [
|
|
5172
|
+
{ "label": "<short option label>", "description": "<one-sentence explanation of what this option means>" },
|
|
5173
|
+
{ "label": "<short option label>", "description": "<one-sentence explanation of what this option means>" },
|
|
5174
|
+
{ "label": "<short option label>", "description": "<one-sentence explanation of what this option means>" }
|
|
5175
|
+
]
|
|
5176
|
+
}
|
|
5177
|
+
]
|
|
5178
|
+
</xuda-questions>
|
|
5179
|
+
|
|
5180
|
+
Rules:
|
|
5181
|
+
- 2 to 3 options per question. The UI automatically adds an "Other (tell me what you meant)" free-text option, so do NOT include it yourself.
|
|
5182
|
+
- You may include multiple questions in the array; the UI will step through them one at a time.
|
|
5183
|
+
- Put the block at the very end, after any explanatory prose. The prose before the block will be shown to the user as your message.
|
|
5184
|
+
- If you do not need clarification, do not emit the block. Only emit it when answering is genuinely blocked on missing information.
|
|
5185
|
+
`.trim();
|
|
5186
|
+
|
|
5187
|
+
const extract_xuda_questions = function (text) {
|
|
5188
|
+
if (typeof text !== 'string') return { prose: text, questions: null };
|
|
5189
|
+
const match = text.match(/<xuda-questions>\s*([\s\S]+?)\s*<\/xuda-questions>\s*$/);
|
|
5190
|
+
if (!match) return { prose: text, questions: null };
|
|
5191
|
+
let parsed;
|
|
5192
|
+
try {
|
|
5193
|
+
parsed = JSON.parse(match[1]);
|
|
5194
|
+
} catch (e) {
|
|
5195
|
+
return { prose: text, questions: null };
|
|
5196
|
+
}
|
|
5197
|
+
if (!Array.isArray(parsed) || !parsed.length) return { prose: text, questions: null };
|
|
5198
|
+
const validated = parsed
|
|
5199
|
+
.map((q) => {
|
|
5200
|
+
if (!q || typeof q.question !== 'string') return null;
|
|
5201
|
+
const options = Array.isArray(q.options)
|
|
5202
|
+
? q.options
|
|
5203
|
+
.map((opt) => (opt && typeof opt.label === 'string' ? { label: opt.label.trim(), description: typeof opt.description === 'string' ? opt.description.trim() : '' } : null))
|
|
5204
|
+
.filter(Boolean)
|
|
5205
|
+
.slice(0, 3)
|
|
5206
|
+
: [];
|
|
5207
|
+
if (options.length < 2) return null;
|
|
5208
|
+
return { question: q.question.trim(), options };
|
|
5209
|
+
})
|
|
5210
|
+
.filter(Boolean);
|
|
5211
|
+
if (!validated.length) return { prose: text, questions: null };
|
|
5212
|
+
return {
|
|
5213
|
+
prose: text.slice(0, match.index).trim(),
|
|
5214
|
+
questions: validated,
|
|
5215
|
+
};
|
|
5216
|
+
};
|
|
5217
|
+
|
|
5218
|
+
// Resolve the structured context object passed by newer clients (ProjectAiPanel
|
|
5219
|
+
// sends { project_id, tab, view, vibe_mode }) or fall back to the legacy string
|
|
5220
|
+
// shape that older callsites still send. Returns an object — string-shaped legacy
|
|
5221
|
+
// inputs are wrapped as { tab: <string> } so downstream readers have one shape.
|
|
5222
|
+
const get_dashboard_context_obj = function (req, conversation_doc) {
|
|
5223
|
+
const candidate = req.context || req.metadata?.context || conversation_doc.context || conversation_doc.metadata?.context;
|
|
5224
|
+
if (candidate && typeof candidate === 'object') return candidate;
|
|
5225
|
+
const legacy_str = (candidate || req.dashboard_context || conversation_doc.dashboard_context || '').toString().trim().toLowerCase();
|
|
5226
|
+
return legacy_str ? { tab: legacy_str } : {};
|
|
5227
|
+
};
|
|
5228
|
+
|
|
5229
|
+
// Primary routing signal: replaces the legacy `dashboard_context === 'console'`
|
|
5230
|
+
// check. Newer clients set `context.vibe_mode = true` when the user is in vibe
|
|
5231
|
+
// mode (full-screen VPS preview) — that's the canonical "send this through the
|
|
5232
|
+
// codex/console runner" flag. dashboard_context is being deprecated as a
|
|
5233
|
+
// routing concept; it remains only as a hint string (context.tab) used by the
|
|
5234
|
+
// CPI tools agent for keyword-based tool selection in the non-vibe branch.
|
|
5235
|
+
const is_vibe_mode_request = function (req, conversation_doc) {
|
|
5236
|
+
const ctx = get_dashboard_context_obj(req, conversation_doc);
|
|
5237
|
+
if (ctx.vibe_mode === true) return true;
|
|
5238
|
+
// Backwards compat: explicit legacy `context: 'console'` still triggers the
|
|
5239
|
+
// codex path. Remove once no callsites pass the string form.
|
|
5240
|
+
if (ctx.tab === 'console') return true;
|
|
5241
|
+
return false;
|
|
5242
|
+
};
|
|
5243
|
+
|
|
5116
5244
|
const get_dashboard_chat_context = function (req, conversation_doc) {
|
|
5117
|
-
|
|
5245
|
+
const ctx = get_dashboard_context_obj(req, conversation_doc);
|
|
5246
|
+
// The legacy "dashboard_context" string was an ad-hoc tab identifier; the new
|
|
5247
|
+
// object's `tab` field is the same concept. Returning '' here lets the CPI
|
|
5248
|
+
// tools agent still do prompt-only keyword matching.
|
|
5249
|
+
return (ctx.tab || '').toString().trim().toLowerCase();
|
|
5118
5250
|
};
|
|
5119
5251
|
|
|
5120
5252
|
const get_dashboard_cpi_keywords = function (context, prompt = '') {
|
|
@@ -5277,6 +5409,7 @@ const dashboard_chat = async function (req, job_id, headers) {
|
|
|
5277
5409
|
|
|
5278
5410
|
const conversation_id = conversation_doc._id;
|
|
5279
5411
|
const dashboard_context = get_dashboard_chat_context(req, conversation_doc);
|
|
5412
|
+
const vibe_mode = is_vibe_mode_request(req, conversation_doc);
|
|
5280
5413
|
const target_app_id = (await _common.get_project_app_id(req.app_id || conversation_doc.reference_id || account_profile_info.app_id, true)) || account_profile_info.app_id;
|
|
5281
5414
|
const prompt_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
|
|
5282
5415
|
const response_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
|
|
@@ -5304,7 +5437,19 @@ const dashboard_chat = async function (req, job_id, headers) {
|
|
|
5304
5437
|
? {
|
|
5305
5438
|
stream_id: response_conversation_item_id,
|
|
5306
5439
|
final_seq: stream_delta_seq,
|
|
5307
|
-
text:
|
|
5440
|
+
// params.text override exists for the clarifying-questions flow:
|
|
5441
|
+
// when the agent embedded an <xuda-questions> block in its
|
|
5442
|
+
// streamed output, the live stream_delta events already
|
|
5443
|
+
// shipped the block to the client. The caller passes the
|
|
5444
|
+
// cleaned prose here so handleStreamEnd can reconcile the
|
|
5445
|
+
// visible bubble against the block-free version (uses the
|
|
5446
|
+
// existing missed-deltas reconciliation path on the client).
|
|
5447
|
+
text: typeof params?.text === 'string' ? params.text : stream_delta_text,
|
|
5448
|
+
// Structured clarifying-questions payload — when the agent
|
|
5449
|
+
// emits an <xuda-questions> block, we parse it, strip it
|
|
5450
|
+
// from the prose, and ship it here so the client can render
|
|
5451
|
+
// the stepper artifact alongside the message.
|
|
5452
|
+
...(params?.questions ? { questions: params.questions } : {}),
|
|
5308
5453
|
}
|
|
5309
5454
|
: {}),
|
|
5310
5455
|
prompt_conversation_item_id,
|
|
@@ -5418,7 +5563,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
5418
5563
|
|
|
5419
5564
|
try {
|
|
5420
5565
|
emitToDashboard('stream_start');
|
|
5421
|
-
emitToDashboard('stream_phase',
|
|
5566
|
+
emitToDashboard('stream_phase', vibe_mode ? 'Starting vibe request' : 'Starting dashboard request');
|
|
5422
5567
|
|
|
5423
5568
|
await saveUserItem();
|
|
5424
5569
|
|
|
@@ -5432,19 +5577,68 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
5432
5577
|
conversation_doc.stat = 2;
|
|
5433
5578
|
conversation_doc.request_started_at = request_started_at;
|
|
5434
5579
|
conversation_doc.job_id = job_id;
|
|
5435
|
-
|
|
5580
|
+
// Persist the structured context (object shape) so multi-turn requests
|
|
5581
|
+
// inherit vibe_mode even when the client doesn't resend it. The legacy
|
|
5582
|
+
// string-form `context = dashboard_context` is dropped — vibe_mode is now
|
|
5583
|
+
// the routing signal and `tab` lives inside the object.
|
|
5584
|
+
conversation_doc.context = { ...get_dashboard_context_obj(req, conversation_doc), vibe_mode };
|
|
5436
5585
|
conversation_doc.target_app_id = target_app_id;
|
|
5437
5586
|
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5438
5587
|
|
|
5439
|
-
if (
|
|
5440
|
-
|
|
5588
|
+
if (vibe_mode) {
|
|
5589
|
+
// Resolve the project's VPS IP — codex SSHes into that host to run
|
|
5590
|
+
// commands. Without this, execute_codex_request was pulling `ip` from
|
|
5591
|
+
// `req.ip` (the user's browser IP), which never matches a real VPS
|
|
5592
|
+
// and either fails the SSH or hangs codex against an unreachable host.
|
|
5593
|
+
let vibe_app_obj = null;
|
|
5594
|
+
try {
|
|
5595
|
+
vibe_app_obj = await get_app_obj(target_app_id);
|
|
5596
|
+
} catch (e) {
|
|
5597
|
+
// get_app_obj throws on missing doc — treat as "no project found"
|
|
5598
|
+
// and fall through to the missing-VPS error path below.
|
|
5599
|
+
}
|
|
5600
|
+
const vps_ip = vibe_app_obj?.app_hosting_server?.ip;
|
|
5601
|
+
const project_name = vibe_app_obj?.menuName || vibe_app_obj?.app_name || vibe_app_obj?.name || '';
|
|
5602
|
+
|
|
5603
|
+
if (!vps_ip) {
|
|
5604
|
+
// No deployed VPS for this project — surface a clear, actionable
|
|
5605
|
+
// message instead of letting codex fail silently or SSH to the user.
|
|
5606
|
+
const missing_vps_msg = `This project does not have a deployed VPS yet, so I can't run vibe-mode commands. Open the project's Instances tab and deploy a VPS first, then try again.`;
|
|
5607
|
+
emitToDashboard('response_start');
|
|
5608
|
+
streamText(missing_vps_msg);
|
|
5609
|
+
emitToDashboard('stream_end');
|
|
5610
|
+
conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
5611
|
+
conversation_doc.ts = Date.now();
|
|
5612
|
+
conversation_doc.stat = 3;
|
|
5613
|
+
conversation_doc.process_stat = 'partial';
|
|
5614
|
+
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5615
|
+
return await saveAssistantItem(missing_vps_msg, { vibe_missing_vps: true });
|
|
5616
|
+
}
|
|
5617
|
+
|
|
5618
|
+
// System block: questions protocol + project/VPS context so codex's
|
|
5619
|
+
// response doesn't drift to a generic answer. Codex sees one prompt
|
|
5620
|
+
// string; the [System]/[User request] framing keeps the boundary
|
|
5621
|
+
// visible to it.
|
|
5622
|
+
const project_context_block = `You are operating in vibe mode for Xuda project "${project_name || target_app_id}" (app_id: ${target_app_id}).\nThe deployed VPS for this project is at ${vps_ip}; all shell commands run there over SSH.\nWhen the user asks you to build, change, or inspect something on "this VPS", that means ${vps_ip}.`;
|
|
5623
|
+
const codex_prompt = `[System]\n${project_context_block}\n\n${CLARIFYING_QUESTIONS_INSTRUCTION}\n\n[User request]\n${prompt}`;
|
|
5624
|
+
|
|
5625
|
+
// Override ip explicitly so the `...req` spread can't smuggle in
|
|
5626
|
+
// req.ip (browser IP). conversation_id / conversation_doc / prompt /
|
|
5627
|
+
// attachments / stream stay after the spread for the same reason.
|
|
5628
|
+
const codex_ret = await execute_codex_request({ ...req, ip: vps_ip, conversation_id, conversation_doc, prompt: codex_prompt, attachments, stream: false }, job_id, headers);
|
|
5441
5629
|
const codex_events = codex_ret?.data?.events || [];
|
|
5442
5630
|
const last_message = [...codex_events].reverse().find((event) => event?.item?.type === 'agent_message' && event.item.text)?.item?.text;
|
|
5443
|
-
const
|
|
5631
|
+
const raw_response_text = codex_ret.code < 0 ? `Vibe request failed: ${get_error_message(codex_ret.data, 'Codex request failed')}` : last_message || 'Vibe request completed.';
|
|
5632
|
+
|
|
5633
|
+
// Pull the structured questions block (if any) off the tail of the
|
|
5634
|
+
// response. response_text is just the prose; questions ride alongside
|
|
5635
|
+
// on the stream_end event + the persisted assistant item.
|
|
5636
|
+
const { prose, questions } = extract_xuda_questions(raw_response_text);
|
|
5637
|
+
const response_text = prose || raw_response_text;
|
|
5444
5638
|
|
|
5445
5639
|
emitToDashboard('response_start');
|
|
5446
5640
|
streamText(response_text);
|
|
5447
|
-
emitToDashboard('stream_end');
|
|
5641
|
+
emitToDashboard('stream_end', undefined, questions ? { questions } : undefined);
|
|
5448
5642
|
|
|
5449
5643
|
conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
5450
5644
|
conversation_doc.ts = Date.now();
|
|
@@ -5452,7 +5646,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
5452
5646
|
conversation_doc.process_stat = codex_ret.code < 0 ? 'partial' : 'full';
|
|
5453
5647
|
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5454
5648
|
|
|
5455
|
-
return await saveAssistantItem(response_text, { codex_result: codex_ret });
|
|
5649
|
+
return await saveAssistantItem(response_text, { codex_result: codex_ret, ...(questions ? { questions } : {}) });
|
|
5456
5650
|
}
|
|
5457
5651
|
|
|
5458
5652
|
const dashboard_prompt = await getDashboardContextPrompt();
|
|
@@ -5476,6 +5670,8 @@ If the screenshot or prompt implies the tab/context, infer the relevant dashboar
|
|
|
5476
5670
|
For data/database requests, use the database/table CPI methods and keep app_id/reference_id anchored to "${target_app_id}".
|
|
5477
5671
|
Summarize the action and important CPI result fields clearly. Do not invent successful changes; report tool errors as errors.
|
|
5478
5672
|
Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}
|
|
5673
|
+
|
|
5674
|
+
${CLARIFYING_QUESTIONS_INSTRUCTION}
|
|
5479
5675
|
`.trim(),
|
|
5480
5676
|
model: ai_model,
|
|
5481
5677
|
tools: cpi_tools_ret.tools,
|
|
@@ -5506,7 +5702,16 @@ Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}
|
|
|
5506
5702
|
}
|
|
5507
5703
|
|
|
5508
5704
|
response_text = response_text || output?.state?._currentStep?.output || output?.finalOutput || output?.output || 'Dashboard request completed.';
|
|
5509
|
-
|
|
5705
|
+
|
|
5706
|
+
// Pull off the structured clarifying-questions block (if the agent emitted
|
|
5707
|
+
// one) before persistence + stream_end. The block was streamed live to the
|
|
5708
|
+
// client during stream_delta, so the client's handleStreamEnd will use the
|
|
5709
|
+
// `text` field on stream_end to overwrite the visible bubble with the
|
|
5710
|
+
// cleaned prose (see AiChat.vue handleStreamEnd reconciliation).
|
|
5711
|
+
const { prose: dashboard_prose, questions: dashboard_questions } = extract_xuda_questions(response_text);
|
|
5712
|
+
const final_response_text = dashboard_prose || response_text;
|
|
5713
|
+
|
|
5714
|
+
emitToDashboard('stream_end', undefined, dashboard_questions ? { questions: dashboard_questions, text: final_response_text } : undefined);
|
|
5510
5715
|
|
|
5511
5716
|
conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
5512
5717
|
conversation_doc.ts = Date.now();
|
|
@@ -5514,9 +5719,10 @@ Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}
|
|
|
5514
5719
|
conversation_doc.process_stat = 'full';
|
|
5515
5720
|
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5516
5721
|
|
|
5517
|
-
return await saveAssistantItem(
|
|
5722
|
+
return await saveAssistantItem(final_response_text, {
|
|
5518
5723
|
cpi_methods: cpi_tools_ret.selected_methods,
|
|
5519
5724
|
ai_agent_id: 'dashboard_cpi_agent',
|
|
5725
|
+
...(dashboard_questions ? { questions: dashboard_questions } : {}),
|
|
5520
5726
|
});
|
|
5521
5727
|
} catch (err) {
|
|
5522
5728
|
const error_message = get_error_message(err, 'dashboard request failed');
|
|
@@ -5540,9 +5746,337 @@ const get_ai_agent_tools = async function ({ ai_agent_doc, agent_id, reference_t
|
|
|
5540
5746
|
let tool_resources = {};
|
|
5541
5747
|
let eligible_agent = true;
|
|
5542
5748
|
|
|
5749
|
+
const add_internal_codex_tool = function ({ name, description, system_prompt }) {
|
|
5750
|
+
if (reference_type !== 'ai_agents' && !prompt_suggestion_activated && !chat_suggestion_activated) {
|
|
5751
|
+
eligible_agent = false;
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5755
|
+
tools.push(
|
|
5756
|
+
tool({
|
|
5757
|
+
name: name.substring(0, 60),
|
|
5758
|
+
description,
|
|
5759
|
+
parameters: z.object({
|
|
5760
|
+
question: z.string().describe('The support question to investigate.'),
|
|
5761
|
+
}),
|
|
5762
|
+
async execute(e) {
|
|
5763
|
+
const question = String(e?.question || '').trim();
|
|
5764
|
+
if (!question) {
|
|
5765
|
+
return 'question is required';
|
|
5766
|
+
}
|
|
5767
|
+
|
|
5768
|
+
const dev_host = _conf?.internal_ai_agents?.dev_server_host || 'dev.xuda.ai';
|
|
5769
|
+
const codex_ret = await execute_codex_request(
|
|
5770
|
+
{
|
|
5771
|
+
ip: dev_host,
|
|
5772
|
+
uid,
|
|
5773
|
+
account_profile_info,
|
|
5774
|
+
job_id,
|
|
5775
|
+
stream: false,
|
|
5776
|
+
prompt: `${system_prompt}
|
|
5777
|
+
|
|
5778
|
+
User question:
|
|
5779
|
+
${question}
|
|
5780
|
+
|
|
5781
|
+
Return a concise answer. Include only non-sensitive findings and mention when the requested information is outside the allowed scope.`,
|
|
5782
|
+
},
|
|
5783
|
+
job_id,
|
|
5784
|
+
headers,
|
|
5785
|
+
);
|
|
5786
|
+
|
|
5787
|
+
if (codex_ret.code < 0) {
|
|
5788
|
+
return `Codex readonly lookup failed: ${get_error_message(codex_ret.data, 'unknown error')}`;
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
const events = codex_ret?.data?.events || [];
|
|
5792
|
+
const final_message = [...events].reverse().find((event) => event?.item?.type === 'agent_message' && event.item.text)?.item?.text;
|
|
5793
|
+
return final_message || 'Codex readonly lookup completed without a final answer.';
|
|
5794
|
+
},
|
|
5795
|
+
}),
|
|
5796
|
+
);
|
|
5797
|
+
};
|
|
5798
|
+
|
|
5799
|
+
const add_xuda_public_website_tool = function ({ name, description }) {
|
|
5800
|
+
if (reference_type !== 'ai_agents' && !prompt_suggestion_activated && !chat_suggestion_activated) {
|
|
5801
|
+
eligible_agent = false;
|
|
5802
|
+
return;
|
|
5803
|
+
}
|
|
5804
|
+
|
|
5805
|
+
const website_origin = _conf?.internal_ai_agents?.public_website_url || 'https://xuda.ai';
|
|
5806
|
+
const dump_dir = path.join(os.tmpdir(), 'xuda_public_website_dump', new URL(website_origin).hostname.replace(/[^a-z0-9.-]/gi, '_'));
|
|
5807
|
+
const manifest_path = path.join(dump_dir, 'manifest.json');
|
|
5808
|
+
const ttl_ms = Number(_conf?.internal_ai_agents?.public_website_dump_ttl_ms || 10 * 60 * 1000);
|
|
5809
|
+
const cache_version = 2;
|
|
5810
|
+
const website_base = website_origin.replace(/\/$/, '');
|
|
5811
|
+
|
|
5812
|
+
const decode_html = function (value = '') {
|
|
5813
|
+
return String(value)
|
|
5814
|
+
.replace(/ /gi, ' ')
|
|
5815
|
+
.replace(/&/gi, '&')
|
|
5816
|
+
.replace(/</gi, '<')
|
|
5817
|
+
.replace(/>/gi, '>')
|
|
5818
|
+
.replace(/"/gi, '"')
|
|
5819
|
+
.replace(/'/g, "'");
|
|
5820
|
+
};
|
|
5821
|
+
|
|
5822
|
+
const strip_html = function (html = '') {
|
|
5823
|
+
return decode_html(
|
|
5824
|
+
String(html)
|
|
5825
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
5826
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
5827
|
+
.replace(/<noscript[\s\S]*?<\/noscript>/gi, ' ')
|
|
5828
|
+
.replace(/<svg[\s\S]*?<\/svg>/gi, ' ')
|
|
5829
|
+
.replace(/<[^>]+>/g, ' ')
|
|
5830
|
+
.replace(/\s+/g, ' ')
|
|
5831
|
+
.trim(),
|
|
5832
|
+
);
|
|
5833
|
+
};
|
|
5834
|
+
|
|
5835
|
+
const extract_title = function (html = '') {
|
|
5836
|
+
const match = String(html).match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
5837
|
+
return match ? decode_html(match[1]).replace(/\s+/g, ' ').trim() : '';
|
|
5838
|
+
};
|
|
5839
|
+
|
|
5840
|
+
const get_terms = function (question) {
|
|
5841
|
+
const terms = String(question)
|
|
5842
|
+
.toLowerCase()
|
|
5843
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
5844
|
+
.split(/\s+/)
|
|
5845
|
+
.filter((term) => term.length > 2 && !['the', 'and', 'for', 'about', 'tell', 'with', 'from', 'what', 'who', 'how', 'xuda'].includes(term));
|
|
5846
|
+
if (/\baddress|where|location|located|office|contact\b/i.test(question)) {
|
|
5847
|
+
terms.push('address', 'contact', 'llc', 'boca', 'raton', 'florida');
|
|
5848
|
+
}
|
|
5849
|
+
return [...new Set(terms)];
|
|
5850
|
+
};
|
|
5851
|
+
|
|
5852
|
+
const score_page = function (page, terms, question) {
|
|
5853
|
+
const haystack = `${page.url} ${page.title} ${page.text}`.toLowerCase();
|
|
5854
|
+
let score = 0;
|
|
5855
|
+
for (const term of terms) {
|
|
5856
|
+
if (haystack.includes(term)) score += term.length > 4 ? 3 : 1;
|
|
5857
|
+
}
|
|
5858
|
+
if (/\b(company|about|xuda)\b/i.test(question) && new URL(page.url).pathname === '/') score += 20;
|
|
5859
|
+
if (/\bdocs?|guide|how|cli|dashboard|deploy|agent|app|vps|database|couchdb\b/i.test(question) && page.url.includes('/docs')) score += 8;
|
|
5860
|
+
if (/\bnetwork|ambassador|mentor|city|region\b/i.test(question) && page.url.includes('network')) score += 8;
|
|
5861
|
+
if (/\baddress|where|location|located|office|contact\b/i.test(question) && /\/legal\/privacy-policy/i.test(page.url)) score += 100;
|
|
5862
|
+
if (/\baddress|where|location|located|office|contact\b/i.test(question) && /\/legal\/terms|contact/i.test(page.url)) score += 30;
|
|
5863
|
+
return score;
|
|
5864
|
+
};
|
|
5865
|
+
|
|
5866
|
+
const make_snippet = function (text = '', terms = [], max_length = 3000) {
|
|
5867
|
+
const lower = text.toLowerCase();
|
|
5868
|
+
const anchors = ['address', 'contact us', 'xuda llc', 'boca raton', 'florida', ...terms];
|
|
5869
|
+
let index = -1;
|
|
5870
|
+
for (const anchor of anchors) {
|
|
5871
|
+
index = lower.indexOf(String(anchor).toLowerCase());
|
|
5872
|
+
if (index > -1) break;
|
|
5873
|
+
}
|
|
5874
|
+
if (index < 0) return text.slice(0, max_length);
|
|
5875
|
+
const start = Math.max(0, index - Math.floor(max_length / 3));
|
|
5876
|
+
const end = Math.min(text.length, start + max_length);
|
|
5877
|
+
return `${start > 0 ? '...' : ''}${text.slice(start, end)}${end < text.length ? '...' : ''}`;
|
|
5878
|
+
};
|
|
5879
|
+
|
|
5880
|
+
const extract_public_facts = function (page, question) {
|
|
5881
|
+
const facts = [];
|
|
5882
|
+
if (/\baddress|where|location|located|office|contact\b/i.test(question)) {
|
|
5883
|
+
const address_match = String(page.text || '').match(/Address:\s*([^|]+?)(?=\s+By using|\s+Privacy Policy|\s+Terms|\s*$)/i);
|
|
5884
|
+
if (address_match?.[1]) {
|
|
5885
|
+
facts.push({
|
|
5886
|
+
label: 'Address',
|
|
5887
|
+
value: address_match[1].replace(/\s+/g, ' ').trim(),
|
|
5888
|
+
url: page.url,
|
|
5889
|
+
});
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
return facts;
|
|
5893
|
+
};
|
|
5894
|
+
|
|
5895
|
+
const fetch_text = async function (url, timeout_ms = 8000) {
|
|
5896
|
+
const controller = new AbortController();
|
|
5897
|
+
const timer = setTimeout(() => controller.abort(), timeout_ms);
|
|
5898
|
+
try {
|
|
5899
|
+
const res = await fetch(url, {
|
|
5900
|
+
signal: controller.signal,
|
|
5901
|
+
headers: { 'User-Agent': 'Xuda Ambassador Support Readonly Website Indexer' },
|
|
5902
|
+
});
|
|
5903
|
+
if (!res.ok) throw new Error(`${url} returned ${res.status}`);
|
|
5904
|
+
return await res.text();
|
|
5905
|
+
} finally {
|
|
5906
|
+
clearTimeout(timer);
|
|
5907
|
+
}
|
|
5908
|
+
};
|
|
5909
|
+
|
|
5910
|
+
const load_dump = async function () {
|
|
5911
|
+
try {
|
|
5912
|
+
const manifest = JSON.parse(await fs.promises.readFile(manifest_path, 'utf8'));
|
|
5913
|
+
if (Number(manifest.cache_version || 0) === cache_version && Date.now() - Number(manifest.created_ts || 0) < ttl_ms && Array.isArray(manifest.pages) && manifest.pages.length) {
|
|
5914
|
+
const pages = [];
|
|
5915
|
+
for (const page of manifest.pages) {
|
|
5916
|
+
try {
|
|
5917
|
+
pages.push(JSON.parse(await fs.promises.readFile(path.join(dump_dir, page.file), 'utf8')));
|
|
5918
|
+
} catch (_) {
|
|
5919
|
+
/* ignore corrupt cache item */
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
if (pages.length) return { pages, from_cache: true };
|
|
5923
|
+
}
|
|
5924
|
+
} catch (_) {
|
|
5925
|
+
/* build cache below */
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
await fs.promises.mkdir(dump_dir, { recursive: true });
|
|
5929
|
+
const sitemap_xml = await fetch_text(`${website_base}/sitemap.xml`, 8000);
|
|
5930
|
+
const priority_urls = [
|
|
5931
|
+
`${website_base}/`,
|
|
5932
|
+
`${website_base}/about`,
|
|
5933
|
+
`${website_base}/legal/privacy-policy`,
|
|
5934
|
+
`${website_base}/legal/terms`,
|
|
5935
|
+
`${website_base}/contact`,
|
|
5936
|
+
];
|
|
5937
|
+
const sitemap_urls = [...sitemap_xml.matchAll(/<loc>([\s\S]*?)<\/loc>/gi)]
|
|
5938
|
+
.map((match) => decode_html(match[1]).trim())
|
|
5939
|
+
.filter((url) => {
|
|
5940
|
+
try {
|
|
5941
|
+
const parsed = new URL(url);
|
|
5942
|
+
return parsed.origin === website_base && !/\.(png|jpe?g|gif|webp|svg|pdf|zip)$/i.test(parsed.pathname);
|
|
5943
|
+
} catch (_) {
|
|
5944
|
+
return false;
|
|
5945
|
+
}
|
|
5946
|
+
})
|
|
5947
|
+
.slice(0, 120);
|
|
5948
|
+
const urls = [...new Set([...priority_urls, ...sitemap_urls])];
|
|
5949
|
+
|
|
5950
|
+
const pages = [];
|
|
5951
|
+
for (const url of urls) {
|
|
5952
|
+
try {
|
|
5953
|
+
const html = await fetch_text(url, 8000);
|
|
5954
|
+
const page = {
|
|
5955
|
+
url,
|
|
5956
|
+
title: extract_title(html),
|
|
5957
|
+
text: strip_html(html).slice(0, 80000),
|
|
5958
|
+
fetched_ts: Date.now(),
|
|
5959
|
+
};
|
|
5960
|
+
if (page.text.length < 80) continue;
|
|
5961
|
+
const file = `${crypto.createHash('sha1').update(url).digest('hex')}.json`;
|
|
5962
|
+
await fs.promises.writeFile(path.join(dump_dir, file), JSON.stringify(page, null, 2));
|
|
5963
|
+
pages.push({ ...page, file });
|
|
5964
|
+
} catch (err) {
|
|
5965
|
+
pages.push({ url, title: '', text: `Fetch failed: ${err.message || String(err)}`, fetch_error: true });
|
|
5966
|
+
}
|
|
5967
|
+
}
|
|
5968
|
+
|
|
5969
|
+
await fs.promises.writeFile(
|
|
5970
|
+
manifest_path,
|
|
5971
|
+
JSON.stringify(
|
|
5972
|
+
{
|
|
5973
|
+
source: website_origin,
|
|
5974
|
+
cache_version,
|
|
5975
|
+
created_ts: Date.now(),
|
|
5976
|
+
pages: pages.filter((page) => !page.fetch_error).map((page) => ({ url: page.url, file: page.file, title: page.title })),
|
|
5977
|
+
},
|
|
5978
|
+
null,
|
|
5979
|
+
2,
|
|
5980
|
+
),
|
|
5981
|
+
);
|
|
5982
|
+
return { pages, from_cache: false };
|
|
5983
|
+
};
|
|
5984
|
+
|
|
5985
|
+
tools.push(
|
|
5986
|
+
tool({
|
|
5987
|
+
name: (name || 'Xuda Ambassador Public Website Readonly').substring(0, 60),
|
|
5988
|
+
description: description || 'Readonly lookup against public xuda.ai website pages for ambassador support answers.',
|
|
5989
|
+
parameters: z.object({
|
|
5990
|
+
question: z.string().describe('The support question to answer from public xuda.ai website pages.'),
|
|
5991
|
+
}),
|
|
5992
|
+
async execute(e) {
|
|
5993
|
+
const question = String(e?.question || '').trim();
|
|
5994
|
+
if (!question) {
|
|
5995
|
+
return 'question is required';
|
|
5996
|
+
}
|
|
5997
|
+
|
|
5998
|
+
const run_lookup = async function () {
|
|
5999
|
+
const { pages } = await load_dump();
|
|
6000
|
+
const terms = get_terms(question);
|
|
6001
|
+
const ranked = pages
|
|
6002
|
+
.filter((page) => !page.fetch_error)
|
|
6003
|
+
.map((page) => ({ page, score: score_page(page, terms, question) }))
|
|
6004
|
+
.filter((item) => item.score > 0)
|
|
6005
|
+
.sort((a, b) => b.score - a.score)
|
|
6006
|
+
.slice(0, 5)
|
|
6007
|
+
.map((item) => ({
|
|
6008
|
+
score: item.score,
|
|
6009
|
+
url: item.page.url,
|
|
6010
|
+
title: item.page.title,
|
|
6011
|
+
snippet: make_snippet(item.page.text, terms),
|
|
6012
|
+
}));
|
|
6013
|
+
const facts = pages
|
|
6014
|
+
.filter((page) => !page.fetch_error)
|
|
6015
|
+
.flatMap((page) => extract_public_facts(page, question))
|
|
6016
|
+
.slice(0, 5);
|
|
6017
|
+
|
|
6018
|
+
return {
|
|
6019
|
+
source: website_origin,
|
|
6020
|
+
readonly: true,
|
|
6021
|
+
question,
|
|
6022
|
+
facts,
|
|
6023
|
+
result_count: ranked.length,
|
|
6024
|
+
results: ranked,
|
|
6025
|
+
instruction: ranked.length
|
|
6026
|
+
? 'Answer only from these public xuda.ai website facts and snippets. If facts are present, use them as the primary source. Cite page URLs inline when useful. Do not mention internal cache, dump folders, or implementation details.'
|
|
6027
|
+
: 'No relevant public xuda.ai pages were found. Say that the public xuda.ai website did not contain enough information and ask for a narrower page or topic. Do not mention internal cache, dump folders, or implementation details.',
|
|
6028
|
+
};
|
|
6029
|
+
};
|
|
6030
|
+
|
|
6031
|
+
try {
|
|
6032
|
+
const data = await Promise.race([
|
|
6033
|
+
run_lookup(),
|
|
6034
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('xuda.ai website lookup timed out')), 20000)),
|
|
6035
|
+
]);
|
|
6036
|
+
return JSON.stringify(data, null, 2);
|
|
6037
|
+
} catch (err) {
|
|
6038
|
+
return `xuda.ai website lookup failed: ${err.message || String(err)}`;
|
|
6039
|
+
}
|
|
6040
|
+
},
|
|
6041
|
+
}),
|
|
6042
|
+
);
|
|
6043
|
+
};
|
|
6044
|
+
|
|
5543
6045
|
for (const [key, val] of Object.entries(ai_agent_doc?.agentConfig?.agent_tools || [])) {
|
|
5544
6046
|
const { name = '', description = '' } = val;
|
|
5545
6047
|
switch (val.type) {
|
|
6048
|
+
case 'xuda_network_ambassador_dev_couchdb_readonly': {
|
|
6049
|
+
add_xuda_public_website_tool({
|
|
6050
|
+
name: name || 'Xuda Ambassador Public Website Readonly',
|
|
6051
|
+
description: description || 'Readonly lookup against public xuda.ai website pages for ambassador support answers.',
|
|
6052
|
+
});
|
|
6053
|
+
break;
|
|
6054
|
+
}
|
|
6055
|
+
|
|
6056
|
+
case 'xuda_network_mentor_dev_server_readonly': {
|
|
6057
|
+
add_internal_codex_tool({
|
|
6058
|
+
name: name || 'Xuda Mentor Dev Server Readonly',
|
|
6059
|
+
description: description || 'Readonly Codex lookup on /var/xuda of the dev server for technical mentor answers.',
|
|
6060
|
+
system_prompt: `You are Codex helping a Xuda Network mentor answer technical questions about the Xuda codebase.
|
|
6061
|
+
|
|
6062
|
+
Scope:
|
|
6063
|
+
- You may inspect only the dev server ${_conf?.internal_ai_agents?.dev_server_host || 'dev.xuda.ai'}.
|
|
6064
|
+
- You may use SSH only for readonly inspection.
|
|
6065
|
+
- You may read only under /var/xuda.
|
|
6066
|
+
- Prefer safe inspection commands such as pwd, ls, find, rg, sed -n, head, tail, wc, git status, git diff --stat, git log --oneline, and git show.
|
|
6067
|
+
|
|
6068
|
+
Hard restrictions:
|
|
6069
|
+
- Never access config, config_dev, .env files, secret folders, SSH keys, certificates, credential files, database dumps, backups, logs likely to contain credentials, or any path outside /var/xuda.
|
|
6070
|
+
- Never query CouchDB or any database.
|
|
6071
|
+
- Never use network commands to call internal services.
|
|
6072
|
+
- Never run commands that write, mutate, install, delete, restart, deploy, publish, chmod/chown, kill processes, or change system state.
|
|
6073
|
+
- Never reveal passwords, tokens, API keys, private keys, names of private people, private emails, personal information, customer data, security details, or anything that could harm Xuda, its team, customers, or product security.
|
|
6074
|
+
- If the answer requires forbidden access, say it is outside the readonly mentor scope.
|
|
6075
|
+
- If sensitive data appears accidentally, do not quote it. State that sensitive output was omitted.`,
|
|
6076
|
+
});
|
|
6077
|
+
break;
|
|
6078
|
+
}
|
|
6079
|
+
|
|
5546
6080
|
case 'web_search': {
|
|
5547
6081
|
tools.push(webSearchTool());
|
|
5548
6082
|
break;
|
|
@@ -5831,8 +6365,8 @@ const load_ai_agent_doc = async function (app_id, agent_id) {
|
|
|
5831
6365
|
ai_agent_doc.reference_doc = reference_doc;
|
|
5832
6366
|
} else if (ai_agent_doc.studio_meta.installed_from_app_uid) {
|
|
5833
6367
|
const reference_doc = _.cloneDeep(ai_agent_doc);
|
|
5834
|
-
await get_account_default_project_id(ai_agent_doc.studio_meta.installed_from_app_uid);
|
|
5835
|
-
ai_agent_doc = await db_module.get_app_couch_doc_native(
|
|
6368
|
+
const source_app_id = ai_agent_doc.studio_meta.installed_from_app_id || (await get_account_default_project_id(ai_agent_doc.studio_meta.installed_from_app_uid));
|
|
6369
|
+
ai_agent_doc = await db_module.get_app_couch_doc_native(source_app_id, ai_agent_doc.studio_meta.share_item_id || ai_agent_doc._id);
|
|
5836
6370
|
ai_agent_doc.reference_doc = reference_doc;
|
|
5837
6371
|
}
|
|
5838
6372
|
|
|
@@ -6642,18 +7176,14 @@ const auto_response = async function (uid, profile_id, contact_id, conversation_
|
|
|
6642
7176
|
// const conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
6643
7177
|
|
|
6644
7178
|
let auto_respond_agents = account_profile_doc.auto_respond_agents;
|
|
6645
|
-
if (auto_respond_agents
|
|
6646
|
-
|
|
6647
|
-
if (!Array.isArray(auto_respond_agents)) {
|
|
6648
|
-
auto_respond_agents = [];
|
|
6649
|
-
}
|
|
7179
|
+
if (!Array.isArray(auto_respond_agents)) auto_respond_agents = [];
|
|
6650
7180
|
|
|
6651
7181
|
if (!auto_respond_agents.length) {
|
|
6652
7182
|
const ai_agents_ret = await get_user_ai_agents(uid);
|
|
6653
7183
|
auto_respond_agents = ai_agents_ret.docs.map((doc) => doc._id).filter(Boolean);
|
|
6654
7184
|
}
|
|
6655
7185
|
|
|
6656
|
-
|
|
7186
|
+
const use_default_agent = auto_respond_agents.length === 0;
|
|
6657
7187
|
|
|
6658
7188
|
const runner = new Runner();
|
|
6659
7189
|
const app_obj = await get_app_obj(account_profile_info.app_id);
|
|
@@ -6681,6 +7211,24 @@ const auto_response = async function (uid, profile_id, contact_id, conversation_
|
|
|
6681
7211
|
let agents = [];
|
|
6682
7212
|
let model = _conf.default_ai_model;
|
|
6683
7213
|
|
|
7214
|
+
if (use_default_agent) {
|
|
7215
|
+
agents.push(
|
|
7216
|
+
new Agent({
|
|
7217
|
+
name: 'Auto Reply',
|
|
7218
|
+
instructions: `You are ${account_profile_doc.profile_name || userName}'s automatic ${conversation_type === 'chat' ? 'chat' : 'email'} assistant.
|
|
7219
|
+
Reply to the latest incoming message from ${contact_doc.name || contact_doc.email} in a friendly, helpful, and concise way.
|
|
7220
|
+
Use the conversation history for context.
|
|
7221
|
+
Match the language the contact wrote in.
|
|
7222
|
+
Do not mention that the reply is automated.
|
|
7223
|
+
${conversation_type === 'chat' ? 'Return only the chat message text, ready to send.' : 'Return only the email body, ready to send.'}
|
|
7224
|
+
If a signature is available, append it at the end:
|
|
7225
|
+
${account_profile_doc.profile_signature || ''}`.trim(),
|
|
7226
|
+
model,
|
|
7227
|
+
metadata: { auto_response: true, default: true, ts: Date.now() },
|
|
7228
|
+
}),
|
|
7229
|
+
);
|
|
7230
|
+
}
|
|
7231
|
+
|
|
6684
7232
|
for (const agent_id of auto_respond_agents) {
|
|
6685
7233
|
try {
|
|
6686
7234
|
const ai_agent_doc = await load_ai_agent_doc(account_profile_info.app_id, agent_id);
|
|
@@ -6773,7 +7321,36 @@ Return only the email body.`;
|
|
|
6773
7321
|
const response_text = output?.state?._currentStep?.output?.trim();
|
|
6774
7322
|
if (!response_text) return;
|
|
6775
7323
|
|
|
6776
|
-
|
|
7324
|
+
const is_widget_reply = contact_doc.source === 'widget';
|
|
7325
|
+
|
|
7326
|
+
if (conversation_type === 'chat' && is_widget_reply) {
|
|
7327
|
+
// Widget visitor has no project — write the reply directly on the rep's app only.
|
|
7328
|
+
let conv_item_ref_id;
|
|
7329
|
+
try {
|
|
7330
|
+
conv_item_ref_id = await add_conversation_item(uid, profile_id, conversation_doc._id, response_text, 'chat', contact_id, { direction: 'out' });
|
|
7331
|
+
} catch (err) {}
|
|
7332
|
+
const now = Date.now();
|
|
7333
|
+
const reply_item = {
|
|
7334
|
+
_id: await _common.xuda_get_uuid('chat_conversation_item'),
|
|
7335
|
+
stat: 3,
|
|
7336
|
+
docType: 'chat_conversation_item',
|
|
7337
|
+
uid,
|
|
7338
|
+
conversation_type: 'chat',
|
|
7339
|
+
type: 'chat',
|
|
7340
|
+
date_created_ts: now,
|
|
7341
|
+
ts: now,
|
|
7342
|
+
conversation_id: conversation_doc._id,
|
|
7343
|
+
text: response_text,
|
|
7344
|
+
reference_id: contact_id,
|
|
7345
|
+
conversation_item_reference_id: conv_item_ref_id,
|
|
7346
|
+
direction: 'out',
|
|
7347
|
+
role: 'user',
|
|
7348
|
+
auto_response: true,
|
|
7349
|
+
widget_source: true,
|
|
7350
|
+
rtl: _common.detectRTL(response_text),
|
|
7351
|
+
};
|
|
7352
|
+
await db_module.save_app_couch_doc(account_profile_info.app_id, reply_item);
|
|
7353
|
+
} else if (conversation_type === 'chat') {
|
|
6777
7354
|
await contact_chat_conversation(
|
|
6778
7355
|
{
|
|
6779
7356
|
profile_id,
|
|
@@ -7161,10 +7738,10 @@ const get_plugin_tool = async function (app_id, uid, plugin_name, method, prop_d
|
|
|
7161
7738
|
}
|
|
7162
7739
|
};
|
|
7163
7740
|
|
|
7164
|
-
const create_and_upload_image_to_drive = async function (drive_type, file_path, file_name, prompt, numGenerations, uid, app_id, job_id, headers = {}, is_system, tags = [], account_profile_info) {
|
|
7741
|
+
const create_and_upload_image_to_drive = async function (drive_type, file_path, file_name, prompt, numGenerations, uid, app_id, job_id, headers = {}, is_system, tags = [], account_profile_info, width = 256, height = 256) {
|
|
7165
7742
|
try {
|
|
7166
7743
|
// 1. Generate the images
|
|
7167
|
-
let result = await create_image(uid, prompt, undefined, undefined, numGenerations,
|
|
7744
|
+
let result = await create_image(uid, prompt, undefined, undefined, numGenerations, width, height, { drive_type, file_path, file_name }, account_profile_info);
|
|
7168
7745
|
|
|
7169
7746
|
// Normalize to array
|
|
7170
7747
|
const images = Array.isArray(result) ? result : [result];
|
|
@@ -8005,12 +8582,12 @@ const create_ai_agent_image = async function (req, job_id, headers) {
|
|
|
8005
8582
|
const tempOutputPath = path.join(tempDir, `output_${uniqueId}.png`);
|
|
8006
8583
|
|
|
8007
8584
|
const { app_id, uid, ai_agent_id, tags } = req;
|
|
8008
|
-
const account_profile_info = await get_active_account_profile_info(uid);
|
|
8585
|
+
const account_profile_info = req.account_profile_info || (await get_active_account_profile_info(uid));
|
|
8009
8586
|
const ai_agent_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, ai_agent_id);
|
|
8010
8587
|
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
8011
8588
|
|
|
8012
8589
|
// faceless humanoid for business
|
|
8013
|
-
if (account_doc.account_info.account_type === 'business') {
|
|
8590
|
+
if (account_doc.account_info.account_type === 'business' || ai_agent_doc.studio_meta?.force_faceless_agent_image) {
|
|
8014
8591
|
const prompt = `
|
|
8015
8592
|
Create a futuristic, faceless humanoid profile portrait with a transparent background.
|
|
8016
8593
|
|
|
@@ -8045,7 +8622,7 @@ const create_ai_agent_image = async function (req, job_id, headers) {
|
|
|
8045
8622
|
|
|
8046
8623
|
`;
|
|
8047
8624
|
|
|
8048
|
-
const images_arr = await create_and_upload_image_to_drive('studio', 'Progs Thumbnails', ai_agent_id, prompt, 1, uid, app_id, job_id, headers, false, tags, account_profile_info);
|
|
8625
|
+
const images_arr = await create_and_upload_image_to_drive('studio', 'Progs Thumbnails', ai_agent_id, prompt, 1, uid, app_id, job_id, headers, false, tags, account_profile_info, 1024, 1024);
|
|
8049
8626
|
return { code: 1, data: images_arr[0] };
|
|
8050
8627
|
}
|
|
8051
8628
|
|
|
@@ -13293,8 +13870,8 @@ export const get_chat_suggestions = async function (req) {
|
|
|
13293
13870
|
ai_agent_doc.reference_doc = reference_doc;
|
|
13294
13871
|
} else if (ai_agent_doc.studio_meta.installed_from_app_uid) {
|
|
13295
13872
|
const reference_doc = _.cloneDeep(ai_agent_doc);
|
|
13296
|
-
await get_account_default_project_id(ai_agent_doc.studio_meta.installed_from_app_uid);
|
|
13297
|
-
ai_agent_doc = await db_module.get_app_couch_doc_native(
|
|
13873
|
+
const source_app_id = ai_agent_doc.studio_meta.installed_from_app_id || (await get_account_default_project_id(ai_agent_doc.studio_meta.installed_from_app_uid));
|
|
13874
|
+
ai_agent_doc = await db_module.get_app_couch_doc_native(source_app_id, ai_agent_doc.studio_meta.share_item_id || ai_agent_doc._id);
|
|
13298
13875
|
ai_agent_doc.reference_doc = reference_doc;
|
|
13299
13876
|
}
|
|
13300
13877
|
|
|
@@ -13453,3 +14030,513 @@ export const validate_credits_limit = async function (uid, profile_id) {
|
|
|
13453
14030
|
return err;
|
|
13454
14031
|
}
|
|
13455
14032
|
};
|
|
14033
|
+
|
|
14034
|
+
////////////////////////////// CHAT WIDGET (embed) //////////////////////////////
|
|
14035
|
+
|
|
14036
|
+
const WIDGET_EMAIL_RE = /^[\w\.\-+]+@[a-zA-Z\d\.\-]+\.[a-zA-Z]{2,}$/;
|
|
14037
|
+
const WIDGET_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
14038
|
+
const WIDGET_DEFAULT_GREETING = 'Hi! How can we help?';
|
|
14039
|
+
|
|
14040
|
+
const _widget_split_name = function (name) {
|
|
14041
|
+
const parts = String(name || '')
|
|
14042
|
+
.trim()
|
|
14043
|
+
.split(/\s+/);
|
|
14044
|
+
return { first_name: parts[0] || name || '', last_name: parts.slice(1).join(' ') || '' };
|
|
14045
|
+
};
|
|
14046
|
+
|
|
14047
|
+
const _widget_verify_google_id_token = async function (id_token) {
|
|
14048
|
+
// Use Google's tokeninfo endpoint — validates signature, expiration, and returns the payload.
|
|
14049
|
+
// Cheaper than carrying google-auth-library; one extra HTTPS round-trip per signup is fine.
|
|
14050
|
+
const r = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(id_token)}`);
|
|
14051
|
+
if (!r.ok) throw new Error('google_token_invalid');
|
|
14052
|
+
const payload = await r.json();
|
|
14053
|
+
if (!payload || !payload.email) throw new Error('google_token_no_email');
|
|
14054
|
+
if (payload.email_verified !== 'true' && payload.email_verified !== true) throw new Error('google_email_unverified');
|
|
14055
|
+
const expected_aud = _conf?.gmail?.clientId;
|
|
14056
|
+
if (expected_aud && payload.aud !== expected_aud) throw new Error('google_token_wrong_aud');
|
|
14057
|
+
return payload;
|
|
14058
|
+
};
|
|
14059
|
+
|
|
14060
|
+
const _widget_find_or_create_visitor = async function (email, name, google_sub) {
|
|
14061
|
+
const ret = await db_module.find_couch_query('xuda_accounts', {
|
|
14062
|
+
selector: { 'account_info.email': email },
|
|
14063
|
+
fields: ['_id'],
|
|
14064
|
+
limit: 1,
|
|
14065
|
+
});
|
|
14066
|
+
if (ret.docs && ret.docs.length) {
|
|
14067
|
+
// If we have a google_sub and the existing account lacks one, attach it.
|
|
14068
|
+
if (google_sub) {
|
|
14069
|
+
try {
|
|
14070
|
+
const acct_ret = await db_module.get_couch_doc('xuda_accounts', ret.docs[0]._id);
|
|
14071
|
+
if (acct_ret.code > -1) {
|
|
14072
|
+
const acct = acct_ret.data;
|
|
14073
|
+
if (!acct.google_sub) {
|
|
14074
|
+
acct.google_sub = google_sub;
|
|
14075
|
+
acct.google_linked = true;
|
|
14076
|
+
acct.ts = Date.now();
|
|
14077
|
+
await db_module.save_couch_doc('xuda_accounts', acct);
|
|
14078
|
+
}
|
|
14079
|
+
}
|
|
14080
|
+
} catch (err) {}
|
|
14081
|
+
}
|
|
14082
|
+
return ret.docs[0]._id;
|
|
14083
|
+
}
|
|
14084
|
+
|
|
14085
|
+
const _utils = await import(path.join(process.env.XUDA_HOME, 'common', 'xuda-cpi-utils.mjs'));
|
|
14086
|
+
const secret = _utils.get_id('tmp_pass', 5);
|
|
14087
|
+
const hash = _utils.hash(secret);
|
|
14088
|
+
const { first_name, last_name } = _widget_split_name(name);
|
|
14089
|
+
const doc = {
|
|
14090
|
+
_id: await _common.xuda_get_uuid('account'),
|
|
14091
|
+
stat: 1,
|
|
14092
|
+
docType: 'account',
|
|
14093
|
+
source: 'widget',
|
|
14094
|
+
password: hash,
|
|
14095
|
+
email,
|
|
14096
|
+
account_info: { email, username: _utils.get_id('xu'), first_name, last_name, full_name: name },
|
|
14097
|
+
};
|
|
14098
|
+
if (google_sub) {
|
|
14099
|
+
doc.google_sub = google_sub;
|
|
14100
|
+
doc.google_linked = true;
|
|
14101
|
+
}
|
|
14102
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', doc);
|
|
14103
|
+
if (save_ret.code < 0) throw new Error(save_ret.data || 'visitor_create_failed');
|
|
14104
|
+
return doc._id;
|
|
14105
|
+
};
|
|
14106
|
+
|
|
14107
|
+
const _widget_find_existing_team_req = async function (visitor_uid, canonical_profile_id) {
|
|
14108
|
+
const ret = await db_module.find_couch_query('xuda_team', {
|
|
14109
|
+
selector: {
|
|
14110
|
+
docType: 'team_request',
|
|
14111
|
+
access_type: 'contact_connection',
|
|
14112
|
+
team_req_from_uid: visitor_uid,
|
|
14113
|
+
share_item_id: canonical_profile_id,
|
|
14114
|
+
contact_source: 'widget',
|
|
14115
|
+
},
|
|
14116
|
+
limit: 1,
|
|
14117
|
+
});
|
|
14118
|
+
return ret?.docs?.[0] || null;
|
|
14119
|
+
};
|
|
14120
|
+
|
|
14121
|
+
const _widget_pick_assignee = async function (canonical_owner_uid, canonical_profile_id) {
|
|
14122
|
+
const member_uids = await account_ms.get_account_profile_group_member_uids(canonical_owner_uid, canonical_profile_id);
|
|
14123
|
+
const candidates = [canonical_owner_uid, ...(member_uids || [])].filter(Boolean);
|
|
14124
|
+
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
14125
|
+
};
|
|
14126
|
+
|
|
14127
|
+
const _widget_auto_accept = async function (team_req_doc, assigned_uid, visitor_email, visitor_name) {
|
|
14128
|
+
// Add contact first (creates the contact on the rep's side). The team_req on disk
|
|
14129
|
+
// is still stat=2 at this point, which is fine because add_contact only stores a
|
|
14130
|
+
// reference (team_req_id) on the contact doc and does not read the team_req body.
|
|
14131
|
+
const add_ret = await account_ms.add_contact({
|
|
14132
|
+
uid: assigned_uid,
|
|
14133
|
+
name: visitor_name,
|
|
14134
|
+
email: visitor_email,
|
|
14135
|
+
contact_uid: team_req_doc.team_req_from_uid,
|
|
14136
|
+
source: 'widget',
|
|
14137
|
+
team_req_id: team_req_doc._id,
|
|
14138
|
+
});
|
|
14139
|
+
let contact_id;
|
|
14140
|
+
if (add_ret.code > -1) {
|
|
14141
|
+
contact_id = add_ret.data?.id;
|
|
14142
|
+
} else if (add_ret.contact_id) {
|
|
14143
|
+
contact_id = add_ret.contact_id;
|
|
14144
|
+
}
|
|
14145
|
+
if (!contact_id) throw new Error('add_contact_failed');
|
|
14146
|
+
|
|
14147
|
+
// Refresh _rev and write the final accepted state in one update.
|
|
14148
|
+
const fresh_ret = await db_module.get_couch_doc('xuda_team', team_req_doc._id);
|
|
14149
|
+
if (fresh_ret.code < 0) throw new Error(fresh_ret.data || 'team_req_missing');
|
|
14150
|
+
const fresh = fresh_ret.data;
|
|
14151
|
+
fresh.team_req_stat = 3;
|
|
14152
|
+
fresh.team_req_stat_desc = 'widget auto-accepted';
|
|
14153
|
+
fresh.team_req_status_date = Date.now();
|
|
14154
|
+
fresh.team_req_to_contact_id = contact_id;
|
|
14155
|
+
const save_ret = await db_module.save_couch_doc('xuda_team', fresh);
|
|
14156
|
+
if (save_ret.code < 0) throw new Error(save_ret.data || 'team_req_save_failed');
|
|
14157
|
+
|
|
14158
|
+
team_req_doc.team_req_stat = 3;
|
|
14159
|
+
team_req_doc.team_req_to_contact_id = contact_id;
|
|
14160
|
+
return contact_id;
|
|
14161
|
+
};
|
|
14162
|
+
|
|
14163
|
+
const _widget_ensure_conversation = async function (assigned_uid, canonical_profile_id, contact_id, job_id, headers) {
|
|
14164
|
+
const rep_app_id = await account_ms.get_account_default_project_id(assigned_uid);
|
|
14165
|
+
const conv_ret = await db_module.find_app_couch_query(rep_app_id, {
|
|
14166
|
+
selector: {
|
|
14167
|
+
docType: 'chat_conversation',
|
|
14168
|
+
conversation_type: 'chat',
|
|
14169
|
+
reference_type: 'contacts',
|
|
14170
|
+
reference_id: contact_id,
|
|
14171
|
+
stat: { $lt: 4 },
|
|
14172
|
+
},
|
|
14173
|
+
limit: 1,
|
|
14174
|
+
sort: [{ ts: 'desc' }],
|
|
14175
|
+
});
|
|
14176
|
+
if (conv_ret?.docs?.length) return conv_ret.docs[0]._id;
|
|
14177
|
+
|
|
14178
|
+
const save_ret = await create_conversation(
|
|
14179
|
+
{
|
|
14180
|
+
uid: assigned_uid,
|
|
14181
|
+
profile_id: canonical_profile_id,
|
|
14182
|
+
conversation_type: 'chat',
|
|
14183
|
+
reference_type: 'contacts',
|
|
14184
|
+
reference_id: contact_id,
|
|
14185
|
+
prompt: '',
|
|
14186
|
+
direction: 'in',
|
|
14187
|
+
perform_ai_execution: false,
|
|
14188
|
+
},
|
|
14189
|
+
job_id,
|
|
14190
|
+
headers,
|
|
14191
|
+
);
|
|
14192
|
+
return save_ret?.data?.id;
|
|
14193
|
+
};
|
|
14194
|
+
|
|
14195
|
+
const _widget_load_session = async function (widget_token) {
|
|
14196
|
+
if (!widget_token) return null;
|
|
14197
|
+
const ret = await db_module.get_couch_doc('xuda_sessions', widget_token);
|
|
14198
|
+
if (ret.code < 0) return null;
|
|
14199
|
+
const session = ret.data;
|
|
14200
|
+
if (session.docType !== 'widget_session') return null;
|
|
14201
|
+
if (session.stat !== 2) return null;
|
|
14202
|
+
if (session.ttl_ts && session.ttl_ts < Date.now()) return null;
|
|
14203
|
+
return session;
|
|
14204
|
+
};
|
|
14205
|
+
|
|
14206
|
+
const _widget_refresh_state = async function (session, job_id, headers) {
|
|
14207
|
+
if (session.state === 'open' || session.state === 'declined') return session;
|
|
14208
|
+
const tr_ret = await db_module.get_couch_doc('xuda_team', session.team_req_id);
|
|
14209
|
+
if (tr_ret.code < 0) return session;
|
|
14210
|
+
const tr = tr_ret.data;
|
|
14211
|
+
if (tr.team_req_stat === 3) {
|
|
14212
|
+
session.state = 'open';
|
|
14213
|
+
session.contact_id = tr.team_req_to_contact_id || session.contact_id;
|
|
14214
|
+
if (!session.conversation_id && session.contact_id) {
|
|
14215
|
+
try {
|
|
14216
|
+
session.conversation_id = await _widget_ensure_conversation(session.assigned_uid, session.profile_id, session.contact_id, job_id, headers);
|
|
14217
|
+
} catch (err) {}
|
|
14218
|
+
}
|
|
14219
|
+
session.ts = Date.now();
|
|
14220
|
+
await db_module.save_couch_doc('xuda_sessions', session);
|
|
14221
|
+
} else if (tr.team_req_stat === 4) {
|
|
14222
|
+
session.state = 'declined';
|
|
14223
|
+
session.ts = Date.now();
|
|
14224
|
+
await db_module.save_couch_doc('xuda_sessions', session);
|
|
14225
|
+
}
|
|
14226
|
+
return session;
|
|
14227
|
+
};
|
|
14228
|
+
|
|
14229
|
+
export const get_widget_embed_snippet = async function (req, job_id, headers) {
|
|
14230
|
+
try {
|
|
14231
|
+
const { uid, profile_id, config } = req;
|
|
14232
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
14233
|
+
const canonical_owner_uid = account_profile_info.uid;
|
|
14234
|
+
const canonical_profile_id = account_profile_info.account_profile_id;
|
|
14235
|
+
|
|
14236
|
+
// Persist widget config + enable only when caller is canonical owner
|
|
14237
|
+
if (canonical_owner_uid === uid) {
|
|
14238
|
+
const doc = account_profile_info.account_profile_obj || { _id: canonical_profile_id };
|
|
14239
|
+
doc.widget_enabled = true;
|
|
14240
|
+
doc.widget_config = { ...(doc.widget_config || {}), ...(config || {}) };
|
|
14241
|
+
doc.ts = Date.now();
|
|
14242
|
+
await db_module.save_app_couch_doc_native(account_profile_info.app_id, doc);
|
|
14243
|
+
}
|
|
14244
|
+
|
|
14245
|
+
const widget_profile_id = `${canonical_owner_uid}.${canonical_profile_id}`;
|
|
14246
|
+
const domain = _conf.domain || 'xuda.io';
|
|
14247
|
+
const iframe_url = `https://${domain}/chat-widget/${widget_profile_id}`;
|
|
14248
|
+
const snippet = `<script>(function(){var d=document,s=d.createElement('script');s.src='https://${domain}/dist/runtime/js/chat-widget-loader.js';s.async=1;s.setAttribute('data-profile','${widget_profile_id}');d.head.appendChild(s);})();</script>`;
|
|
14249
|
+
|
|
14250
|
+
return { code: 1, data: { widget_profile_id, snippet, iframe_url } };
|
|
14251
|
+
} catch (err) {
|
|
14252
|
+
return { code: -1, data: err.message || String(err) };
|
|
14253
|
+
}
|
|
14254
|
+
};
|
|
14255
|
+
|
|
14256
|
+
const _widget_signup_core = async function (req, job_id, headers, opts) {
|
|
14257
|
+
const { profile_id, email, name, widget_origin, google_sub, signup_method } = req;
|
|
14258
|
+
const [owner_uid_part, pid_part] = String(profile_id || '').split('.');
|
|
14259
|
+
if (!owner_uid_part || !pid_part) return { code: -404, data: 'invalid_profile' };
|
|
14260
|
+
|
|
14261
|
+
const account_profile_info = await get_active_account_profile_info(owner_uid_part, pid_part);
|
|
14262
|
+
const canonical_owner_uid = account_profile_info.uid;
|
|
14263
|
+
const canonical_profile_id = account_profile_info.account_profile_id;
|
|
14264
|
+
|
|
14265
|
+
const ap_doc = account_profile_info.account_profile_obj || {};
|
|
14266
|
+
if (ap_doc.widget_enabled === false) return { code: -404, data: 'widget_disabled' };
|
|
14267
|
+
|
|
14268
|
+
const visitor_uid = await _widget_find_or_create_visitor(email, name, google_sub);
|
|
14269
|
+
return await _widget_signup_finalize(visitor_uid, email, name, widget_origin, signup_method, account_profile_info, canonical_owner_uid, canonical_profile_id, ap_doc, job_id, headers);
|
|
14270
|
+
};
|
|
14271
|
+
|
|
14272
|
+
export const widget_signup = async function (req, job_id, headers) {
|
|
14273
|
+
try {
|
|
14274
|
+
let { profile_id, email, name, widget_origin } = req;
|
|
14275
|
+
if (typeof email !== 'string' || typeof name !== 'string') return { code: -1, data: 'invalid_input' };
|
|
14276
|
+
email = email.toLowerCase().trim();
|
|
14277
|
+
name = name.trim();
|
|
14278
|
+
if (!email || !name) return { code: -1, data: 'invalid_input' };
|
|
14279
|
+
if (!WIDGET_EMAIL_RE.test(email)) return { code: -1, data: 'invalid_email' };
|
|
14280
|
+
|
|
14281
|
+
return await _widget_signup_core({ profile_id, email, name, widget_origin, signup_method: 'email' }, job_id, headers);
|
|
14282
|
+
} catch (err) {
|
|
14283
|
+
return { code: -1, data: err.message || String(err) };
|
|
14284
|
+
}
|
|
14285
|
+
};
|
|
14286
|
+
|
|
14287
|
+
export const widget_signup_google = async function (req, job_id, headers) {
|
|
14288
|
+
try {
|
|
14289
|
+
const { profile_id, id_token, widget_origin } = req;
|
|
14290
|
+
if (!id_token || typeof id_token !== 'string') return { code: -1, data: 'missing_id_token' };
|
|
14291
|
+
|
|
14292
|
+
let payload;
|
|
14293
|
+
try {
|
|
14294
|
+
payload = await _widget_verify_google_id_token(id_token);
|
|
14295
|
+
} catch (err) {
|
|
14296
|
+
return { code: -401, data: err.message || 'google_token_invalid' };
|
|
14297
|
+
}
|
|
14298
|
+
|
|
14299
|
+
const email = String(payload.email || '')
|
|
14300
|
+
.toLowerCase()
|
|
14301
|
+
.trim();
|
|
14302
|
+
const name = String(payload.name || payload.email?.split('@')[0] || 'Friend').trim();
|
|
14303
|
+
if (!email) return { code: -1, data: 'no_email_in_token' };
|
|
14304
|
+
|
|
14305
|
+
return await _widget_signup_core({ profile_id, email, name, widget_origin, google_sub: payload.sub, signup_method: 'google' }, job_id, headers);
|
|
14306
|
+
} catch (err) {
|
|
14307
|
+
return { code: -1, data: err.message || String(err) };
|
|
14308
|
+
}
|
|
14309
|
+
};
|
|
14310
|
+
|
|
14311
|
+
const _widget_signup_finalize = async function (visitor_uid, email, name, widget_origin, signup_method, account_profile_info, canonical_owner_uid, canonical_profile_id, ap_doc, job_id, headers) {
|
|
14312
|
+
try {
|
|
14313
|
+
let team_req_doc = await _widget_find_existing_team_req(visitor_uid, canonical_profile_id);
|
|
14314
|
+
let assigned_uid;
|
|
14315
|
+
|
|
14316
|
+
if (team_req_doc) {
|
|
14317
|
+
assigned_uid = team_req_doc.team_req_to_uid;
|
|
14318
|
+
} else {
|
|
14319
|
+
assigned_uid = await _widget_pick_assignee(canonical_owner_uid, canonical_profile_id);
|
|
14320
|
+
if (!assigned_uid) return { code: -1, data: 'no_assignee' };
|
|
14321
|
+
|
|
14322
|
+
const assigned_acct_ret = await db_module.get_couch_doc('xuda_accounts', assigned_uid);
|
|
14323
|
+
const assigned_email = assigned_acct_ret?.data?.account_info?.email || assigned_acct_ret?.data?.email || '';
|
|
14324
|
+
|
|
14325
|
+
const { first_name, last_name } = _widget_split_name(name);
|
|
14326
|
+
const team_req_id = await _common.xuda_get_uuid('team_req');
|
|
14327
|
+
team_req_doc = {
|
|
14328
|
+
_id: team_req_id,
|
|
14329
|
+
team_req_id,
|
|
14330
|
+
team_req_date: Date.now(),
|
|
14331
|
+
team_req_status_date: null,
|
|
14332
|
+
team_req_to_email: assigned_email,
|
|
14333
|
+
team_req_to_uid: assigned_uid,
|
|
14334
|
+
team_req_stat: 2,
|
|
14335
|
+
team_req_stat_desc: 'widget signup',
|
|
14336
|
+
team_req_from_uid: visitor_uid,
|
|
14337
|
+
docType: 'team_request',
|
|
14338
|
+
team_req_app_id: null,
|
|
14339
|
+
access_type: 'contact_connection',
|
|
14340
|
+
uid: visitor_uid,
|
|
14341
|
+
contact_source: 'widget',
|
|
14342
|
+
share_item_id: canonical_profile_id,
|
|
14343
|
+
widget_origin: widget_origin || '',
|
|
14344
|
+
sender_account_info: { email, first_name, last_name, full_name: name },
|
|
14345
|
+
};
|
|
14346
|
+
const tr_save = await db_module.save_couch_doc('xuda_team', team_req_doc);
|
|
14347
|
+
if (tr_save.code < 0) return { code: -1, data: tr_save.data || 'team_request_create_failed' };
|
|
14348
|
+
|
|
14349
|
+
// fire and forget — sending the invitation email shouldn't block the visitor's response
|
|
14350
|
+
team_ms.send_access_invitation({ team_req_id: team_req_doc._id }).catch(() => {});
|
|
14351
|
+
}
|
|
14352
|
+
|
|
14353
|
+
let state = team_req_doc.team_req_stat === 3 ? 'open' : team_req_doc.team_req_stat === 4 ? 'declined' : 'pending';
|
|
14354
|
+
let contact_id = team_req_doc.team_req_to_contact_id;
|
|
14355
|
+
let conversation_id;
|
|
14356
|
+
|
|
14357
|
+
const auto_accept = !!ap_doc?.auto_respond;
|
|
14358
|
+
if (state === 'pending' && auto_accept) {
|
|
14359
|
+
try {
|
|
14360
|
+
contact_id = await _widget_auto_accept(team_req_doc, assigned_uid, email, name);
|
|
14361
|
+
state = 'open';
|
|
14362
|
+
} catch (err) {
|
|
14363
|
+
state = 'pending';
|
|
14364
|
+
}
|
|
14365
|
+
}
|
|
14366
|
+
|
|
14367
|
+
if (state === 'open' && contact_id) {
|
|
14368
|
+
try {
|
|
14369
|
+
conversation_id = await _widget_ensure_conversation(assigned_uid, canonical_profile_id, contact_id, job_id, headers);
|
|
14370
|
+
} catch (err) {}
|
|
14371
|
+
}
|
|
14372
|
+
|
|
14373
|
+
const widget_token = `wds_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
14374
|
+
const now = Date.now();
|
|
14375
|
+
const session_doc = {
|
|
14376
|
+
_id: widget_token,
|
|
14377
|
+
docType: 'widget_session',
|
|
14378
|
+
stat: 2,
|
|
14379
|
+
state,
|
|
14380
|
+
visitor_uid,
|
|
14381
|
+
team_req_id: team_req_doc._id,
|
|
14382
|
+
assigned_uid,
|
|
14383
|
+
owner_uid: canonical_owner_uid,
|
|
14384
|
+
profile_id: canonical_profile_id,
|
|
14385
|
+
contact_id,
|
|
14386
|
+
conversation_id,
|
|
14387
|
+
widget_origin: widget_origin || '',
|
|
14388
|
+
date_created_ts: now,
|
|
14389
|
+
ts: now,
|
|
14390
|
+
ttl_ts: now + WIDGET_SESSION_TTL_MS,
|
|
14391
|
+
};
|
|
14392
|
+
await db_module.save_couch_doc('xuda_sessions', session_doc);
|
|
14393
|
+
|
|
14394
|
+
const assigned_info_ret = await account_ms.get_account_name({ uid_query: assigned_uid });
|
|
14395
|
+
const assigned_name = assigned_info_ret?.data?.full_name || assigned_info_ret?.data?.first_name || '';
|
|
14396
|
+
const assigned_avatar = assigned_info_ret?.data?.profile_picture || assigned_info_ret?.data?.profile_avatar || '';
|
|
14397
|
+
const greeting = ap_doc?.widget_config?.greeting || WIDGET_DEFAULT_GREETING;
|
|
14398
|
+
|
|
14399
|
+
return {
|
|
14400
|
+
code: 1,
|
|
14401
|
+
data: {
|
|
14402
|
+
widget_token,
|
|
14403
|
+
state,
|
|
14404
|
+
assigned_name,
|
|
14405
|
+
assigned_avatar,
|
|
14406
|
+
greeting,
|
|
14407
|
+
conversation_id,
|
|
14408
|
+
signup_method: signup_method || 'email',
|
|
14409
|
+
account_active: signup_method === 'google',
|
|
14410
|
+
signin_url: signup_method === 'google' ? `https://${_conf.domain || 'xuda.ai'}/dashboard/login` : undefined,
|
|
14411
|
+
},
|
|
14412
|
+
};
|
|
14413
|
+
} catch (err) {
|
|
14414
|
+
return { code: -1, data: err.message || String(err) };
|
|
14415
|
+
}
|
|
14416
|
+
};
|
|
14417
|
+
|
|
14418
|
+
export const widget_send_message = async function (req, job_id, headers) {
|
|
14419
|
+
try {
|
|
14420
|
+
const { widget_token, prompt = '', attachments = [] } = req;
|
|
14421
|
+
const text = typeof prompt === 'string' ? prompt : '';
|
|
14422
|
+
if (!text.trim() && (!Array.isArray(attachments) || !attachments.length)) {
|
|
14423
|
+
return { code: -1, data: 'invalid_prompt' };
|
|
14424
|
+
}
|
|
14425
|
+
|
|
14426
|
+
// Validate + sanitize image attachments. Inline base64 images only, max 5, ~2.5MB each.
|
|
14427
|
+
const safe_attachments = [];
|
|
14428
|
+
for (const att of (Array.isArray(attachments) ? attachments : []).slice(0, 5)) {
|
|
14429
|
+
if (!att || typeof att.data_uri !== 'string') continue;
|
|
14430
|
+
if (!/^data:image\/(png|jpeg|jpg|gif|webp);base64,/i.test(att.data_uri)) continue;
|
|
14431
|
+
if (att.data_uri.length > 3_500_000) continue;
|
|
14432
|
+
safe_attachments.push({
|
|
14433
|
+
data_uri: att.data_uri,
|
|
14434
|
+
filename: String(att.filename || 'image').slice(0, 200),
|
|
14435
|
+
size: Number(att.size) || 0,
|
|
14436
|
+
type: String(att.type || 'image/png').slice(0, 60),
|
|
14437
|
+
});
|
|
14438
|
+
}
|
|
14439
|
+
|
|
14440
|
+
let session = await _widget_load_session(widget_token);
|
|
14441
|
+
if (!session) return { code: -401, data: 'invalid_token' };
|
|
14442
|
+
|
|
14443
|
+
session = await _widget_refresh_state(session, job_id, headers);
|
|
14444
|
+
if (session.state !== 'open') return { code: -409, data: 'pending_approval' };
|
|
14445
|
+
|
|
14446
|
+
const { assigned_uid, profile_id, conversation_id, contact_id, widget_origin } = session;
|
|
14447
|
+
if (!conversation_id || !contact_id) return { code: -409, data: 'not_ready' };
|
|
14448
|
+
|
|
14449
|
+
const rep_app_id = await account_ms.get_account_default_project_id(assigned_uid);
|
|
14450
|
+
|
|
14451
|
+
// Compose the prompt text to include attachment notice (visible to the agent + history)
|
|
14452
|
+
let prompt_for_history = text.trim();
|
|
14453
|
+
if (safe_attachments.length) {
|
|
14454
|
+
const att_note = `[Attached ${safe_attachments.length} image${safe_attachments.length > 1 ? 's' : ''}: ${safe_attachments.map((a) => a.filename).join(', ')}]`;
|
|
14455
|
+
prompt_for_history = prompt_for_history ? `${prompt_for_history}\n\n${att_note}` : att_note;
|
|
14456
|
+
}
|
|
14457
|
+
|
|
14458
|
+
let conversation_item_reference_id;
|
|
14459
|
+
try {
|
|
14460
|
+
conversation_item_reference_id = await add_conversation_item(assigned_uid, profile_id, conversation_id, prompt_for_history, 'chat', contact_id, { direction: 'in' });
|
|
14461
|
+
report_ai_status('conversations');
|
|
14462
|
+
} catch (err) {
|
|
14463
|
+
report_ai_status('conversations', err);
|
|
14464
|
+
}
|
|
14465
|
+
|
|
14466
|
+
const now = Date.now();
|
|
14467
|
+
const item_doc = {
|
|
14468
|
+
_id: await _common.xuda_get_uuid('chat_conversation_item'),
|
|
14469
|
+
stat: 3,
|
|
14470
|
+
docType: 'chat_conversation_item',
|
|
14471
|
+
uid: assigned_uid,
|
|
14472
|
+
conversation_type: 'chat',
|
|
14473
|
+
type: 'chat',
|
|
14474
|
+
date_created_ts: now,
|
|
14475
|
+
ts: now,
|
|
14476
|
+
conversation_id,
|
|
14477
|
+
text: text.trim(),
|
|
14478
|
+
reference_id: contact_id,
|
|
14479
|
+
conversation_item_reference_id,
|
|
14480
|
+
direction: 'in',
|
|
14481
|
+
role: 'user',
|
|
14482
|
+
widget_source: true,
|
|
14483
|
+
widget_origin: widget_origin || '',
|
|
14484
|
+
rtl: _common.detectRTL(text),
|
|
14485
|
+
attachments: safe_attachments,
|
|
14486
|
+
};
|
|
14487
|
+
const save_ret = await db_module.save_app_couch_doc(rep_app_id, item_doc);
|
|
14488
|
+
|
|
14489
|
+
session.ts = now;
|
|
14490
|
+
await db_module.save_couch_doc('xuda_sessions', session);
|
|
14491
|
+
|
|
14492
|
+
try {
|
|
14493
|
+
auto_response(assigned_uid, profile_id, contact_id, 'chat');
|
|
14494
|
+
} catch (err) {}
|
|
14495
|
+
|
|
14496
|
+
return { code: 15, data: save_ret };
|
|
14497
|
+
} catch (err) {
|
|
14498
|
+
return { code: -1, data: err.message || String(err) };
|
|
14499
|
+
}
|
|
14500
|
+
};
|
|
14501
|
+
|
|
14502
|
+
export const widget_get_messages = async function (req, job_id, headers) {
|
|
14503
|
+
try {
|
|
14504
|
+
const { widget_token, since_ts = 0, limit = 100 } = req;
|
|
14505
|
+
let session = await _widget_load_session(widget_token);
|
|
14506
|
+
if (!session) return { code: -401, data: 'invalid_token' };
|
|
14507
|
+
|
|
14508
|
+
session = await _widget_refresh_state(session, job_id, headers);
|
|
14509
|
+
|
|
14510
|
+
const server_ts = Date.now();
|
|
14511
|
+
if (session.state !== 'open' || !session.conversation_id) {
|
|
14512
|
+
return { code: 15, data: { state: session.state, items: [], conversation_id: session.conversation_id || null, server_ts } };
|
|
14513
|
+
}
|
|
14514
|
+
|
|
14515
|
+
const rep_app_id = await account_ms.get_account_default_project_id(session.assigned_uid);
|
|
14516
|
+
const ret = await db_module.find_app_couch_query(rep_app_id, {
|
|
14517
|
+
selector: {
|
|
14518
|
+
docType: 'chat_conversation_item',
|
|
14519
|
+
conversation_id: session.conversation_id,
|
|
14520
|
+
stat: 3,
|
|
14521
|
+
date_created_ts: { $gt: Number(since_ts) || 0 },
|
|
14522
|
+
},
|
|
14523
|
+
sort: [{ date_created_ts: 'asc' }],
|
|
14524
|
+
limit: Math.min(Number(limit) || 100, 500),
|
|
14525
|
+
});
|
|
14526
|
+
|
|
14527
|
+
const items = (ret?.docs || []).map((d) => ({
|
|
14528
|
+
_id: d._id,
|
|
14529
|
+
text: d.text,
|
|
14530
|
+
direction: d.direction,
|
|
14531
|
+
role: d.role,
|
|
14532
|
+
date_created_ts: d.date_created_ts,
|
|
14533
|
+
conversation_type: d.conversation_type,
|
|
14534
|
+
attachments: d.attachments,
|
|
14535
|
+
rtl: d.rtl,
|
|
14536
|
+
}));
|
|
14537
|
+
|
|
14538
|
+
return { code: 15, data: { state: 'open', items, conversation_id: session.conversation_id, server_ts } };
|
|
14539
|
+
} catch (err) {
|
|
14540
|
+
return { code: -1, data: err.message || String(err) };
|
|
14541
|
+
}
|
|
14542
|
+
};
|