@xuda.io/ai_module 1.1.5609 → 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 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
- return (req.context || req.dashboard_context || req.metadata?.context || conversation_doc.context || conversation_doc.dashboard_context || conversation_doc.metadata?.context || '').toString().trim().toLowerCase();
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: stream_delta_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', dashboard_context === 'console' ? 'Starting console request' : 'Starting dashboard request');
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
- conversation_doc.context = dashboard_context;
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 (dashboard_context === 'console') {
5440
- const codex_ret = await execute_codex_request({ ...req, conversation_id, conversation_doc, prompt, attachments, stream: false }, job_id, headers);
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 response_text = codex_ret.code < 0 ? `Console request failed: ${get_error_message(codex_ret.data, 'Codex request failed')}` : last_message || 'Console request completed.';
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
- emitToDashboard('stream_end');
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(response_text, {
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 === null || typeof auto_respond_agents === 'undefined') return;
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
- if (!auto_respond_agents.length) return;
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
- if (conversation_type === 'chat') {
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.5609",
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": "^1.4.5",
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": "^9.6.0",
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": "^2.1.3",
25
+ "youtube-dl-exec": "^3.1.7",
26
26
  "zod": "^3.25.67",
27
27
  "zod-to-json-schema": "^3.24.6"
28
28
  },