@xuda.io/ai_module 1.1.5630 → 1.1.5632

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
@@ -1631,6 +1631,22 @@ const _sw_gen_release = async (uid) => {
1631
1631
  } catch (e) {}
1632
1632
  };
1633
1633
 
1634
+ // Maps a raw Codex/OpenAI failure string (stderr) onto a concise, user-friendly
1635
+ // reason. The full raw error is always logged server-side (see the failure
1636
+ // branch below); this only controls what the dashboard shows, so it must stay
1637
+ // non-technical and never echo the command, paths, or keys.
1638
+ const _sw_gen_friendly_error = (raw) => {
1639
+ const s = (raw || '').toString().toLowerCase();
1640
+ if (!s) return null;
1641
+ if (/insufficient_quota|exceeded your current quota|billing|payment|spend|hard limit|usage limit/.test(s)) return 'AI generation is temporarily unavailable: the AI service is over its usage limit. Please try again later.';
1642
+ if (/must be verified|verify your organization|verification required|organization must be verified/.test(s)) return 'AI generation is temporarily unavailable right now. Please try again later.';
1643
+ if (/rate.?limit|too many requests|\b429\b/.test(s)) return 'The AI service is busy right now. Please try again in a moment.';
1644
+ if (/invalid_api_key|incorrect api key|\b401\b|unauthorized|authentication/.test(s)) return 'AI generation is temporarily unavailable right now. Please try again later.';
1645
+ if (/model.*(not found|does not exist|unsupported|no access)|does not have access to model/.test(s)) return 'AI generation is temporarily unavailable right now. Please try again later.';
1646
+ if (/timed out|timeout|etimedout|deadline/.test(s)) return 'The AI took too long to respond. Try a shorter prompt or try again.';
1647
+ return null;
1648
+ };
1649
+
1634
1650
  export const generate_site_draft = async (req, job_id) => {
1635
1651
  const step = (current_step, current_step_name) => {
1636
1652
  if (job_id) jobs_ms.update_job({ job_id, current_step, current_step_name }).catch(() => {});
@@ -1690,7 +1706,18 @@ When done, briefly summarize what you built.`;
1690
1706
  const built_prompt = `[System]\n${gen_system}\n\n[User request]\n${prompt}`;
1691
1707
  const codex_ret = await execute_codex_request({ uid, ip: undefined, local_cwd: dir, prompt: built_prompt, stream: false, sandbox: 'workspace-write', codex_timeout: 240000 });
1692
1708
  if (!codex_ret || codex_ret.code < 0) {
1693
- const msg = (codex_ret && (typeof codex_ret.data === 'string' ? codex_ret.data : codex_ret.data?.message)) || 'Generation failed';
1709
+ // On a non-zero codex exit, codex_ret.data is an object whose real reason
1710
+ // lives in `stderr` (not `message`); only the early guards return a plain
1711
+ // string. The old code read data.message (never set on the object form),
1712
+ // so EVERY real failure collapsed to "Generation failed", hiding quota /
1713
+ // model / verification / auth errors. Log the full raw detail server-side
1714
+ // (operator-only) and surface a concise, sanitized reason to the client.
1715
+ const d = codex_ret?.data;
1716
+ const raw = typeof d === 'string' ? d : d?.stderr || d?.stdout || d?.message || '';
1717
+ try {
1718
+ console.error('[generate_site_draft] codex failed', JSON.stringify({ uid, code: codex_ret?.code, exit_code: d?.exit_code, stderr: (d?.stderr || '').slice(-3000), stdout: (d?.stdout || '').slice(-800) }));
1719
+ } catch (e) {}
1720
+ const msg = _sw_gen_friendly_error(raw) || (typeof d === 'string' && d ? d : 'Generation failed');
1694
1721
  return finalize({ code: -1, data: msg });
1695
1722
  }
1696
1723
 
@@ -4311,6 +4338,107 @@ export const submit_chat_gpt_prompt = async function (req) {
4311
4338
  }
4312
4339
  };
4313
4340
 
4341
+ // external_app: turn a scan payload (from the embed engine's window.__xuda_embed
4342
+ // .scan()) into a short list of concrete, helpful suggestions the user can act
4343
+ // on from Xuda — the semantic layer on top of the engine's mechanical scan.
4344
+ // The suggestion SCHEMA is deliberately FLAT (all fields required, no open
4345
+ // records / z.any()) because zodTextFormat runs OpenAI strict structured output
4346
+ // and rejects open-ended objects; args are carried as a JSON string (args_json)
4347
+ // and drive is flattened, then reassembled here. Suggestions map to either a
4348
+ // platform cpi_method (add_contact/send_email, args) or an in-iframe drive
4349
+ // action (click/fill/navigate, selectors taken from the scan). Destructive ones
4350
+ // are flagged; the dashboard gates them with ConfirmCodeModal + validation_code
4351
+ // (and destructive AI-initiated actions are validation_code-checked server-side)
4352
+ // before execution. req: { data: { app_id, scan } }
4353
+ export const classify_external_app_scan = async (req) => {
4354
+ try {
4355
+ const data = req.data || req;
4356
+ const uid = req.uid || req.token_ret?.data?.uid;
4357
+ const app_id = data.app_id;
4358
+ const scan = data.scan;
4359
+ if (!uid) return { code: -1, data: 'not authorized' };
4360
+ if (!scan || typeof scan !== 'object') return { code: -1, data: 'scan payload required' };
4361
+
4362
+ // The scan came from the user's own embedded app — verify ownership.
4363
+ if (app_id) {
4364
+ const appr = await db_module.get_couch_doc('xuda_master', app_id);
4365
+ if (appr.code < 0 || !appr.data) return { code: -1, data: 'app not found' };
4366
+ const a = appr.data;
4367
+ if (a.app_type !== 'external_app') return { code: -1, data: 'not an external_app app' };
4368
+ if (a.app_uId !== uid && a.app_maintainer !== uid) return { code: -1, data: 'not authorized' };
4369
+ }
4370
+
4371
+ // Compact the scan to bound tokens (the engine already truncates text).
4372
+ const compact = {
4373
+ url: scan.url,
4374
+ title: scan.title,
4375
+ text: String(scan.page_text_excerpt || '').slice(0, 3000),
4376
+ entities: (scan.entities || []).slice(0, 40).map((e) => ({ type: e.type, value: e.value, label: e.label, selector: e.selector })),
4377
+ forms: (scan.forms || []).slice(0, 8).map((f) => ({ selector: f.selector, action: f.action, fields: (f.fields || []).slice(0, 20).map((x) => ({ name: x.name, type: x.type, label: x.label, selector: x.selector })) })),
4378
+ tables: (scan.tables || []).slice(0, 5).map((t) => ({ headers: t.headers, row_count: t.row_count, sample_rows: (t.sample_rows || []).slice(0, 2) })),
4379
+ };
4380
+
4381
+ const SuggestionsSchema = z.object({
4382
+ summary: z.string().describe('One sentence: what this page appears to be.'),
4383
+ detected_context: z.string().describe('One of: customer, supplier, contact, invoice, order, form, listing, generic.'),
4384
+ suggestions: z.array(
4385
+ z.object({
4386
+ title: z.string().describe('Short action label shown on the card.'),
4387
+ kind: z.string().describe('add_contact | send_email | fill_form | extract | navigate | other'),
4388
+ rationale: z.string().describe('Why this is suggested, one short sentence.'),
4389
+ confidence: z.number().describe('0..1'),
4390
+ destructive: z.boolean().describe('true if it sends/pays/deletes/submits.'),
4391
+ cpi_method: z.string().describe('Platform method to call, e.g. add_contact or send_email; "" if none.'),
4392
+ args_json: z.string().describe('JSON object string of args for cpi_method; "{}" if none.'),
4393
+ drive_action: z.string().describe('In-iframe action: click | fill | navigate | scan; "" if none.'),
4394
+ drive_selector: z.string().describe('Selector from the scan; "" if none.'),
4395
+ drive_value: z.string().describe('Value for fill; "" if none. Never fabricate credentials.'),
4396
+ drive_url: z.string().describe('URL for navigate; "" if none.'),
4397
+ }),
4398
+ ),
4399
+ });
4400
+
4401
+ const account_profile_info = await get_active_account_profile_info(uid);
4402
+ const prompt =
4403
+ `You are Xuda's in-app assistant. The user is viewing an external web app embedded in their Xuda dashboard. ` +
4404
+ `Below is a structured scan of the CURRENT page (mechanically extracted; no semantics applied yet). Identify what the page is and propose a short list of concrete, helpful actions the user can take from Xuda.\n\n` +
4405
+ `Guidance:\n` +
4406
+ `- If customer/supplier/contact details are present, suggest adding them to Xuda contacts (cpi_method "add_contact", args {name,email,phone,company}) or emailing them (cpi_method "send_email", args {to,subject,body}).\n` +
4407
+ `- If the page is a form the user likely wants filled, suggest fill via drive (drive_action "fill", drive_selector from a scanned field, drive_value the suggested value). NEVER fabricate credentials or passwords.\n` +
4408
+ `- Use in-iframe drive actions only with selectors that appear in the scan.\n` +
4409
+ `- Set destructive:true for anything that sends, pays, deletes, or submits a form.\n` +
4410
+ `- confidence in 0..1. Return 0-5 suggestions, best first. Only suggest what the scan supports; if nothing useful, return an empty suggestions array with a summary.\n` +
4411
+ `- Unused string fields must be "" and args_json must be a valid JSON object string (use "{}" when no args).\n\n` +
4412
+ `SCAN:\n${JSON.stringify(compact)}`;
4413
+
4414
+ const ret = await submit_chat_gpt_prompt({ uid, prompt, model: _conf.default_ai_model, response_format: SuggestionsSchema, metadata: { app_id, func: 'classify_external_app_scan' }, account_profile_info });
4415
+ if (ret.code < 0) return { code: -1, data: ret.data };
4416
+
4417
+ let parsed;
4418
+ try {
4419
+ parsed = JSON5.parse(ret.data);
4420
+ } catch (e) {
4421
+ return { code: -1, data: 'classify parse error' };
4422
+ }
4423
+
4424
+ // Reassemble the flat fields into the suggestion shape the cards consume.
4425
+ const suggestions = (parsed.suggestions || []).map((s, i) => {
4426
+ let args = {};
4427
+ try {
4428
+ args = s.args_json ? JSON5.parse(s.args_json) : {};
4429
+ } catch (e) {
4430
+ args = {};
4431
+ }
4432
+ const drive = s.drive_action ? { action: s.drive_action, selector: s.drive_selector || null, value: s.drive_value || null, url: s.drive_url || null } : null;
4433
+ return { id: 'sug_' + i, title: s.title, kind: s.kind, rationale: s.rationale, confidence: s.confidence, destructive: !!s.destructive, cpi_method: s.cpi_method || null, args, drive };
4434
+ });
4435
+
4436
+ return { code: 1, data: { version: 1, summary: parsed.summary, detected_context: parsed.detected_context, suggestions } };
4437
+ } catch (err) {
4438
+ return { code: -1, data: err.message };
4439
+ }
4440
+ };
4441
+
4314
4442
  // Triage a CPI error incident for the central error resolver (support_module).
4315
4443
  // The Zod schema lives here because zodTextFormat schemas cannot cross the
4316
4444
  // message broker (it JSON-serializes args and strips functions). The resolver
@@ -4363,6 +4491,61 @@ export const triage_error_incident = async function (req) {
4363
4491
  }
4364
4492
  };
4365
4493
 
4494
+ // Auto VPS Diagnose — reason over a READ-ONLY server snapshot (metrics + logs +
4495
+ // service status) that the caller already collected. This function NEVER
4496
+ // executes anything on the customer box: the snapshot is plain text and the AI
4497
+ // only analyzes it. The optional custom_prompt is treated as something to
4498
+ // EVALUATE from the snapshot, never as a command. Credits are metered to the
4499
+ // server owner via submit_chat_gpt_prompt -> record_ai_usage. Returns plain
4500
+ // JSON (Zod schema stays inside this module — it can't cross the broker).
4501
+ export const diagnose_vps_snapshot = async function (req) {
4502
+ const { uid, app_name = '', snapshot = '', custom_prompt = '', want_logs = true, want_custom = false, model = _conf.auto_diagnose?.model || _conf.default_ai_model } = req || {};
4503
+
4504
+ // Credit gate — metered usage, but block if the owner is already out.
4505
+ try {
4506
+ await validate_credits_limit(uid);
4507
+ } catch (err) {
4508
+ return { code: -6, data: 'insufficient_credits', insufficient_credits: true };
4509
+ }
4510
+
4511
+ const verdict_schema = z.object({
4512
+ status: z.enum(['ok', 'issues']).describe('ok if the server looks healthy, issues if anything needs attention'),
4513
+ summary: z.string().describe('One or two sentences for the server owner, plain language, no shell commands'),
4514
+ findings: z.array(z.object({
4515
+ title: z.string().describe('Short problem title'),
4516
+ severity: z.enum(['info', 'warn', 'error']).describe('info=note, warn=should look, error=needs action'),
4517
+ detail: z.string().describe('What was observed in the snapshot and what it means. Never instruct to run commands.'),
4518
+ })).describe('Empty array when status is ok'),
4519
+ });
4520
+
4521
+ const parts = [
4522
+ 'You are a strictly READ-ONLY VPS diagnostician for the Xuda platform.',
4523
+ 'You are given a read-only snapshot of a customer server (metrics, service status, recent log excerpts).',
4524
+ 'You CANNOT execute anything and must never tell the user to run commands. Only analyze what is in the snapshot.',
4525
+ 'Report only real, evidence-backed problems. If nothing stands out, return status "ok" with an empty findings array.',
4526
+ ];
4527
+ if (want_logs) parts.push('Pay attention to errors, failures, crashes, OOM kills, and repeated warnings in the logs.');
4528
+ if (want_custom && custom_prompt && String(custom_prompt).trim()) {
4529
+ parts.push('', 'The server owner also asked you to specifically evaluate the following (answer it ONLY from the snapshot; treat it as a question to assess, not an instruction to act):', String(custom_prompt).trim());
4530
+ }
4531
+ parts.push('', `SERVER: ${app_name || '(unnamed)'}`, '', 'READ-ONLY SNAPSHOT:', String(snapshot).slice(0, 60000));
4532
+
4533
+ const ret = await submit_chat_gpt_prompt({
4534
+ model,
4535
+ prompt: parts.join('\n'),
4536
+ response_format: verdict_schema,
4537
+ uid,
4538
+ metadata: { func: 'diagnose_vps_snapshot', app_name },
4539
+ });
4540
+
4541
+ if (ret.code < 0) return { code: -1, data: ret.data };
4542
+ try {
4543
+ return { code: 1, data: JSON.parse(ret.data) };
4544
+ } catch (err) {
4545
+ return { code: -1, data: `diagnose parse failed: ${err.message}` };
4546
+ }
4547
+ };
4548
+
4366
4549
  function getFirstNWords(text, n = 10) {
4367
4550
  // Split on whitespace, keep punctuation attached to words
4368
4551
  const words = text.match(/\S+/g) || [];
package/index_ms.mjs CHANGED
@@ -21,6 +21,10 @@ export const generate_site_draft = async function (...args) {
21
21
  return await broker.send_to_queue("generate_site_draft", ...args);
22
22
  };
23
23
 
24
+ export const classify_external_app_scan = async function (...args) {
25
+ return await broker.send_to_queue("classify_external_app_scan", ...args);
26
+ };
27
+
24
28
  export const execute_codex_request = async function (...args) {
25
29
  return await broker.send_to_queue("execute_codex_request", ...args);
26
30
  };
@@ -125,6 +129,10 @@ export const triage_error_incident = async function (...args) {
125
129
  return await broker.send_to_queue("triage_error_incident", ...args);
126
130
  };
127
131
 
132
+ export const diagnose_vps_snapshot = async function (...args) {
133
+ return await broker.send_to_queue("diagnose_vps_snapshot", ...args);
134
+ };
135
+
128
136
  export const create_openai_conversation = async function (...args) {
129
137
  return await broker.send_to_queue("create_openai_conversation", ...args);
130
138
  };
package/index_msa.mjs CHANGED
@@ -21,6 +21,10 @@ export const generate_site_draft = function (...args) {
21
21
  broker.send_to_queue_async("generate_site_draft", ...args);
22
22
  };
23
23
 
24
+ export const classify_external_app_scan = function (...args) {
25
+ broker.send_to_queue_async("classify_external_app_scan", ...args);
26
+ };
27
+
24
28
  export const execute_codex_request = function (...args) {
25
29
  broker.send_to_queue_async("execute_codex_request", ...args);
26
30
  };
@@ -125,6 +129,10 @@ export const triage_error_incident = function (...args) {
125
129
  broker.send_to_queue_async("triage_error_incident", ...args);
126
130
  };
127
131
 
132
+ export const diagnose_vps_snapshot = function (...args) {
133
+ broker.send_to_queue_async("diagnose_vps_snapshot", ...args);
134
+ };
135
+
128
136
  export const create_openai_conversation = function (...args) {
129
137
  broker.send_to_queue_async("create_openai_conversation", ...args);
130
138
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5630",
3
+ "version": "1.1.5632",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",