@xuda.io/ai_module 1.1.5610 → 1.1.5611
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 +773 -18
- 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 npm_publish_msa = await import(`${module_path}/npm_publish_module/index_msa.mjs`);
|
|
235
236
|
|
|
236
237
|
var open_ai_status = {};
|
|
237
238
|
const report_ai_status = function (model, err) {
|
|
@@ -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
|
+
npm_publish_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,6 +2888,38 @@ 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 conversation_id = await _common.xuda_get_uuid('chat_conversation');
|
|
2898
|
+
const now = Date.now();
|
|
2899
|
+
const doc = {
|
|
2900
|
+
_id: conversation_id,
|
|
2901
|
+
docType: 'chat_conversation',
|
|
2902
|
+
title: 'CLI session',
|
|
2903
|
+
date_created_ts: now,
|
|
2904
|
+
ts: now,
|
|
2905
|
+
stat: 1,
|
|
2906
|
+
uid,
|
|
2907
|
+
messages: [],
|
|
2908
|
+
reference_type: 'ai_agents',
|
|
2909
|
+
reference_id: agent_id,
|
|
2910
|
+
conversation_type: 'ai_chat',
|
|
2911
|
+
initiator_uid: uid,
|
|
2912
|
+
account_profiles: [account_profile_info._id],
|
|
2913
|
+
source: 'cli',
|
|
2914
|
+
};
|
|
2915
|
+
const save_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, doc);
|
|
2916
|
+
if (save_ret.code < 0) return save_ret;
|
|
2917
|
+
return { code: 1, data: { conversation_id, profile_id: account_profile_info._id, app_id: account_profile_info.app_id } };
|
|
2918
|
+
} catch (err) {
|
|
2919
|
+
return { code: -1, data: err.message };
|
|
2920
|
+
}
|
|
2921
|
+
};
|
|
2922
|
+
|
|
2886
2923
|
export const update_ai_agent = async function (req, job_id, headers) {
|
|
2887
2924
|
let { agent_id, agentConfig, uid } = req;
|
|
2888
2925
|
|
|
@@ -5113,8 +5150,99 @@ ${conversation_history || `User (studio): ${prompt}`}
|
|
|
5113
5150
|
}
|
|
5114
5151
|
};
|
|
5115
5152
|
|
|
5153
|
+
// Clarifying-questions protocol shared by the vibe/codex path AND the dashboard
|
|
5154
|
+
// CPI agent path. Both emit plain prose by default, so to get a structured
|
|
5155
|
+
// "ask the user" payload (Claude-Code-style options stepper) we instruct them
|
|
5156
|
+
// to emit a JSON block at the end of the response when they would otherwise
|
|
5157
|
+
// ask follow-up questions in prose. The block is parsed off, prose is shipped
|
|
5158
|
+
// as the normal text, and the structured questions ride alongside on the
|
|
5159
|
+
// stream_end event + the persisted chat_conversation_item.
|
|
5160
|
+
const CLARIFYING_QUESTIONS_INSTRUCTION = `
|
|
5161
|
+
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:
|
|
5162
|
+
|
|
5163
|
+
<xuda-questions>
|
|
5164
|
+
[
|
|
5165
|
+
{
|
|
5166
|
+
"question": "<full question text>",
|
|
5167
|
+
"options": [
|
|
5168
|
+
{ "label": "<short option label>", "description": "<one-sentence explanation of what this option means>" },
|
|
5169
|
+
{ "label": "<short option label>", "description": "<one-sentence explanation of what this option means>" },
|
|
5170
|
+
{ "label": "<short option label>", "description": "<one-sentence explanation of what this option means>" }
|
|
5171
|
+
]
|
|
5172
|
+
}
|
|
5173
|
+
]
|
|
5174
|
+
</xuda-questions>
|
|
5175
|
+
|
|
5176
|
+
Rules:
|
|
5177
|
+
- 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.
|
|
5178
|
+
- You may include multiple questions in the array; the UI will step through them one at a time.
|
|
5179
|
+
- 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.
|
|
5180
|
+
- If you do not need clarification, do not emit the block. Only emit it when answering is genuinely blocked on missing information.
|
|
5181
|
+
`.trim();
|
|
5182
|
+
|
|
5183
|
+
const extract_xuda_questions = function (text) {
|
|
5184
|
+
if (typeof text !== 'string') return { prose: text, questions: null };
|
|
5185
|
+
const match = text.match(/<xuda-questions>\s*([\s\S]+?)\s*<\/xuda-questions>\s*$/);
|
|
5186
|
+
if (!match) return { prose: text, questions: null };
|
|
5187
|
+
let parsed;
|
|
5188
|
+
try {
|
|
5189
|
+
parsed = JSON.parse(match[1]);
|
|
5190
|
+
} catch (e) {
|
|
5191
|
+
return { prose: text, questions: null };
|
|
5192
|
+
}
|
|
5193
|
+
if (!Array.isArray(parsed) || !parsed.length) return { prose: text, questions: null };
|
|
5194
|
+
const validated = parsed
|
|
5195
|
+
.map((q) => {
|
|
5196
|
+
if (!q || typeof q.question !== 'string') return null;
|
|
5197
|
+
const options = Array.isArray(q.options)
|
|
5198
|
+
? q.options
|
|
5199
|
+
.map((opt) => (opt && typeof opt.label === 'string' ? { label: opt.label.trim(), description: typeof opt.description === 'string' ? opt.description.trim() : '' } : null))
|
|
5200
|
+
.filter(Boolean)
|
|
5201
|
+
.slice(0, 3)
|
|
5202
|
+
: [];
|
|
5203
|
+
if (options.length < 2) return null;
|
|
5204
|
+
return { question: q.question.trim(), options };
|
|
5205
|
+
})
|
|
5206
|
+
.filter(Boolean);
|
|
5207
|
+
if (!validated.length) return { prose: text, questions: null };
|
|
5208
|
+
return {
|
|
5209
|
+
prose: text.slice(0, match.index).trim(),
|
|
5210
|
+
questions: validated,
|
|
5211
|
+
};
|
|
5212
|
+
};
|
|
5213
|
+
|
|
5214
|
+
// Resolve the structured context object passed by newer clients (ProjectAiPanel
|
|
5215
|
+
// sends { project_id, tab, view, vibe_mode }) or fall back to the legacy string
|
|
5216
|
+
// shape that older callsites still send. Returns an object — string-shaped legacy
|
|
5217
|
+
// inputs are wrapped as { tab: <string> } so downstream readers have one shape.
|
|
5218
|
+
const get_dashboard_context_obj = function (req, conversation_doc) {
|
|
5219
|
+
const candidate = req.context || req.metadata?.context || conversation_doc.context || conversation_doc.metadata?.context;
|
|
5220
|
+
if (candidate && typeof candidate === 'object') return candidate;
|
|
5221
|
+
const legacy_str = (candidate || req.dashboard_context || conversation_doc.dashboard_context || '').toString().trim().toLowerCase();
|
|
5222
|
+
return legacy_str ? { tab: legacy_str } : {};
|
|
5223
|
+
};
|
|
5224
|
+
|
|
5225
|
+
// Primary routing signal: replaces the legacy `dashboard_context === 'console'`
|
|
5226
|
+
// check. Newer clients set `context.vibe_mode = true` when the user is in vibe
|
|
5227
|
+
// mode (full-screen VPS preview) — that's the canonical "send this through the
|
|
5228
|
+
// codex/console runner" flag. dashboard_context is being deprecated as a
|
|
5229
|
+
// routing concept; it remains only as a hint string (context.tab) used by the
|
|
5230
|
+
// CPI tools agent for keyword-based tool selection in the non-vibe branch.
|
|
5231
|
+
const is_vibe_mode_request = function (req, conversation_doc) {
|
|
5232
|
+
const ctx = get_dashboard_context_obj(req, conversation_doc);
|
|
5233
|
+
if (ctx.vibe_mode === true) return true;
|
|
5234
|
+
// Backwards compat: explicit legacy `context: 'console'` still triggers the
|
|
5235
|
+
// codex path. Remove once no callsites pass the string form.
|
|
5236
|
+
if (ctx.tab === 'console') return true;
|
|
5237
|
+
return false;
|
|
5238
|
+
};
|
|
5239
|
+
|
|
5116
5240
|
const get_dashboard_chat_context = function (req, conversation_doc) {
|
|
5117
|
-
|
|
5241
|
+
const ctx = get_dashboard_context_obj(req, conversation_doc);
|
|
5242
|
+
// The legacy "dashboard_context" string was an ad-hoc tab identifier; the new
|
|
5243
|
+
// object's `tab` field is the same concept. Returning '' here lets the CPI
|
|
5244
|
+
// tools agent still do prompt-only keyword matching.
|
|
5245
|
+
return (ctx.tab || '').toString().trim().toLowerCase();
|
|
5118
5246
|
};
|
|
5119
5247
|
|
|
5120
5248
|
const get_dashboard_cpi_keywords = function (context, prompt = '') {
|
|
@@ -5277,6 +5405,7 @@ const dashboard_chat = async function (req, job_id, headers) {
|
|
|
5277
5405
|
|
|
5278
5406
|
const conversation_id = conversation_doc._id;
|
|
5279
5407
|
const dashboard_context = get_dashboard_chat_context(req, conversation_doc);
|
|
5408
|
+
const vibe_mode = is_vibe_mode_request(req, conversation_doc);
|
|
5280
5409
|
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
5410
|
const prompt_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
|
|
5282
5411
|
const response_conversation_item_id = await _common.xuda_get_uuid('chat_conversation_item');
|
|
@@ -5304,7 +5433,19 @@ const dashboard_chat = async function (req, job_id, headers) {
|
|
|
5304
5433
|
? {
|
|
5305
5434
|
stream_id: response_conversation_item_id,
|
|
5306
5435
|
final_seq: stream_delta_seq,
|
|
5307
|
-
text:
|
|
5436
|
+
// params.text override exists for the clarifying-questions flow:
|
|
5437
|
+
// when the agent embedded an <xuda-questions> block in its
|
|
5438
|
+
// streamed output, the live stream_delta events already
|
|
5439
|
+
// shipped the block to the client. The caller passes the
|
|
5440
|
+
// cleaned prose here so handleStreamEnd can reconcile the
|
|
5441
|
+
// visible bubble against the block-free version (uses the
|
|
5442
|
+
// existing missed-deltas reconciliation path on the client).
|
|
5443
|
+
text: typeof params?.text === 'string' ? params.text : stream_delta_text,
|
|
5444
|
+
// Structured clarifying-questions payload — when the agent
|
|
5445
|
+
// emits an <xuda-questions> block, we parse it, strip it
|
|
5446
|
+
// from the prose, and ship it here so the client can render
|
|
5447
|
+
// the stepper artifact alongside the message.
|
|
5448
|
+
...(params?.questions ? { questions: params.questions } : {}),
|
|
5308
5449
|
}
|
|
5309
5450
|
: {}),
|
|
5310
5451
|
prompt_conversation_item_id,
|
|
@@ -5418,7 +5559,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
5418
5559
|
|
|
5419
5560
|
try {
|
|
5420
5561
|
emitToDashboard('stream_start');
|
|
5421
|
-
emitToDashboard('stream_phase',
|
|
5562
|
+
emitToDashboard('stream_phase', vibe_mode ? 'Starting vibe request' : 'Starting dashboard request');
|
|
5422
5563
|
|
|
5423
5564
|
await saveUserItem();
|
|
5424
5565
|
|
|
@@ -5432,19 +5573,68 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
5432
5573
|
conversation_doc.stat = 2;
|
|
5433
5574
|
conversation_doc.request_started_at = request_started_at;
|
|
5434
5575
|
conversation_doc.job_id = job_id;
|
|
5435
|
-
|
|
5576
|
+
// Persist the structured context (object shape) so multi-turn requests
|
|
5577
|
+
// inherit vibe_mode even when the client doesn't resend it. The legacy
|
|
5578
|
+
// string-form `context = dashboard_context` is dropped — vibe_mode is now
|
|
5579
|
+
// the routing signal and `tab` lives inside the object.
|
|
5580
|
+
conversation_doc.context = { ...get_dashboard_context_obj(req, conversation_doc), vibe_mode };
|
|
5436
5581
|
conversation_doc.target_app_id = target_app_id;
|
|
5437
5582
|
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5438
5583
|
|
|
5439
|
-
if (
|
|
5440
|
-
|
|
5584
|
+
if (vibe_mode) {
|
|
5585
|
+
// Resolve the project's VPS IP — codex SSHes into that host to run
|
|
5586
|
+
// commands. Without this, execute_codex_request was pulling `ip` from
|
|
5587
|
+
// `req.ip` (the user's browser IP), which never matches a real VPS
|
|
5588
|
+
// and either fails the SSH or hangs codex against an unreachable host.
|
|
5589
|
+
let vibe_app_obj = null;
|
|
5590
|
+
try {
|
|
5591
|
+
vibe_app_obj = await get_app_obj(target_app_id);
|
|
5592
|
+
} catch (e) {
|
|
5593
|
+
// get_app_obj throws on missing doc — treat as "no project found"
|
|
5594
|
+
// and fall through to the missing-VPS error path below.
|
|
5595
|
+
}
|
|
5596
|
+
const vps_ip = vibe_app_obj?.app_hosting_server?.ip;
|
|
5597
|
+
const project_name = vibe_app_obj?.menuName || vibe_app_obj?.app_name || vibe_app_obj?.name || '';
|
|
5598
|
+
|
|
5599
|
+
if (!vps_ip) {
|
|
5600
|
+
// No deployed VPS for this project — surface a clear, actionable
|
|
5601
|
+
// message instead of letting codex fail silently or SSH to the user.
|
|
5602
|
+
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.`;
|
|
5603
|
+
emitToDashboard('response_start');
|
|
5604
|
+
streamText(missing_vps_msg);
|
|
5605
|
+
emitToDashboard('stream_end');
|
|
5606
|
+
conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
5607
|
+
conversation_doc.ts = Date.now();
|
|
5608
|
+
conversation_doc.stat = 3;
|
|
5609
|
+
conversation_doc.process_stat = 'partial';
|
|
5610
|
+
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5611
|
+
return await saveAssistantItem(missing_vps_msg, { vibe_missing_vps: true });
|
|
5612
|
+
}
|
|
5613
|
+
|
|
5614
|
+
// System block: questions protocol + project/VPS context so codex's
|
|
5615
|
+
// response doesn't drift to a generic answer. Codex sees one prompt
|
|
5616
|
+
// string; the [System]/[User request] framing keeps the boundary
|
|
5617
|
+
// visible to it.
|
|
5618
|
+
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}.`;
|
|
5619
|
+
const codex_prompt = `[System]\n${project_context_block}\n\n${CLARIFYING_QUESTIONS_INSTRUCTION}\n\n[User request]\n${prompt}`;
|
|
5620
|
+
|
|
5621
|
+
// Override ip explicitly so the `...req` spread can't smuggle in
|
|
5622
|
+
// req.ip (browser IP). conversation_id / conversation_doc / prompt /
|
|
5623
|
+
// attachments / stream stay after the spread for the same reason.
|
|
5624
|
+
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
5625
|
const codex_events = codex_ret?.data?.events || [];
|
|
5442
5626
|
const last_message = [...codex_events].reverse().find((event) => event?.item?.type === 'agent_message' && event.item.text)?.item?.text;
|
|
5443
|
-
const
|
|
5627
|
+
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.';
|
|
5628
|
+
|
|
5629
|
+
// Pull the structured questions block (if any) off the tail of the
|
|
5630
|
+
// response. response_text is just the prose; questions ride alongside
|
|
5631
|
+
// on the stream_end event + the persisted assistant item.
|
|
5632
|
+
const { prose, questions } = extract_xuda_questions(raw_response_text);
|
|
5633
|
+
const response_text = prose || raw_response_text;
|
|
5444
5634
|
|
|
5445
5635
|
emitToDashboard('response_start');
|
|
5446
5636
|
streamText(response_text);
|
|
5447
|
-
emitToDashboard('stream_end');
|
|
5637
|
+
emitToDashboard('stream_end', undefined, questions ? { questions } : undefined);
|
|
5448
5638
|
|
|
5449
5639
|
conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
5450
5640
|
conversation_doc.ts = Date.now();
|
|
@@ -5452,7 +5642,7 @@ ${conversation_history || `User (dashboard): ${prompt}`}
|
|
|
5452
5642
|
conversation_doc.process_stat = codex_ret.code < 0 ? 'partial' : 'full';
|
|
5453
5643
|
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5454
5644
|
|
|
5455
|
-
return await saveAssistantItem(response_text, { codex_result: codex_ret });
|
|
5645
|
+
return await saveAssistantItem(response_text, { codex_result: codex_ret, ...(questions ? { questions } : {}) });
|
|
5456
5646
|
}
|
|
5457
5647
|
|
|
5458
5648
|
const dashboard_prompt = await getDashboardContextPrompt();
|
|
@@ -5476,6 +5666,8 @@ If the screenshot or prompt implies the tab/context, infer the relevant dashboar
|
|
|
5476
5666
|
For data/database requests, use the database/table CPI methods and keep app_id/reference_id anchored to "${target_app_id}".
|
|
5477
5667
|
Summarize the action and important CPI result fields clearly. Do not invent successful changes; report tool errors as errors.
|
|
5478
5668
|
Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}
|
|
5669
|
+
|
|
5670
|
+
${CLARIFYING_QUESTIONS_INSTRUCTION}
|
|
5479
5671
|
`.trim(),
|
|
5480
5672
|
model: ai_model,
|
|
5481
5673
|
tools: cpi_tools_ret.tools,
|
|
@@ -5506,7 +5698,16 @@ Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}
|
|
|
5506
5698
|
}
|
|
5507
5699
|
|
|
5508
5700
|
response_text = response_text || output?.state?._currentStep?.output || output?.finalOutput || output?.output || 'Dashboard request completed.';
|
|
5509
|
-
|
|
5701
|
+
|
|
5702
|
+
// Pull off the structured clarifying-questions block (if the agent emitted
|
|
5703
|
+
// one) before persistence + stream_end. The block was streamed live to the
|
|
5704
|
+
// client during stream_delta, so the client's handleStreamEnd will use the
|
|
5705
|
+
// `text` field on stream_end to overwrite the visible bubble with the
|
|
5706
|
+
// cleaned prose (see AiChat.vue handleStreamEnd reconciliation).
|
|
5707
|
+
const { prose: dashboard_prose, questions: dashboard_questions } = extract_xuda_questions(response_text);
|
|
5708
|
+
const final_response_text = dashboard_prose || response_text;
|
|
5709
|
+
|
|
5710
|
+
emitToDashboard('stream_end', undefined, dashboard_questions ? { questions: dashboard_questions, text: final_response_text } : undefined);
|
|
5510
5711
|
|
|
5511
5712
|
conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
5512
5713
|
conversation_doc.ts = Date.now();
|
|
@@ -5514,9 +5715,10 @@ Available CPI methods: ${cpi_tools_ret.selected_methods.join(', ')}
|
|
|
5514
5715
|
conversation_doc.process_stat = 'full';
|
|
5515
5716
|
await db_module.save_app_couch_doc_native(account_profile_info.app_id, conversation_doc);
|
|
5516
5717
|
|
|
5517
|
-
return await saveAssistantItem(
|
|
5718
|
+
return await saveAssistantItem(final_response_text, {
|
|
5518
5719
|
cpi_methods: cpi_tools_ret.selected_methods,
|
|
5519
5720
|
ai_agent_id: 'dashboard_cpi_agent',
|
|
5721
|
+
...(dashboard_questions ? { questions: dashboard_questions } : {}),
|
|
5520
5722
|
});
|
|
5521
5723
|
} catch (err) {
|
|
5522
5724
|
const error_message = get_error_message(err, 'dashboard request failed');
|
|
@@ -6642,18 +6844,14 @@ const auto_response = async function (uid, profile_id, contact_id, conversation_
|
|
|
6642
6844
|
// const conversation_doc = await db_module.get_app_couch_doc_native(account_profile_info.app_id, conversation_id);
|
|
6643
6845
|
|
|
6644
6846
|
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
|
-
}
|
|
6847
|
+
if (!Array.isArray(auto_respond_agents)) auto_respond_agents = [];
|
|
6650
6848
|
|
|
6651
6849
|
if (!auto_respond_agents.length) {
|
|
6652
6850
|
const ai_agents_ret = await get_user_ai_agents(uid);
|
|
6653
6851
|
auto_respond_agents = ai_agents_ret.docs.map((doc) => doc._id).filter(Boolean);
|
|
6654
6852
|
}
|
|
6655
6853
|
|
|
6656
|
-
|
|
6854
|
+
const use_default_agent = auto_respond_agents.length === 0;
|
|
6657
6855
|
|
|
6658
6856
|
const runner = new Runner();
|
|
6659
6857
|
const app_obj = await get_app_obj(account_profile_info.app_id);
|
|
@@ -6681,6 +6879,24 @@ const auto_response = async function (uid, profile_id, contact_id, conversation_
|
|
|
6681
6879
|
let agents = [];
|
|
6682
6880
|
let model = _conf.default_ai_model;
|
|
6683
6881
|
|
|
6882
|
+
if (use_default_agent) {
|
|
6883
|
+
agents.push(
|
|
6884
|
+
new Agent({
|
|
6885
|
+
name: 'Auto Reply',
|
|
6886
|
+
instructions: `You are ${account_profile_doc.profile_name || userName}'s automatic ${conversation_type === 'chat' ? 'chat' : 'email'} assistant.
|
|
6887
|
+
Reply to the latest incoming message from ${contact_doc.name || contact_doc.email} in a friendly, helpful, and concise way.
|
|
6888
|
+
Use the conversation history for context.
|
|
6889
|
+
Match the language the contact wrote in.
|
|
6890
|
+
Do not mention that the reply is automated.
|
|
6891
|
+
${conversation_type === 'chat' ? 'Return only the chat message text, ready to send.' : 'Return only the email body, ready to send.'}
|
|
6892
|
+
If a signature is available, append it at the end:
|
|
6893
|
+
${account_profile_doc.profile_signature || ''}`.trim(),
|
|
6894
|
+
model,
|
|
6895
|
+
metadata: { auto_response: true, default: true, ts: Date.now() },
|
|
6896
|
+
}),
|
|
6897
|
+
);
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6684
6900
|
for (const agent_id of auto_respond_agents) {
|
|
6685
6901
|
try {
|
|
6686
6902
|
const ai_agent_doc = await load_ai_agent_doc(account_profile_info.app_id, agent_id);
|
|
@@ -6773,7 +6989,36 @@ Return only the email body.`;
|
|
|
6773
6989
|
const response_text = output?.state?._currentStep?.output?.trim();
|
|
6774
6990
|
if (!response_text) return;
|
|
6775
6991
|
|
|
6776
|
-
|
|
6992
|
+
const is_widget_reply = contact_doc.source === 'widget';
|
|
6993
|
+
|
|
6994
|
+
if (conversation_type === 'chat' && is_widget_reply) {
|
|
6995
|
+
// Widget visitor has no project — write the reply directly on the rep's app only.
|
|
6996
|
+
let conv_item_ref_id;
|
|
6997
|
+
try {
|
|
6998
|
+
conv_item_ref_id = await add_conversation_item(uid, profile_id, conversation_doc._id, response_text, 'chat', contact_id, { direction: 'out' });
|
|
6999
|
+
} catch (err) {}
|
|
7000
|
+
const now = Date.now();
|
|
7001
|
+
const reply_item = {
|
|
7002
|
+
_id: await _common.xuda_get_uuid('chat_conversation_item'),
|
|
7003
|
+
stat: 3,
|
|
7004
|
+
docType: 'chat_conversation_item',
|
|
7005
|
+
uid,
|
|
7006
|
+
conversation_type: 'chat',
|
|
7007
|
+
type: 'chat',
|
|
7008
|
+
date_created_ts: now,
|
|
7009
|
+
ts: now,
|
|
7010
|
+
conversation_id: conversation_doc._id,
|
|
7011
|
+
text: response_text,
|
|
7012
|
+
reference_id: contact_id,
|
|
7013
|
+
conversation_item_reference_id: conv_item_ref_id,
|
|
7014
|
+
direction: 'out',
|
|
7015
|
+
role: 'user',
|
|
7016
|
+
auto_response: true,
|
|
7017
|
+
widget_source: true,
|
|
7018
|
+
rtl: _common.detectRTL(response_text),
|
|
7019
|
+
};
|
|
7020
|
+
await db_module.save_app_couch_doc(account_profile_info.app_id, reply_item);
|
|
7021
|
+
} else if (conversation_type === 'chat') {
|
|
6777
7022
|
await contact_chat_conversation(
|
|
6778
7023
|
{
|
|
6779
7024
|
profile_id,
|
|
@@ -13453,3 +13698,513 @@ export const validate_credits_limit = async function (uid, profile_id) {
|
|
|
13453
13698
|
return err;
|
|
13454
13699
|
}
|
|
13455
13700
|
};
|
|
13701
|
+
|
|
13702
|
+
////////////////////////////// CHAT WIDGET (embed) //////////////////////////////
|
|
13703
|
+
|
|
13704
|
+
const WIDGET_EMAIL_RE = /^[\w\.\-+]+@[a-zA-Z\d\.\-]+\.[a-zA-Z]{2,}$/;
|
|
13705
|
+
const WIDGET_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13706
|
+
const WIDGET_DEFAULT_GREETING = 'Hi! How can we help?';
|
|
13707
|
+
|
|
13708
|
+
const _widget_split_name = function (name) {
|
|
13709
|
+
const parts = String(name || '')
|
|
13710
|
+
.trim()
|
|
13711
|
+
.split(/\s+/);
|
|
13712
|
+
return { first_name: parts[0] || name || '', last_name: parts.slice(1).join(' ') || '' };
|
|
13713
|
+
};
|
|
13714
|
+
|
|
13715
|
+
const _widget_verify_google_id_token = async function (id_token) {
|
|
13716
|
+
// Use Google's tokeninfo endpoint — validates signature, expiration, and returns the payload.
|
|
13717
|
+
// Cheaper than carrying google-auth-library; one extra HTTPS round-trip per signup is fine.
|
|
13718
|
+
const r = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(id_token)}`);
|
|
13719
|
+
if (!r.ok) throw new Error('google_token_invalid');
|
|
13720
|
+
const payload = await r.json();
|
|
13721
|
+
if (!payload || !payload.email) throw new Error('google_token_no_email');
|
|
13722
|
+
if (payload.email_verified !== 'true' && payload.email_verified !== true) throw new Error('google_email_unverified');
|
|
13723
|
+
const expected_aud = _conf?.gmail?.clientId;
|
|
13724
|
+
if (expected_aud && payload.aud !== expected_aud) throw new Error('google_token_wrong_aud');
|
|
13725
|
+
return payload;
|
|
13726
|
+
};
|
|
13727
|
+
|
|
13728
|
+
const _widget_find_or_create_visitor = async function (email, name, google_sub) {
|
|
13729
|
+
const ret = await db_module.find_couch_query('xuda_accounts', {
|
|
13730
|
+
selector: { 'account_info.email': email },
|
|
13731
|
+
fields: ['_id'],
|
|
13732
|
+
limit: 1,
|
|
13733
|
+
});
|
|
13734
|
+
if (ret.docs && ret.docs.length) {
|
|
13735
|
+
// If we have a google_sub and the existing account lacks one, attach it.
|
|
13736
|
+
if (google_sub) {
|
|
13737
|
+
try {
|
|
13738
|
+
const acct_ret = await db_module.get_couch_doc('xuda_accounts', ret.docs[0]._id);
|
|
13739
|
+
if (acct_ret.code > -1) {
|
|
13740
|
+
const acct = acct_ret.data;
|
|
13741
|
+
if (!acct.google_sub) {
|
|
13742
|
+
acct.google_sub = google_sub;
|
|
13743
|
+
acct.google_linked = true;
|
|
13744
|
+
acct.ts = Date.now();
|
|
13745
|
+
await db_module.save_couch_doc('xuda_accounts', acct);
|
|
13746
|
+
}
|
|
13747
|
+
}
|
|
13748
|
+
} catch (err) {}
|
|
13749
|
+
}
|
|
13750
|
+
return ret.docs[0]._id;
|
|
13751
|
+
}
|
|
13752
|
+
|
|
13753
|
+
const _utils = await import(path.join(process.env.XUDA_HOME, 'common', 'xuda-cpi-utils.mjs'));
|
|
13754
|
+
const secret = _utils.get_id('tmp_pass', 5);
|
|
13755
|
+
const hash = _utils.hash(secret);
|
|
13756
|
+
const { first_name, last_name } = _widget_split_name(name);
|
|
13757
|
+
const doc = {
|
|
13758
|
+
_id: await _common.xuda_get_uuid('account'),
|
|
13759
|
+
stat: 1,
|
|
13760
|
+
docType: 'account',
|
|
13761
|
+
source: 'widget',
|
|
13762
|
+
password: hash,
|
|
13763
|
+
email,
|
|
13764
|
+
account_info: { email, username: _utils.get_id('xu'), first_name, last_name, full_name: name },
|
|
13765
|
+
};
|
|
13766
|
+
if (google_sub) {
|
|
13767
|
+
doc.google_sub = google_sub;
|
|
13768
|
+
doc.google_linked = true;
|
|
13769
|
+
}
|
|
13770
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', doc);
|
|
13771
|
+
if (save_ret.code < 0) throw new Error(save_ret.data || 'visitor_create_failed');
|
|
13772
|
+
return doc._id;
|
|
13773
|
+
};
|
|
13774
|
+
|
|
13775
|
+
const _widget_find_existing_team_req = async function (visitor_uid, canonical_profile_id) {
|
|
13776
|
+
const ret = await db_module.find_couch_query('xuda_team', {
|
|
13777
|
+
selector: {
|
|
13778
|
+
docType: 'team_request',
|
|
13779
|
+
access_type: 'contact_connection',
|
|
13780
|
+
team_req_from_uid: visitor_uid,
|
|
13781
|
+
share_item_id: canonical_profile_id,
|
|
13782
|
+
contact_source: 'widget',
|
|
13783
|
+
},
|
|
13784
|
+
limit: 1,
|
|
13785
|
+
});
|
|
13786
|
+
return ret?.docs?.[0] || null;
|
|
13787
|
+
};
|
|
13788
|
+
|
|
13789
|
+
const _widget_pick_assignee = async function (canonical_owner_uid, canonical_profile_id) {
|
|
13790
|
+
const member_uids = await account_ms.get_account_profile_group_member_uids(canonical_owner_uid, canonical_profile_id);
|
|
13791
|
+
const candidates = [canonical_owner_uid, ...(member_uids || [])].filter(Boolean);
|
|
13792
|
+
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
13793
|
+
};
|
|
13794
|
+
|
|
13795
|
+
const _widget_auto_accept = async function (team_req_doc, assigned_uid, visitor_email, visitor_name) {
|
|
13796
|
+
// Add contact first (creates the contact on the rep's side). The team_req on disk
|
|
13797
|
+
// is still stat=2 at this point, which is fine because add_contact only stores a
|
|
13798
|
+
// reference (team_req_id) on the contact doc and does not read the team_req body.
|
|
13799
|
+
const add_ret = await account_ms.add_contact({
|
|
13800
|
+
uid: assigned_uid,
|
|
13801
|
+
name: visitor_name,
|
|
13802
|
+
email: visitor_email,
|
|
13803
|
+
contact_uid: team_req_doc.team_req_from_uid,
|
|
13804
|
+
source: 'widget',
|
|
13805
|
+
team_req_id: team_req_doc._id,
|
|
13806
|
+
});
|
|
13807
|
+
let contact_id;
|
|
13808
|
+
if (add_ret.code > -1) {
|
|
13809
|
+
contact_id = add_ret.data?.id;
|
|
13810
|
+
} else if (add_ret.contact_id) {
|
|
13811
|
+
contact_id = add_ret.contact_id;
|
|
13812
|
+
}
|
|
13813
|
+
if (!contact_id) throw new Error('add_contact_failed');
|
|
13814
|
+
|
|
13815
|
+
// Refresh _rev and write the final accepted state in one update.
|
|
13816
|
+
const fresh_ret = await db_module.get_couch_doc('xuda_team', team_req_doc._id);
|
|
13817
|
+
if (fresh_ret.code < 0) throw new Error(fresh_ret.data || 'team_req_missing');
|
|
13818
|
+
const fresh = fresh_ret.data;
|
|
13819
|
+
fresh.team_req_stat = 3;
|
|
13820
|
+
fresh.team_req_stat_desc = 'widget auto-accepted';
|
|
13821
|
+
fresh.team_req_status_date = Date.now();
|
|
13822
|
+
fresh.team_req_to_contact_id = contact_id;
|
|
13823
|
+
const save_ret = await db_module.save_couch_doc('xuda_team', fresh);
|
|
13824
|
+
if (save_ret.code < 0) throw new Error(save_ret.data || 'team_req_save_failed');
|
|
13825
|
+
|
|
13826
|
+
team_req_doc.team_req_stat = 3;
|
|
13827
|
+
team_req_doc.team_req_to_contact_id = contact_id;
|
|
13828
|
+
return contact_id;
|
|
13829
|
+
};
|
|
13830
|
+
|
|
13831
|
+
const _widget_ensure_conversation = async function (assigned_uid, canonical_profile_id, contact_id, job_id, headers) {
|
|
13832
|
+
const rep_app_id = await account_ms.get_account_default_project_id(assigned_uid);
|
|
13833
|
+
const conv_ret = await db_module.find_app_couch_query(rep_app_id, {
|
|
13834
|
+
selector: {
|
|
13835
|
+
docType: 'chat_conversation',
|
|
13836
|
+
conversation_type: 'chat',
|
|
13837
|
+
reference_type: 'contacts',
|
|
13838
|
+
reference_id: contact_id,
|
|
13839
|
+
stat: { $lt: 4 },
|
|
13840
|
+
},
|
|
13841
|
+
limit: 1,
|
|
13842
|
+
sort: [{ ts: 'desc' }],
|
|
13843
|
+
});
|
|
13844
|
+
if (conv_ret?.docs?.length) return conv_ret.docs[0]._id;
|
|
13845
|
+
|
|
13846
|
+
const save_ret = await create_conversation(
|
|
13847
|
+
{
|
|
13848
|
+
uid: assigned_uid,
|
|
13849
|
+
profile_id: canonical_profile_id,
|
|
13850
|
+
conversation_type: 'chat',
|
|
13851
|
+
reference_type: 'contacts',
|
|
13852
|
+
reference_id: contact_id,
|
|
13853
|
+
prompt: '',
|
|
13854
|
+
direction: 'in',
|
|
13855
|
+
perform_ai_execution: false,
|
|
13856
|
+
},
|
|
13857
|
+
job_id,
|
|
13858
|
+
headers,
|
|
13859
|
+
);
|
|
13860
|
+
return save_ret?.data?.id;
|
|
13861
|
+
};
|
|
13862
|
+
|
|
13863
|
+
const _widget_load_session = async function (widget_token) {
|
|
13864
|
+
if (!widget_token) return null;
|
|
13865
|
+
const ret = await db_module.get_couch_doc('xuda_sessions', widget_token);
|
|
13866
|
+
if (ret.code < 0) return null;
|
|
13867
|
+
const session = ret.data;
|
|
13868
|
+
if (session.docType !== 'widget_session') return null;
|
|
13869
|
+
if (session.stat !== 2) return null;
|
|
13870
|
+
if (session.ttl_ts && session.ttl_ts < Date.now()) return null;
|
|
13871
|
+
return session;
|
|
13872
|
+
};
|
|
13873
|
+
|
|
13874
|
+
const _widget_refresh_state = async function (session, job_id, headers) {
|
|
13875
|
+
if (session.state === 'open' || session.state === 'declined') return session;
|
|
13876
|
+
const tr_ret = await db_module.get_couch_doc('xuda_team', session.team_req_id);
|
|
13877
|
+
if (tr_ret.code < 0) return session;
|
|
13878
|
+
const tr = tr_ret.data;
|
|
13879
|
+
if (tr.team_req_stat === 3) {
|
|
13880
|
+
session.state = 'open';
|
|
13881
|
+
session.contact_id = tr.team_req_to_contact_id || session.contact_id;
|
|
13882
|
+
if (!session.conversation_id && session.contact_id) {
|
|
13883
|
+
try {
|
|
13884
|
+
session.conversation_id = await _widget_ensure_conversation(session.assigned_uid, session.profile_id, session.contact_id, job_id, headers);
|
|
13885
|
+
} catch (err) {}
|
|
13886
|
+
}
|
|
13887
|
+
session.ts = Date.now();
|
|
13888
|
+
await db_module.save_couch_doc('xuda_sessions', session);
|
|
13889
|
+
} else if (tr.team_req_stat === 4) {
|
|
13890
|
+
session.state = 'declined';
|
|
13891
|
+
session.ts = Date.now();
|
|
13892
|
+
await db_module.save_couch_doc('xuda_sessions', session);
|
|
13893
|
+
}
|
|
13894
|
+
return session;
|
|
13895
|
+
};
|
|
13896
|
+
|
|
13897
|
+
export const get_widget_embed_snippet = async function (req, job_id, headers) {
|
|
13898
|
+
try {
|
|
13899
|
+
const { uid, profile_id, config } = req;
|
|
13900
|
+
const account_profile_info = await get_active_account_profile_info(uid, profile_id);
|
|
13901
|
+
const canonical_owner_uid = account_profile_info.uid;
|
|
13902
|
+
const canonical_profile_id = account_profile_info.account_profile_id;
|
|
13903
|
+
|
|
13904
|
+
// Persist widget config + enable only when caller is canonical owner
|
|
13905
|
+
if (canonical_owner_uid === uid) {
|
|
13906
|
+
const doc = account_profile_info.account_profile_obj || { _id: canonical_profile_id };
|
|
13907
|
+
doc.widget_enabled = true;
|
|
13908
|
+
doc.widget_config = { ...(doc.widget_config || {}), ...(config || {}) };
|
|
13909
|
+
doc.ts = Date.now();
|
|
13910
|
+
await db_module.save_app_couch_doc_native(account_profile_info.app_id, doc);
|
|
13911
|
+
}
|
|
13912
|
+
|
|
13913
|
+
const widget_profile_id = `${canonical_owner_uid}.${canonical_profile_id}`;
|
|
13914
|
+
const domain = _conf.domain || 'xuda.io';
|
|
13915
|
+
const iframe_url = `https://${domain}/chat-widget/${widget_profile_id}`;
|
|
13916
|
+
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>`;
|
|
13917
|
+
|
|
13918
|
+
return { code: 1, data: { widget_profile_id, snippet, iframe_url } };
|
|
13919
|
+
} catch (err) {
|
|
13920
|
+
return { code: -1, data: err.message || String(err) };
|
|
13921
|
+
}
|
|
13922
|
+
};
|
|
13923
|
+
|
|
13924
|
+
const _widget_signup_core = async function (req, job_id, headers, opts) {
|
|
13925
|
+
const { profile_id, email, name, widget_origin, google_sub, signup_method } = req;
|
|
13926
|
+
const [owner_uid_part, pid_part] = String(profile_id || '').split('.');
|
|
13927
|
+
if (!owner_uid_part || !pid_part) return { code: -404, data: 'invalid_profile' };
|
|
13928
|
+
|
|
13929
|
+
const account_profile_info = await get_active_account_profile_info(owner_uid_part, pid_part);
|
|
13930
|
+
const canonical_owner_uid = account_profile_info.uid;
|
|
13931
|
+
const canonical_profile_id = account_profile_info.account_profile_id;
|
|
13932
|
+
|
|
13933
|
+
const ap_doc = account_profile_info.account_profile_obj || {};
|
|
13934
|
+
if (ap_doc.widget_enabled === false) return { code: -404, data: 'widget_disabled' };
|
|
13935
|
+
|
|
13936
|
+
const visitor_uid = await _widget_find_or_create_visitor(email, name, google_sub);
|
|
13937
|
+
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);
|
|
13938
|
+
};
|
|
13939
|
+
|
|
13940
|
+
export const widget_signup = async function (req, job_id, headers) {
|
|
13941
|
+
try {
|
|
13942
|
+
let { profile_id, email, name, widget_origin } = req;
|
|
13943
|
+
if (typeof email !== 'string' || typeof name !== 'string') return { code: -1, data: 'invalid_input' };
|
|
13944
|
+
email = email.toLowerCase().trim();
|
|
13945
|
+
name = name.trim();
|
|
13946
|
+
if (!email || !name) return { code: -1, data: 'invalid_input' };
|
|
13947
|
+
if (!WIDGET_EMAIL_RE.test(email)) return { code: -1, data: 'invalid_email' };
|
|
13948
|
+
|
|
13949
|
+
return await _widget_signup_core({ profile_id, email, name, widget_origin, signup_method: 'email' }, job_id, headers);
|
|
13950
|
+
} catch (err) {
|
|
13951
|
+
return { code: -1, data: err.message || String(err) };
|
|
13952
|
+
}
|
|
13953
|
+
};
|
|
13954
|
+
|
|
13955
|
+
export const widget_signup_google = async function (req, job_id, headers) {
|
|
13956
|
+
try {
|
|
13957
|
+
const { profile_id, id_token, widget_origin } = req;
|
|
13958
|
+
if (!id_token || typeof id_token !== 'string') return { code: -1, data: 'missing_id_token' };
|
|
13959
|
+
|
|
13960
|
+
let payload;
|
|
13961
|
+
try {
|
|
13962
|
+
payload = await _widget_verify_google_id_token(id_token);
|
|
13963
|
+
} catch (err) {
|
|
13964
|
+
return { code: -401, data: err.message || 'google_token_invalid' };
|
|
13965
|
+
}
|
|
13966
|
+
|
|
13967
|
+
const email = String(payload.email || '')
|
|
13968
|
+
.toLowerCase()
|
|
13969
|
+
.trim();
|
|
13970
|
+
const name = String(payload.name || payload.email?.split('@')[0] || 'Friend').trim();
|
|
13971
|
+
if (!email) return { code: -1, data: 'no_email_in_token' };
|
|
13972
|
+
|
|
13973
|
+
return await _widget_signup_core({ profile_id, email, name, widget_origin, google_sub: payload.sub, signup_method: 'google' }, job_id, headers);
|
|
13974
|
+
} catch (err) {
|
|
13975
|
+
return { code: -1, data: err.message || String(err) };
|
|
13976
|
+
}
|
|
13977
|
+
};
|
|
13978
|
+
|
|
13979
|
+
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) {
|
|
13980
|
+
try {
|
|
13981
|
+
let team_req_doc = await _widget_find_existing_team_req(visitor_uid, canonical_profile_id);
|
|
13982
|
+
let assigned_uid;
|
|
13983
|
+
|
|
13984
|
+
if (team_req_doc) {
|
|
13985
|
+
assigned_uid = team_req_doc.team_req_to_uid;
|
|
13986
|
+
} else {
|
|
13987
|
+
assigned_uid = await _widget_pick_assignee(canonical_owner_uid, canonical_profile_id);
|
|
13988
|
+
if (!assigned_uid) return { code: -1, data: 'no_assignee' };
|
|
13989
|
+
|
|
13990
|
+
const assigned_acct_ret = await db_module.get_couch_doc('xuda_accounts', assigned_uid);
|
|
13991
|
+
const assigned_email = assigned_acct_ret?.data?.account_info?.email || assigned_acct_ret?.data?.email || '';
|
|
13992
|
+
|
|
13993
|
+
const { first_name, last_name } = _widget_split_name(name);
|
|
13994
|
+
const team_req_id = await _common.xuda_get_uuid('team_req');
|
|
13995
|
+
team_req_doc = {
|
|
13996
|
+
_id: team_req_id,
|
|
13997
|
+
team_req_id,
|
|
13998
|
+
team_req_date: Date.now(),
|
|
13999
|
+
team_req_status_date: null,
|
|
14000
|
+
team_req_to_email: assigned_email,
|
|
14001
|
+
team_req_to_uid: assigned_uid,
|
|
14002
|
+
team_req_stat: 2,
|
|
14003
|
+
team_req_stat_desc: 'widget signup',
|
|
14004
|
+
team_req_from_uid: visitor_uid,
|
|
14005
|
+
docType: 'team_request',
|
|
14006
|
+
team_req_app_id: null,
|
|
14007
|
+
access_type: 'contact_connection',
|
|
14008
|
+
uid: visitor_uid,
|
|
14009
|
+
contact_source: 'widget',
|
|
14010
|
+
share_item_id: canonical_profile_id,
|
|
14011
|
+
widget_origin: widget_origin || '',
|
|
14012
|
+
sender_account_info: { email, first_name, last_name, full_name: name },
|
|
14013
|
+
};
|
|
14014
|
+
const tr_save = await db_module.save_couch_doc('xuda_team', team_req_doc);
|
|
14015
|
+
if (tr_save.code < 0) return { code: -1, data: tr_save.data || 'team_request_create_failed' };
|
|
14016
|
+
|
|
14017
|
+
// fire and forget — sending the invitation email shouldn't block the visitor's response
|
|
14018
|
+
team_ms.send_access_invitation({ team_req_id: team_req_doc._id }).catch(() => {});
|
|
14019
|
+
}
|
|
14020
|
+
|
|
14021
|
+
let state = team_req_doc.team_req_stat === 3 ? 'open' : team_req_doc.team_req_stat === 4 ? 'declined' : 'pending';
|
|
14022
|
+
let contact_id = team_req_doc.team_req_to_contact_id;
|
|
14023
|
+
let conversation_id;
|
|
14024
|
+
|
|
14025
|
+
const auto_accept = !!ap_doc?.auto_respond;
|
|
14026
|
+
if (state === 'pending' && auto_accept) {
|
|
14027
|
+
try {
|
|
14028
|
+
contact_id = await _widget_auto_accept(team_req_doc, assigned_uid, email, name);
|
|
14029
|
+
state = 'open';
|
|
14030
|
+
} catch (err) {
|
|
14031
|
+
state = 'pending';
|
|
14032
|
+
}
|
|
14033
|
+
}
|
|
14034
|
+
|
|
14035
|
+
if (state === 'open' && contact_id) {
|
|
14036
|
+
try {
|
|
14037
|
+
conversation_id = await _widget_ensure_conversation(assigned_uid, canonical_profile_id, contact_id, job_id, headers);
|
|
14038
|
+
} catch (err) {}
|
|
14039
|
+
}
|
|
14040
|
+
|
|
14041
|
+
const widget_token = `wds_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
14042
|
+
const now = Date.now();
|
|
14043
|
+
const session_doc = {
|
|
14044
|
+
_id: widget_token,
|
|
14045
|
+
docType: 'widget_session',
|
|
14046
|
+
stat: 2,
|
|
14047
|
+
state,
|
|
14048
|
+
visitor_uid,
|
|
14049
|
+
team_req_id: team_req_doc._id,
|
|
14050
|
+
assigned_uid,
|
|
14051
|
+
owner_uid: canonical_owner_uid,
|
|
14052
|
+
profile_id: canonical_profile_id,
|
|
14053
|
+
contact_id,
|
|
14054
|
+
conversation_id,
|
|
14055
|
+
widget_origin: widget_origin || '',
|
|
14056
|
+
date_created_ts: now,
|
|
14057
|
+
ts: now,
|
|
14058
|
+
ttl_ts: now + WIDGET_SESSION_TTL_MS,
|
|
14059
|
+
};
|
|
14060
|
+
await db_module.save_couch_doc('xuda_sessions', session_doc);
|
|
14061
|
+
|
|
14062
|
+
const assigned_info_ret = await account_ms.get_account_name({ uid_query: assigned_uid });
|
|
14063
|
+
const assigned_name = assigned_info_ret?.data?.full_name || assigned_info_ret?.data?.first_name || '';
|
|
14064
|
+
const assigned_avatar = assigned_info_ret?.data?.profile_picture || assigned_info_ret?.data?.profile_avatar || '';
|
|
14065
|
+
const greeting = ap_doc?.widget_config?.greeting || WIDGET_DEFAULT_GREETING;
|
|
14066
|
+
|
|
14067
|
+
return {
|
|
14068
|
+
code: 1,
|
|
14069
|
+
data: {
|
|
14070
|
+
widget_token,
|
|
14071
|
+
state,
|
|
14072
|
+
assigned_name,
|
|
14073
|
+
assigned_avatar,
|
|
14074
|
+
greeting,
|
|
14075
|
+
conversation_id,
|
|
14076
|
+
signup_method: signup_method || 'email',
|
|
14077
|
+
account_active: signup_method === 'google',
|
|
14078
|
+
signin_url: signup_method === 'google' ? `https://${_conf.domain || 'xuda.ai'}/dashboard/login` : undefined,
|
|
14079
|
+
},
|
|
14080
|
+
};
|
|
14081
|
+
} catch (err) {
|
|
14082
|
+
return { code: -1, data: err.message || String(err) };
|
|
14083
|
+
}
|
|
14084
|
+
};
|
|
14085
|
+
|
|
14086
|
+
export const widget_send_message = async function (req, job_id, headers) {
|
|
14087
|
+
try {
|
|
14088
|
+
const { widget_token, prompt = '', attachments = [] } = req;
|
|
14089
|
+
const text = typeof prompt === 'string' ? prompt : '';
|
|
14090
|
+
if (!text.trim() && (!Array.isArray(attachments) || !attachments.length)) {
|
|
14091
|
+
return { code: -1, data: 'invalid_prompt' };
|
|
14092
|
+
}
|
|
14093
|
+
|
|
14094
|
+
// Validate + sanitize image attachments. Inline base64 images only, max 5, ~2.5MB each.
|
|
14095
|
+
const safe_attachments = [];
|
|
14096
|
+
for (const att of (Array.isArray(attachments) ? attachments : []).slice(0, 5)) {
|
|
14097
|
+
if (!att || typeof att.data_uri !== 'string') continue;
|
|
14098
|
+
if (!/^data:image\/(png|jpeg|jpg|gif|webp);base64,/i.test(att.data_uri)) continue;
|
|
14099
|
+
if (att.data_uri.length > 3_500_000) continue;
|
|
14100
|
+
safe_attachments.push({
|
|
14101
|
+
data_uri: att.data_uri,
|
|
14102
|
+
filename: String(att.filename || 'image').slice(0, 200),
|
|
14103
|
+
size: Number(att.size) || 0,
|
|
14104
|
+
type: String(att.type || 'image/png').slice(0, 60),
|
|
14105
|
+
});
|
|
14106
|
+
}
|
|
14107
|
+
|
|
14108
|
+
let session = await _widget_load_session(widget_token);
|
|
14109
|
+
if (!session) return { code: -401, data: 'invalid_token' };
|
|
14110
|
+
|
|
14111
|
+
session = await _widget_refresh_state(session, job_id, headers);
|
|
14112
|
+
if (session.state !== 'open') return { code: -409, data: 'pending_approval' };
|
|
14113
|
+
|
|
14114
|
+
const { assigned_uid, profile_id, conversation_id, contact_id, widget_origin } = session;
|
|
14115
|
+
if (!conversation_id || !contact_id) return { code: -409, data: 'not_ready' };
|
|
14116
|
+
|
|
14117
|
+
const rep_app_id = await account_ms.get_account_default_project_id(assigned_uid);
|
|
14118
|
+
|
|
14119
|
+
// Compose the prompt text to include attachment notice (visible to the agent + history)
|
|
14120
|
+
let prompt_for_history = text.trim();
|
|
14121
|
+
if (safe_attachments.length) {
|
|
14122
|
+
const att_note = `[Attached ${safe_attachments.length} image${safe_attachments.length > 1 ? 's' : ''}: ${safe_attachments.map((a) => a.filename).join(', ')}]`;
|
|
14123
|
+
prompt_for_history = prompt_for_history ? `${prompt_for_history}\n\n${att_note}` : att_note;
|
|
14124
|
+
}
|
|
14125
|
+
|
|
14126
|
+
let conversation_item_reference_id;
|
|
14127
|
+
try {
|
|
14128
|
+
conversation_item_reference_id = await add_conversation_item(assigned_uid, profile_id, conversation_id, prompt_for_history, 'chat', contact_id, { direction: 'in' });
|
|
14129
|
+
report_ai_status('conversations');
|
|
14130
|
+
} catch (err) {
|
|
14131
|
+
report_ai_status('conversations', err);
|
|
14132
|
+
}
|
|
14133
|
+
|
|
14134
|
+
const now = Date.now();
|
|
14135
|
+
const item_doc = {
|
|
14136
|
+
_id: await _common.xuda_get_uuid('chat_conversation_item'),
|
|
14137
|
+
stat: 3,
|
|
14138
|
+
docType: 'chat_conversation_item',
|
|
14139
|
+
uid: assigned_uid,
|
|
14140
|
+
conversation_type: 'chat',
|
|
14141
|
+
type: 'chat',
|
|
14142
|
+
date_created_ts: now,
|
|
14143
|
+
ts: now,
|
|
14144
|
+
conversation_id,
|
|
14145
|
+
text: text.trim(),
|
|
14146
|
+
reference_id: contact_id,
|
|
14147
|
+
conversation_item_reference_id,
|
|
14148
|
+
direction: 'in',
|
|
14149
|
+
role: 'user',
|
|
14150
|
+
widget_source: true,
|
|
14151
|
+
widget_origin: widget_origin || '',
|
|
14152
|
+
rtl: _common.detectRTL(text),
|
|
14153
|
+
attachments: safe_attachments,
|
|
14154
|
+
};
|
|
14155
|
+
const save_ret = await db_module.save_app_couch_doc(rep_app_id, item_doc);
|
|
14156
|
+
|
|
14157
|
+
session.ts = now;
|
|
14158
|
+
await db_module.save_couch_doc('xuda_sessions', session);
|
|
14159
|
+
|
|
14160
|
+
try {
|
|
14161
|
+
auto_response(assigned_uid, profile_id, contact_id, 'chat');
|
|
14162
|
+
} catch (err) {}
|
|
14163
|
+
|
|
14164
|
+
return { code: 15, data: save_ret };
|
|
14165
|
+
} catch (err) {
|
|
14166
|
+
return { code: -1, data: err.message || String(err) };
|
|
14167
|
+
}
|
|
14168
|
+
};
|
|
14169
|
+
|
|
14170
|
+
export const widget_get_messages = async function (req, job_id, headers) {
|
|
14171
|
+
try {
|
|
14172
|
+
const { widget_token, since_ts = 0, limit = 100 } = req;
|
|
14173
|
+
let session = await _widget_load_session(widget_token);
|
|
14174
|
+
if (!session) return { code: -401, data: 'invalid_token' };
|
|
14175
|
+
|
|
14176
|
+
session = await _widget_refresh_state(session, job_id, headers);
|
|
14177
|
+
|
|
14178
|
+
const server_ts = Date.now();
|
|
14179
|
+
if (session.state !== 'open' || !session.conversation_id) {
|
|
14180
|
+
return { code: 15, data: { state: session.state, items: [], conversation_id: session.conversation_id || null, server_ts } };
|
|
14181
|
+
}
|
|
14182
|
+
|
|
14183
|
+
const rep_app_id = await account_ms.get_account_default_project_id(session.assigned_uid);
|
|
14184
|
+
const ret = await db_module.find_app_couch_query(rep_app_id, {
|
|
14185
|
+
selector: {
|
|
14186
|
+
docType: 'chat_conversation_item',
|
|
14187
|
+
conversation_id: session.conversation_id,
|
|
14188
|
+
stat: 3,
|
|
14189
|
+
date_created_ts: { $gt: Number(since_ts) || 0 },
|
|
14190
|
+
},
|
|
14191
|
+
sort: [{ date_created_ts: 'asc' }],
|
|
14192
|
+
limit: Math.min(Number(limit) || 100, 500),
|
|
14193
|
+
});
|
|
14194
|
+
|
|
14195
|
+
const items = (ret?.docs || []).map((d) => ({
|
|
14196
|
+
_id: d._id,
|
|
14197
|
+
text: d.text,
|
|
14198
|
+
direction: d.direction,
|
|
14199
|
+
role: d.role,
|
|
14200
|
+
date_created_ts: d.date_created_ts,
|
|
14201
|
+
conversation_type: d.conversation_type,
|
|
14202
|
+
attachments: d.attachments,
|
|
14203
|
+
rtl: d.rtl,
|
|
14204
|
+
}));
|
|
14205
|
+
|
|
14206
|
+
return { code: 15, data: { state: 'open', items, conversation_id: session.conversation_id, server_ts } };
|
|
14207
|
+
} catch (err) {
|
|
14208
|
+
return { code: -1, data: err.message || String(err) };
|
|
14209
|
+
}
|
|
14210
|
+
};
|
package/index_ms.mjs
CHANGED
|
@@ -81,6 +81,10 @@ export const delete_mini_app = async function (...args) {
|
|
|
81
81
|
return await broker.send_to_queue("delete_mini_app", ...args);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
export const start_cli_agent_conversation = async function (...args) {
|
|
85
|
+
return await broker.send_to_queue("start_cli_agent_conversation", ...args);
|
|
86
|
+
};
|
|
87
|
+
|
|
84
88
|
export const update_ai_agent = async function (...args) {
|
|
85
89
|
return await broker.send_to_queue("update_ai_agent", ...args);
|
|
86
90
|
};
|
|
@@ -236,3 +240,23 @@ export const get_chat_suggestions = async function (...args) {
|
|
|
236
240
|
export const validate_credits_limit = async function (...args) {
|
|
237
241
|
return await broker.send_to_queue("validate_credits_limit", ...args);
|
|
238
242
|
};
|
|
243
|
+
|
|
244
|
+
export const get_widget_embed_snippet = async function (...args) {
|
|
245
|
+
return await broker.send_to_queue("get_widget_embed_snippet", ...args);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export const widget_signup = async function (...args) {
|
|
249
|
+
return await broker.send_to_queue("widget_signup", ...args);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const widget_signup_google = async function (...args) {
|
|
253
|
+
return await broker.send_to_queue("widget_signup_google", ...args);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const widget_send_message = async function (...args) {
|
|
257
|
+
return await broker.send_to_queue("widget_send_message", ...args);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const widget_get_messages = async function (...args) {
|
|
261
|
+
return await broker.send_to_queue("widget_get_messages", ...args);
|
|
262
|
+
};
|
package/index_msa.mjs
CHANGED
|
@@ -81,6 +81,10 @@ export const delete_mini_app = function (...args) {
|
|
|
81
81
|
broker.send_to_queue_async("delete_mini_app", ...args);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
export const start_cli_agent_conversation = function (...args) {
|
|
85
|
+
broker.send_to_queue_async("start_cli_agent_conversation", ...args);
|
|
86
|
+
};
|
|
87
|
+
|
|
84
88
|
export const update_ai_agent = function (...args) {
|
|
85
89
|
broker.send_to_queue_async("update_ai_agent", ...args);
|
|
86
90
|
};
|
|
@@ -236,3 +240,23 @@ export const get_chat_suggestions = function (...args) {
|
|
|
236
240
|
export const validate_credits_limit = function (...args) {
|
|
237
241
|
broker.send_to_queue_async("validate_credits_limit", ...args);
|
|
238
242
|
};
|
|
243
|
+
|
|
244
|
+
export const get_widget_embed_snippet = function (...args) {
|
|
245
|
+
broker.send_to_queue_async("get_widget_embed_snippet", ...args);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export const widget_signup = function (...args) {
|
|
249
|
+
broker.send_to_queue_async("widget_signup", ...args);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const widget_signup_google = function (...args) {
|
|
253
|
+
broker.send_to_queue_async("widget_signup_google", ...args);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export const widget_send_message = function (...args) {
|
|
257
|
+
broker.send_to_queue_async("widget_send_message", ...args);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const widget_get_messages = function (...args) {
|
|
261
|
+
broker.send_to_queue_async("widget_get_messages", ...args);
|
|
262
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xuda.io/ai_module",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5611",
|
|
4
4
|
"description": "Xuda AI Module",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@imgly/background-removal-node": "
|
|
8
|
+
"@imgly/background-removal-node": "1.4.3",
|
|
9
9
|
"@openai/agents": "^0.3.4",
|
|
10
10
|
"amqplib": "^0.10.9",
|
|
11
11
|
"dotenv": "^16.0.3",
|
|
12
|
-
"firebase-admin": "^
|
|
12
|
+
"firebase-admin": "^13.10.0",
|
|
13
13
|
"fs-extra": "^11.2.0",
|
|
14
14
|
"https-proxy-agent": "^7.0.6",
|
|
15
15
|
"json5": "^2.2.3",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"uglify-js": "^3.19.3",
|
|
23
23
|
"utf8": "^3.0.0",
|
|
24
24
|
"vm2": "^3.10.0",
|
|
25
|
-
"youtube-dl-exec": "^
|
|
25
|
+
"youtube-dl-exec": "^3.1.7",
|
|
26
26
|
"zod": "^3.25.67",
|
|
27
27
|
"zod-to-json-schema": "^3.24.6"
|
|
28
28
|
},
|