@xuda.io/ai_module 1.1.5614 → 1.1.5615

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
@@ -2085,6 +2085,10 @@ const create_image = async function (uid, prompt, model = 'gpt-image-1-mini', si
2085
2085
  prompt,
2086
2086
  size,
2087
2087
  n,
2088
+ // Output is downscaled to width×height (default 256²) right below, so a
2089
+ // high-quality 1024² render is wasted. 'low' is indistinguishable at
2090
+ // thumbnail size and 2-8x cheaper. Tunable via _conf.chat_image_quality.
2091
+ quality: _conf.chat_image_quality || 'low',
2088
2092
  });
2089
2093
  report_ai_status(model);
2090
2094
  } catch (err) {
@@ -9475,9 +9479,9 @@ async function restoreFaceWithOpenAI(base64Image, ctx = {}) {
9475
9479
  response = await client.images.edit({
9476
9480
  model,
9477
9481
  image: imageFile,
9478
- input_fidelity: 'high',
9479
- quality: 'high',
9480
- size: 'auto',
9482
+ input_fidelity: 'high', // identity preservation — keep high (don't degrade)
9483
+ quality: _conf.avatar_image_quality || 'low', // 'low' approved 2026-06-02 (A/B vs high/medium); identity held by input_fidelity:high. ~$0.08 vs $0.23 high. Override via _conf.avatar_image_quality.
9484
+ size: _conf.avatar_image_size || '1024x1024', // was 'auto' (could pick 1536 = ~1.5x output tokens); avatars are square
9481
9485
  prompt:
9482
9486
  'Restore this portrait photograph. Remove scratches, dust, grain, noise, faded color, and any age-related damage. Recover natural skin texture, sharpen face details, and balance lighting and color. Preserve the exact same person completely unchanged: same face shape, same eye shape and color, same nose, same mouth, same hairline and hair, same skin tone, same age, same gender, same facial expression, and same clothing. Do not alter, beautify, age, or de-age the person. Output a clean, naturally lit photograph of the same person.',
9483
9487
  });
@@ -14629,3 +14633,557 @@ export const widget_get_messages = async function (req, job_id, headers) {
14629
14633
  return { code: -1, data: err.message || String(err) };
14630
14634
  }
14631
14635
  };
14636
+
14637
+ // ============================================================================
14638
+ // Static-website generation pipeline (helpers + orchestrator)
14639
+ // ============================================================================
14640
+ //
14641
+ // pick_image_source - three-bucket router (Pexels / gpt-image-1 / chatgpt-image-latest)
14642
+ // pexels_search - first consumer of _conf.pexels.api_key
14643
+ // estimate_static_website_cost - credit pre-check for create + modify
14644
+ // ingest_attachments - categorize user-uploaded files into the output tree + manifest
14645
+ // generate_site_files - planning prompt + per-page HTML + per-image source + SEO files
14646
+ // modify_site_files - modify-by-prompt over existing file set
14647
+ //
14648
+ // Image-source bucket order (cheapest -> most expensive): Pexels (free) ->
14649
+ // gpt-image-1 (cheap AI) -> chatgpt-image-latest (premium AI). Decision is
14650
+ // driven by membership/workspace tier and credit balance so a paid user
14651
+ // running low auto-degrades from premium to cheap rather than failing.
14652
+
14653
+ const LOW_CREDIT_THRESHOLD = _conf.static_website?.low_credit_threshold ?? 10;
14654
+ const HIGH_CREDIT_THRESHOLD = _conf.static_website?.high_credit_threshold ?? 100;
14655
+
14656
+ export const pick_image_source = (account_doc, credit_balance) => {
14657
+ const free_mem = (account_doc?.membership_plan || 'free') === 'free';
14658
+ const free_ws = (account_doc?.ai_workspace_plan || 'free_ai_workspace') === 'free_ai_workspace';
14659
+ const credits = Number(credit_balance ?? 0);
14660
+ const paid_tier = !free_mem || !free_ws;
14661
+
14662
+ // Bucket 1: no AI option (fully-free + no credits)
14663
+ if (!paid_tier && credits < LOW_CREDIT_THRESHOLD) return 'pexels';
14664
+ // Bucket 3: premium, only for paid users with a comfortable buffer
14665
+ if (paid_tier && credits >= HIGH_CREDIT_THRESHOLD) return 'chatgpt-image-latest';
14666
+ // Bucket 2: cheaper AI - free users with credits, paid users running low (auto-degrade)
14667
+ return 'gpt-image-1';
14668
+ };
14669
+
14670
+ export const pexels_search = async (query, n = 1) => {
14671
+ const api_key = _conf.pexels?.api_key;
14672
+ if (!api_key) throw new Error('pexels.api_key not configured');
14673
+ const url = `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=${Math.max(1, Math.min(80, Number(n) || 1))}`;
14674
+ const r = await fetch(url, { headers: { Authorization: api_key } });
14675
+ if (!r.ok) throw new Error(`pexels search failed: ${r.status}`);
14676
+ const j = await r.json();
14677
+ const photos = (j.photos || []).slice(0, n);
14678
+ return Promise.all(
14679
+ photos.map(async (p) => {
14680
+ const img = await fetch(p.src.large || p.src.medium || p.src.original);
14681
+ if (!img.ok) throw new Error(`pexels fetch failed: ${img.status}`);
14682
+ const buf = Buffer.from(await img.arrayBuffer());
14683
+ return {
14684
+ content_b64: buf.toString('base64'),
14685
+ content_type: 'image/jpeg',
14686
+ src_url: p.url,
14687
+ photographer: p.photographer,
14688
+ photographer_url: p.photographer_url,
14689
+ };
14690
+ }),
14691
+ );
14692
+ };
14693
+
14694
+ // Expected per-tier work for the credit estimator. Real generated count is
14695
+ // usually a fraction of the hard cap (page_count_limit in PLAN_OBJ).
14696
+ const EXPECTED_STATIC_WEBSITE_WORK = {
14697
+ landing: { pages: 1, imgs_per_page: 2, seo: 0, css: 1 },
14698
+ full: { pages: 8, imgs_per_page: 3, seo: 0, css: 1 },
14699
+ seo: { pages: 25, imgs_per_page: 3, seo: 2, css: 1 },
14700
+ enterprise: { pages: 100, imgs_per_page: 3, seo: 2, css: 1 },
14701
+ };
14702
+
14703
+ const _image_price = (image_source) => {
14704
+ if (image_source === 'pexels') return 0;
14705
+ const m = _conf.ai_models?.[image_source];
14706
+ return m?.price ?? Math.max(m?.input ?? 0, m?.output ?? 0) ?? 5;
14707
+ };
14708
+
14709
+ const _text_price = () => {
14710
+ const k = _conf.default_ai_model || 'gpt-5.4-mini';
14711
+ const m = _conf.ai_models?.[k];
14712
+ return m?.price ?? 1;
14713
+ };
14714
+
14715
+ export const estimate_static_website_cost = (req) => {
14716
+ const plan_tier = req?.plan_tier;
14717
+ const image_source = req?.image_source || 'gpt-image-1';
14718
+ const mode = req?.mode || 'create';
14719
+ const current_page_count = Number(req?.current_page_count || 0);
14720
+
14721
+ const W = EXPECTED_STATIC_WEBSITE_WORK[plan_tier] || EXPECTED_STATIC_WEBSITE_WORK.landing;
14722
+ const pages =
14723
+ mode === 'modify'
14724
+ ? Math.max(1, Math.ceil(current_page_count * 0.3)) // modify touches ~30% by default
14725
+ : W.pages;
14726
+ const text_calls = 1 /* site plan */ + W.css + pages /* per-page HTML */ + W.seo;
14727
+ const image_calls = pages * W.imgs_per_page;
14728
+
14729
+ const text_unit = _text_price();
14730
+ const image_unit = _image_price(image_source);
14731
+ const credits_needed = text_calls * text_unit + image_calls * image_unit;
14732
+
14733
+ return {
14734
+ credits_needed,
14735
+ breakdown: { text_calls, text_unit, image_calls, image_unit, pages, mode },
14736
+ image_source,
14737
+ };
14738
+ };
14739
+
14740
+ // ── Attachment ingestion ────────────────────────────────────────────────
14741
+ //
14742
+ // Shape: [{ filename, mime_type, drive_path? | content_b64? | url? }]
14743
+ // At least one source field must be present. Files land in the deployed site
14744
+ // at a stable /uploads/{img,docs,text,data,files}/{slug} path so the LLM can
14745
+ // reference them by URL in generated HTML.
14746
+
14747
+ const _categorize_attachment = (att) => {
14748
+ const mt = (att?.mime_type || '').toLowerCase();
14749
+ if (mt.startsWith('image/')) return 'image';
14750
+ if (mt === 'application/pdf') return 'pdf';
14751
+ if (mt === 'text/csv' || mt === 'application/vnd.ms-excel') return 'data';
14752
+ if (mt === 'application/json' || mt === 'text/markdown' || mt.startsWith('text/')) return 'text';
14753
+ return 'binary';
14754
+ };
14755
+
14756
+ const _load_attachment_bytes = async (att) => {
14757
+ if (att.content_b64) return Buffer.from(att.content_b64, 'base64');
14758
+ if (att.drive_path) {
14759
+ const abs = path.join(_conf.studio_drive_path, att.drive_path);
14760
+ return await readFile(abs);
14761
+ }
14762
+ if (att.url) {
14763
+ const r = await fetch(att.url);
14764
+ if (!r.ok) throw new Error(`attachment fetch ${att.url}: ${r.status}`);
14765
+ return Buffer.from(await r.arrayBuffer());
14766
+ }
14767
+ throw new Error(`attachment ${att.filename} has no source (drive_path | content_b64 | url)`);
14768
+ };
14769
+
14770
+ export const ingest_attachments = async (attachments = []) => {
14771
+ const out = { manifest: [], file_entries: [], text_corpus: [] };
14772
+ for (const att of attachments) {
14773
+ if (!att || !att.filename) continue;
14774
+ const kind = _categorize_attachment(att);
14775
+ let bytes;
14776
+ try {
14777
+ bytes = await _load_attachment_bytes(att);
14778
+ } catch (err) {
14779
+ console.error('[ingest_attachments] skip', att.filename, err.message);
14780
+ continue;
14781
+ }
14782
+ const slug = String(att.filename)
14783
+ .replace(/[^a-z0-9.-]/gi, '_')
14784
+ .toLowerCase();
14785
+ let public_path;
14786
+ if (kind === 'image') public_path = `/uploads/img/${slug}`;
14787
+ else if (kind === 'pdf') public_path = `/uploads/docs/${slug}`;
14788
+ else if (kind === 'text') public_path = `/uploads/text/${slug}`;
14789
+ else if (kind === 'data') public_path = `/uploads/data/${slug}`;
14790
+ else public_path = `/uploads/files/${slug}`;
14791
+
14792
+ out.file_entries.push({
14793
+ path: public_path,
14794
+ content_b64: bytes.toString('base64'),
14795
+ content_type: att.mime_type || 'application/octet-stream',
14796
+ });
14797
+
14798
+ const manifest_entry = { filename: att.filename, kind, public_path };
14799
+ if (kind === 'text' || kind === 'data') {
14800
+ const text = bytes.toString('utf8').slice(0, 8000);
14801
+ manifest_entry.excerpt = text;
14802
+ out.text_corpus.push({ filename: att.filename, text });
14803
+ }
14804
+ if (kind === 'image') {
14805
+ manifest_entry.usage = `Embed via <img src="${public_path}" alt="..."> on relevant pages.`;
14806
+ }
14807
+ out.manifest.push(manifest_entry);
14808
+ }
14809
+ return out;
14810
+ };
14811
+
14812
+ // ── Generation orchestrator ─────────────────────────────────────────────
14813
+ //
14814
+ // generate_site_files runs the prompt -> plan -> pages -> images -> SEO chain.
14815
+ // Returns a flat [{ path, content_b64, content_type }] list ready for
14816
+ // domains_module.deploy_site.
14817
+
14818
+ const _safe_json_parse = (s) => {
14819
+ if (!s || typeof s !== 'string') return null;
14820
+ // The LLM sometimes wraps JSON in ```json fences despite instructions.
14821
+ const cleaned = s
14822
+ .trim()
14823
+ .replace(/^```(?:json)?\s*/i, '')
14824
+ .replace(/\s*```\s*$/i, '');
14825
+ try {
14826
+ return JSON.parse(cleaned);
14827
+ } catch (_) {
14828
+ return null;
14829
+ }
14830
+ };
14831
+
14832
+ const _chat = async (uid, prompt, account_profile_info, metadata) => {
14833
+ const ret = await submit_chat_gpt_prompt({
14834
+ uid,
14835
+ prompt,
14836
+ model: _conf.default_ai_model,
14837
+ metadata: metadata || {},
14838
+ account_profile_info,
14839
+ });
14840
+ if (ret.code < 0) throw new Error(`LLM call failed: ${ret.data}`);
14841
+ return ret.data || '';
14842
+ };
14843
+
14844
+ const _site_planning_prompt = (user_prompt, attachments_manifest, plan_tier, page_count_limit) => {
14845
+ const W = EXPECTED_STATIC_WEBSITE_WORK[plan_tier] || EXPECTED_STATIC_WEBSITE_WORK.landing;
14846
+ const default_pages = Math.min(W.pages, page_count_limit);
14847
+ return `You are planning a static website. Output STRICT JSON only, no prose, no markdown fences.
14848
+
14849
+ User brief:
14850
+ ${user_prompt}
14851
+
14852
+ User-uploaded files (manifest). The generated pages MUST use these where appropriate: embed images via <img src="$public_path">, quote / paraphrase text excerpts where they fit, link to PDFs, structure pages around data when relevant.
14853
+ ${JSON.stringify(attachments_manifest, null, 2)}
14854
+
14855
+ Hard rules:
14856
+ - Hosting tier: ${plan_tier}. Maximum ${page_count_limit} pages total. Aim for ${default_pages} pages unless the brief clearly implies more.
14857
+ - Each page has a slug (kebab-case path component, "" for home), title, and an ordered list of sections.
14858
+ - Each page also lists image_specs: image slots needed on that page. For each slot, decide source: 'attachment' (when an uploaded image fits) OR 'generate' (when one must be created).
14859
+ - For 'generate' slots, write a concise visual prompt AND a Pexels-friendly query string.
14860
+ - For 'attachment' slots, set attachment_path to one of the public_paths from the manifest.
14861
+ - style_directive is one paragraph describing the desired visual style (palette, typography, density) used to generate the global stylesheet.
14862
+
14863
+ Schema:
14864
+ {
14865
+ "pages": [
14866
+ {
14867
+ "slug": "string (empty for home)",
14868
+ "title": "string",
14869
+ "sections": [{"kind": "prose|image|quote|data_table|callout", "...": "..."}],
14870
+ "image_specs": [
14871
+ {"slot": "kebab-slug", "source": "generate|attachment", "prompt": "...", "query": "...", "attachment_path": "/uploads/...", "aspect": "16/10|1/1|3/2"}
14872
+ ]
14873
+ }
14874
+ ],
14875
+ "style_directive": "string"
14876
+ }
14877
+
14878
+ Respond with ONLY the JSON object.`;
14879
+ };
14880
+
14881
+ const _page_html_prompt = (page, style_directive, attachments_manifest) => `Generate a single static HTML page. Output ONLY the HTML, no markdown fences, no commentary.
14882
+
14883
+ Page brief:
14884
+ ${JSON.stringify(page, null, 2)}
14885
+
14886
+ Global style directive (matches /style.css that's already written):
14887
+ ${style_directive}
14888
+
14889
+ Attachments available (use these public_paths verbatim when referenced):
14890
+ ${JSON.stringify(attachments_manifest, null, 2)}
14891
+
14892
+ Hard rules:
14893
+ - DOCTYPE html, lang="en", responsive viewport meta.
14894
+ - Link to /style.css.
14895
+ - Reference images from /img/{slot}.{ext} for 'generate' slots and from their public_path for 'attachment' slots, exactly as listed in image_specs.
14896
+ - Semantic HTML5 (header, main, section, footer). No inline styles unless absolutely necessary.
14897
+ - No external CDNs, no JavaScript.
14898
+ - Title from page.title.
14899
+
14900
+ Respond with ONLY the raw HTML document.`;
14901
+
14902
+ const _css_prompt = (style_directive, plan_tier) => `Generate ONE global stylesheet for a ${plan_tier}-tier static website. Output ONLY the CSS, no markdown fences, no commentary.
14903
+
14904
+ Style directive:
14905
+ ${style_directive}
14906
+
14907
+ Hard rules:
14908
+ - Modern CSS only (no preprocessors). Use CSS variables for palette.
14909
+ - Responsive: mobile-first, breakpoints at 640px and 980px.
14910
+ - Typography: choose a serif heading + sans-serif body OR all sans, consistent with the directive.
14911
+ - Include base resets, layout containers, header, footer, basic image styling (max-width 100%, rounded), button styles.
14912
+ - Keep under 6 KB.
14913
+
14914
+ Respond with ONLY the raw CSS text.`;
14915
+
14916
+ const _sitemap_xml = (pages, hostname) => {
14917
+ const base = `https://${hostname}`;
14918
+ const urls = pages
14919
+ .map((p) => {
14920
+ const loc = p.slug ? `${base}/${p.slug}/` : `${base}/`;
14921
+ return ` <url><loc>${loc}</loc></url>`;
14922
+ })
14923
+ .join('\n');
14924
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
14925
+ };
14926
+
14927
+ const _robots_txt = (hostname) => `User-agent: *\nAllow: /\nSitemap: https://${hostname}/sitemap.xml\n`;
14928
+
14929
+ // Picks an extension for a generated image filename from its content-type.
14930
+ const _image_ext = (content_type) => {
14931
+ if (!content_type) return 'png';
14932
+ if (/webp/i.test(content_type)) return 'webp';
14933
+ if (/jpe?g/i.test(content_type)) return 'jpg';
14934
+ if (/png/i.test(content_type)) return 'png';
14935
+ if (/svg/i.test(content_type)) return 'svg';
14936
+ return 'png';
14937
+ };
14938
+
14939
+ export const generate_site_files = async (req) => {
14940
+ try {
14941
+ const { uid, prompt, attachments, plan_tier, page_count_limit, image_source, account_profile_info, hostname = 'sites.xuda.io' } = req;
14942
+ if (!uid || !prompt) return { code: -1, data: 'uid + prompt required' };
14943
+ if (!plan_tier) return { code: -1, data: 'plan_tier required' };
14944
+
14945
+ // 1. Ingest attachments (copies them into the output file list verbatim)
14946
+ const ingestion = await ingest_attachments(attachments || []);
14947
+
14948
+ // 2. Plan the site
14949
+ const plan_text = await _chat(uid, _site_planning_prompt(prompt, ingestion.manifest, plan_tier, page_count_limit), account_profile_info, { stage: 'static_website_plan', plan_tier });
14950
+ const plan = _safe_json_parse(plan_text);
14951
+ if (!plan || !Array.isArray(plan.pages) || !plan.pages.length) {
14952
+ return { code: -2, data: 'planning JSON malformed', plan_text };
14953
+ }
14954
+
14955
+ // 3. Hard-cap page count
14956
+ plan.pages = plan.pages.slice(0, Math.max(1, Math.min(page_count_limit, plan.pages.length)));
14957
+
14958
+ // 4. Global CSS
14959
+ const css = await _chat(uid, _css_prompt(plan.style_directive || '', plan_tier), account_profile_info, { stage: 'static_website_css', plan_tier });
14960
+
14961
+ // 5. Per-page HTML
14962
+ const html_files = [];
14963
+ for (const page of plan.pages) {
14964
+ const html = await _chat(uid, _page_html_prompt(page, plan.style_directive || '', ingestion.manifest), account_profile_info, { stage: 'static_website_page', plan_tier, slug: page.slug || '' });
14965
+ const rel = page.slug ? `/${page.slug}/index.html` : '/index.html';
14966
+ html_files.push({
14967
+ path: rel,
14968
+ content_b64: Buffer.from(html, 'utf8').toString('base64'),
14969
+ content_type: 'text/html',
14970
+ });
14971
+ }
14972
+
14973
+ // 6. Per-image generation
14974
+ const image_files = [];
14975
+ for (const page of plan.pages) {
14976
+ const specs = Array.isArray(page.image_specs) ? page.image_specs : [];
14977
+ for (const spec of specs) {
14978
+ try {
14979
+ if (spec.source === 'attachment') {
14980
+ // already in ingestion.file_entries — nothing to add
14981
+ continue;
14982
+ }
14983
+ if (image_source === 'pexels') {
14984
+ const [img] = await pexels_search(spec.query || spec.prompt || page.title, 1);
14985
+ if (!img) continue;
14986
+ image_files.push({
14987
+ path: `/img/${spec.slot}.jpg`,
14988
+ content_b64: img.content_b64,
14989
+ content_type: 'image/jpeg',
14990
+ });
14991
+ } else {
14992
+ const b64 = await create_image(
14993
+ uid,
14994
+ spec.prompt || spec.query || page.title,
14995
+ image_source, // 'gpt-image-1' | 'chatgpt-image-latest'
14996
+ '1024x1024',
14997
+ 1,
14998
+ 1024,
14999
+ 1024, // full-resolution output
15000
+ { kind: 'static_website', slot: spec.slot },
15001
+ account_profile_info,
15002
+ );
15003
+ if (!b64) continue;
15004
+ image_files.push({
15005
+ path: `/img/${spec.slot}.png`,
15006
+ content_b64: b64,
15007
+ content_type: 'image/png',
15008
+ });
15009
+ }
15010
+ } catch (err) {
15011
+ console.error('[generate_site_files] image fail', spec.slot, err.message);
15012
+ }
15013
+ }
15014
+ }
15015
+
15016
+ // 7. SEO files (seo + enterprise tiers only)
15017
+ const seo_files = [];
15018
+ if (plan_tier === 'seo' || plan_tier === 'enterprise') {
15019
+ seo_files.push({
15020
+ path: '/sitemap.xml',
15021
+ content_b64: Buffer.from(_sitemap_xml(plan.pages, hostname), 'utf8').toString('base64'),
15022
+ content_type: 'application/xml',
15023
+ });
15024
+ seo_files.push({
15025
+ path: '/robots.txt',
15026
+ content_b64: Buffer.from(_robots_txt(hostname), 'utf8').toString('base64'),
15027
+ content_type: 'text/plain',
15028
+ });
15029
+ }
15030
+
15031
+ // 8. Compose final flat file list
15032
+ const css_file = {
15033
+ path: '/style.css',
15034
+ content_b64: Buffer.from(css, 'utf8').toString('base64'),
15035
+ content_type: 'text/css',
15036
+ };
15037
+ const files = [css_file, ...html_files, ...image_files, ...seo_files, ...ingestion.file_entries];
15038
+
15039
+ return {
15040
+ code: 1,
15041
+ data: {
15042
+ files,
15043
+ plan,
15044
+ manifest: ingestion.manifest,
15045
+ stats: {
15046
+ page_count: plan.pages.length,
15047
+ image_count: image_files.length,
15048
+ attachment_count: ingestion.file_entries.length,
15049
+ },
15050
+ },
15051
+ };
15052
+ } catch (err) {
15053
+ return { code: -400, data: err.message };
15054
+ }
15055
+ };
15056
+
15057
+ // ── Modify orchestrator ────────────────────────────────────────────────
15058
+ //
15059
+ // Loads current files, asks LLM what to change, writes back only the deltas.
15060
+ // Image source stays pinned to whatever was chosen at creation - modify does
15061
+ // NOT silently upgrade Pexels -> AI (that would surprise free users).
15062
+
15063
+ const _modify_prompt = (user_prompt, current_files_index, attachments_manifest) => `You are editing an existing static website. Output STRICT JSON only, no prose, no markdown fences.
15064
+
15065
+ User request:
15066
+ ${user_prompt}
15067
+
15068
+ Current file index (path + size + content-type — NOT full content; ask the user to re-attach if you need to read a file's body):
15069
+ ${JSON.stringify(current_files_index, null, 2)}
15070
+
15071
+ New attachments uploaded with this request:
15072
+ ${JSON.stringify(attachments_manifest, null, 2)}
15073
+
15074
+ For each change you want to make, emit one entry in "changes":
15075
+ { "path": "/index.html", "action": "replace", "new_content": "...full new file content..." }
15076
+ { "path": "/about/index.html", "action": "create", "new_content": "..." }
15077
+ { "path": "/old-page/index.html", "action": "delete" }
15078
+
15079
+ Image regeneration: emit { "image_change": { "path": "/img/hero.png", "new_prompt": "..." } } and the orchestrator will re-render via the locked image source.
15080
+
15081
+ Hard rules:
15082
+ - Touch only files you actually need to change. Do NOT rewrite the whole site.
15083
+ - new_content for HTML files: full document, DOCTYPE through </html>. Link /style.css unchanged unless explicitly told to restyle.
15084
+ - Do NOT change file paths for unchanged pages.
15085
+
15086
+ Schema:
15087
+ { "changes": [...], "summary": "one sentence" }
15088
+
15089
+ Respond with ONLY the JSON object.`;
15090
+
15091
+ export const modify_site_files = async (req) => {
15092
+ try {
15093
+ const { uid, current_files, prompt, attachments, plan_tier, image_source, account_profile_info } = req;
15094
+ if (!uid || !prompt) return { code: -1, data: 'uid + prompt required' };
15095
+ if (!Array.isArray(current_files) || !current_files.length) return { code: -1, data: 'current_files required' };
15096
+
15097
+ const ingestion = await ingest_attachments(attachments || []);
15098
+ const index = current_files.map((f) => ({
15099
+ path: f.path,
15100
+ content_type: f.content_type,
15101
+ size_b64: (f.content_b64 || '').length,
15102
+ }));
15103
+
15104
+ const diff_text = await _chat(uid, _modify_prompt(prompt, index, ingestion.manifest), account_profile_info, { stage: 'static_website_modify', plan_tier });
15105
+ const diff = _safe_json_parse(diff_text);
15106
+ if (!diff || !Array.isArray(diff.changes)) {
15107
+ return { code: -2, data: 'modify JSON malformed', diff_text };
15108
+ }
15109
+
15110
+ const changed_paths = [];
15111
+ const updated_files = [...current_files];
15112
+
15113
+ const _find = (p) => updated_files.findIndex((f) => f.path === p);
15114
+
15115
+ for (const ch of diff.changes) {
15116
+ // Re-rendered image
15117
+ if (ch.image_change && ch.image_change.path && ch.image_change.new_prompt) {
15118
+ const ip = ch.image_change.path;
15119
+ try {
15120
+ if (image_source === 'pexels') {
15121
+ const [img] = await pexels_search(ch.image_change.new_prompt, 1);
15122
+ if (img) {
15123
+ const entry = { path: ip, content_b64: img.content_b64, content_type: 'image/jpeg' };
15124
+ const idx = _find(ip);
15125
+ if (idx >= 0) updated_files[idx] = entry;
15126
+ else updated_files.push(entry);
15127
+ changed_paths.push(ip);
15128
+ }
15129
+ } else {
15130
+ const b64 = await create_image(uid, ch.image_change.new_prompt, image_source, '1024x1024', 1, 1024, 1024, { kind: 'static_website_modify' }, account_profile_info);
15131
+ if (b64) {
15132
+ const entry = { path: ip, content_b64: b64, content_type: 'image/png' };
15133
+ const idx = _find(ip);
15134
+ if (idx >= 0) updated_files[idx] = entry;
15135
+ else updated_files.push(entry);
15136
+ changed_paths.push(ip);
15137
+ }
15138
+ }
15139
+ } catch (err) {
15140
+ console.error('[modify_site_files] image regen fail', ip, err.message);
15141
+ }
15142
+ continue;
15143
+ }
15144
+
15145
+ if (!ch.path || !ch.action) continue;
15146
+ const idx = _find(ch.path);
15147
+
15148
+ if (ch.action === 'delete') {
15149
+ if (idx >= 0) {
15150
+ updated_files.splice(idx, 1);
15151
+ changed_paths.push(ch.path);
15152
+ }
15153
+ continue;
15154
+ }
15155
+
15156
+ if ((ch.action === 'replace' || ch.action === 'create') && typeof ch.new_content === 'string') {
15157
+ const ext = (ch.path.match(/\.([a-z0-9]+)$/i) || [, ''])[1].toLowerCase();
15158
+ const content_type = ext === 'html' ? 'text/html' : ext === 'css' ? 'text/css' : ext === 'js' ? 'application/javascript' : ext === 'json' ? 'application/json' : ext === 'xml' ? 'application/xml' : ext === 'txt' ? 'text/plain' : 'text/plain';
15159
+ const entry = {
15160
+ path: ch.path,
15161
+ content_b64: Buffer.from(ch.new_content, 'utf8').toString('base64'),
15162
+ content_type,
15163
+ };
15164
+ if (idx >= 0) updated_files[idx] = entry;
15165
+ else updated_files.push(entry);
15166
+ changed_paths.push(ch.path);
15167
+ }
15168
+ }
15169
+
15170
+ // Any new attachments uploaded this round join the file set verbatim.
15171
+ for (const entry of ingestion.file_entries) {
15172
+ const idx = _find(entry.path);
15173
+ if (idx >= 0) updated_files[idx] = entry;
15174
+ else updated_files.push(entry);
15175
+ changed_paths.push(entry.path);
15176
+ }
15177
+
15178
+ return {
15179
+ code: 1,
15180
+ data: {
15181
+ updated_files,
15182
+ changed_paths,
15183
+ summary: diff.summary || '',
15184
+ },
15185
+ };
15186
+ } catch (err) {
15187
+ return { code: -400, data: err.message };
15188
+ }
15189
+ };
package/index_ms.mjs CHANGED
@@ -17,6 +17,30 @@
17
17
 
18
18
 
19
19
 
20
+ export const pick_image_source = async function (...args) {
21
+ return await broker.send_to_queue("pick_image_source", ...args);
22
+ };
23
+
24
+ export const pexels_search = async function (...args) {
25
+ return await broker.send_to_queue("pexels_search", ...args);
26
+ };
27
+
28
+ export const estimate_static_website_cost = async function (...args) {
29
+ return await broker.send_to_queue("estimate_static_website_cost", ...args);
30
+ };
31
+
32
+ export const ingest_attachments = async function (...args) {
33
+ return await broker.send_to_queue("ingest_attachments", ...args);
34
+ };
35
+
36
+ export const generate_site_files = async function (...args) {
37
+ return await broker.send_to_queue("generate_site_files", ...args);
38
+ };
39
+
40
+ export const modify_site_files = async function (...args) {
41
+ return await broker.send_to_queue("modify_site_files", ...args);
42
+ };
43
+
20
44
  export const execute_codex_request = async function (...args) {
21
45
  return await broker.send_to_queue("execute_codex_request", ...args);
22
46
  };
@@ -193,6 +217,10 @@ export const is_spam_email = async function (...args) {
193
217
  return await broker.send_to_queue("is_spam_email", ...args);
194
218
  };
195
219
 
220
+ export const marketplace_review_item = async function (...args) {
221
+ return await broker.send_to_queue("marketplace_review_item", ...args);
222
+ };
223
+
196
224
  export const is_business_contact = async function (...args) {
197
225
  return await broker.send_to_queue("is_business_contact", ...args);
198
226
  };
@@ -260,7 +288,3 @@ export const widget_send_message = async function (...args) {
260
288
  export const widget_get_messages = async function (...args) {
261
289
  return await broker.send_to_queue("widget_get_messages", ...args);
262
290
  };
263
-
264
- export const marketplace_review_item = async function (...args) {
265
- return await broker.send_to_queue("marketplace_review_item", ...args);
266
- };
package/index_msa.mjs CHANGED
@@ -17,6 +17,30 @@
17
17
 
18
18
 
19
19
 
20
+ export const pick_image_source = function (...args) {
21
+ broker.send_to_queue_async("pick_image_source", ...args);
22
+ };
23
+
24
+ export const pexels_search = function (...args) {
25
+ broker.send_to_queue_async("pexels_search", ...args);
26
+ };
27
+
28
+ export const estimate_static_website_cost = function (...args) {
29
+ broker.send_to_queue_async("estimate_static_website_cost", ...args);
30
+ };
31
+
32
+ export const ingest_attachments = function (...args) {
33
+ broker.send_to_queue_async("ingest_attachments", ...args);
34
+ };
35
+
36
+ export const generate_site_files = function (...args) {
37
+ broker.send_to_queue_async("generate_site_files", ...args);
38
+ };
39
+
40
+ export const modify_site_files = function (...args) {
41
+ broker.send_to_queue_async("modify_site_files", ...args);
42
+ };
43
+
20
44
  export const execute_codex_request = function (...args) {
21
45
  broker.send_to_queue_async("execute_codex_request", ...args);
22
46
  };
@@ -193,6 +217,10 @@ export const is_spam_email = function (...args) {
193
217
  broker.send_to_queue_async("is_spam_email", ...args);
194
218
  };
195
219
 
220
+ export const marketplace_review_item = function (...args) {
221
+ broker.send_to_queue_async("marketplace_review_item", ...args);
222
+ };
223
+
196
224
  export const is_business_contact = function (...args) {
197
225
  broker.send_to_queue_async("is_business_contact", ...args);
198
226
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5614",
3
+ "version": "1.1.5615",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",