@xuda.io/ai_module 1.1.5629 → 1.1.5631
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 +130 -2
- package/index.mjs.premerge.bak +15949 -0
- package/index_ms.mjs +4 -0
- package/index_msa.mjs +4 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -195,7 +195,7 @@ const get_openai_codex_exec_args = function (codex_model, opts = {}) {
|
|
|
195
195
|
return args;
|
|
196
196
|
};
|
|
197
197
|
|
|
198
|
-
global._conf =
|
|
198
|
+
global._conf = (await import(path.join(process.env.XUDA_HOME, "common", "load_conf.mjs"))).loadConf();
|
|
199
199
|
|
|
200
200
|
const _defaultAppConfig = {
|
|
201
201
|
credential: admin.credential.cert(_conf.firebase_serviceAccount),
|
|
@@ -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
|
-
|
|
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
|