@xuda.io/ai_module 1.1.5637 → 1.1.5639
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 +241 -15
- package/index_ms.mjs +4 -0
- package/index_msa.mjs +4 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -1818,6 +1818,7 @@ export const generate_site_draft = async (req, job_id) => {
|
|
|
1818
1818
|
- Semantic HTML5; a clean CSS design system in its own stylesheet (sensible typography, spacing, color, mobile-first responsive); vanilla JS only where it adds value.
|
|
1819
1819
|
- Use RELATIVE asset paths (./styles.css, ./assets/...), never absolute (/...), so the site serves correctly from a sub-path.
|
|
1820
1820
|
${visuals_line}
|
|
1821
|
+
- FAVICON (required): create ./assets/favicon.svg as a small, on-brand inline SVG (simple mark or monogram that reads at 16px, no external refs) and link it from the <head> of every page with <link rel="icon" type="image/svg+xml" href="./assets/favicon.svg">. Also add <link rel="apple-touch-icon" href="./assets/favicon.svg">. Never leave the site without a favicon.
|
|
1821
1822
|
- NO build step or framework that needs compiling, plain static files that run as-is.
|
|
1822
1823
|
- SECURITY: work only inside this working directory (never read, reveal, or embed environment variables, credentials, config files, SSH keys, or anything outside this folder), and make no network requests.
|
|
1823
1824
|
When done, briefly summarize what you built.`;
|
|
@@ -4737,6 +4738,154 @@ export const diagnose_vps_snapshot = async function (req) {
|
|
|
4737
4738
|
}
|
|
4738
4739
|
};
|
|
4739
4740
|
|
|
4741
|
+
// Map a studio program's menuType to a friendly, user-facing noun so the
|
|
4742
|
+
// release-notes prompt talks about "screens" and "workflows" rather than the
|
|
4743
|
+
// internal type ids. Unknown types fall back to a de-underscored label.
|
|
4744
|
+
const _human_menu_type = function (t) {
|
|
4745
|
+
const map = {
|
|
4746
|
+
component: 'screen',
|
|
4747
|
+
workflow: 'workflow',
|
|
4748
|
+
table: 'data table',
|
|
4749
|
+
javascript: 'logic',
|
|
4750
|
+
js: 'logic',
|
|
4751
|
+
ai_agent: 'AI agent',
|
|
4752
|
+
api: 'API',
|
|
4753
|
+
style: 'style',
|
|
4754
|
+
function: 'function',
|
|
4755
|
+
query: 'query',
|
|
4756
|
+
report: 'report',
|
|
4757
|
+
};
|
|
4758
|
+
return map[t] || (t ? String(t).replace(/_/g, ' ') : 'item');
|
|
4759
|
+
};
|
|
4760
|
+
|
|
4761
|
+
// Generate user-facing release notes for a build the developer is about to
|
|
4762
|
+
// create. READ-ONLY: reads the project's PUBLISHED studio programs, diffs them
|
|
4763
|
+
// against the previous release's timestamp to see what changed since the last
|
|
4764
|
+
// build, and asks the model for a concise, plain-language changelog. Never
|
|
4765
|
+
// writes the release or mutates a program — the notes are returned for the
|
|
4766
|
+
// developer to review/edit before they hit Build. Credits are metered to the
|
|
4767
|
+
// caller via submit_chat_gpt_prompt -> record_ai_usage.
|
|
4768
|
+
// req: { app_id, version?, build_type?(major|minor|patch), instructions? }
|
|
4769
|
+
// -> { code:1, data:{ notes, changed_count, since_build, version } }
|
|
4770
|
+
// { code:-6, insufficient_credits:true } when the account is out of credits.
|
|
4771
|
+
export const generate_release_notes = async function (req) {
|
|
4772
|
+
const uid = req.uid || req.token_ret?.data?.uid;
|
|
4773
|
+
const app_id_in = req.app_id;
|
|
4774
|
+
const version = String(req.version || '').trim();
|
|
4775
|
+
const build_type = String(req.build_type || '').trim(); // major | minor | patch
|
|
4776
|
+
const steer = String(req.instructions || '')
|
|
4777
|
+
.trim()
|
|
4778
|
+
.slice(0, 400); // optional free-text hint from the developer
|
|
4779
|
+
|
|
4780
|
+
if (!uid) return { code: -1, data: 'not authorized' };
|
|
4781
|
+
if (!app_id_in) return { code: -1, data: 'app_id required' };
|
|
4782
|
+
|
|
4783
|
+
// Credit gate — usage is metered, but block up-front if the account is
|
|
4784
|
+
// already out (validate_credits_limit RETURNS the error, never throws).
|
|
4785
|
+
const credit_err = await validate_credits_limit(uid, null, _conf.release_notes?.model || _conf.default_ai_model, 'release notes');
|
|
4786
|
+
if (credit_err) return { code: -6, data: credit_err.message || 'insufficient credits', insufficient_credits: true };
|
|
4787
|
+
|
|
4788
|
+
// Resolve to the project (master) app — build/release data lives there.
|
|
4789
|
+
let app_obj;
|
|
4790
|
+
let app_ref;
|
|
4791
|
+
try {
|
|
4792
|
+
app_ref = await _common.get_project_app_id(app_id_in, true);
|
|
4793
|
+
const app_ret = await db_module.get_app_obj(app_ref);
|
|
4794
|
+
if (app_ret.code < 0 || !app_ret.data) return { code: -1, data: 'app not found' };
|
|
4795
|
+
app_obj = app_ret.data;
|
|
4796
|
+
} catch (e) {
|
|
4797
|
+
return { code: -1, data: 'app not found' };
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
const app_name = app_obj.app_name || 'the app';
|
|
4801
|
+
const prev_version = app_obj.app_publish_info?.version || '';
|
|
4802
|
+
const last_build = app_obj.app_publish_info?.app_lastBuild || 0;
|
|
4803
|
+
|
|
4804
|
+
// Cutoff = the previous release's timestamp. Any program whose `ts`
|
|
4805
|
+
// (last-saved time) is newer than this changed since that build.
|
|
4806
|
+
let since_ts = 0;
|
|
4807
|
+
try {
|
|
4808
|
+
const rel = await db_module.find_app_couch_query(app_ref, {
|
|
4809
|
+
selector: { docType: 'release', stat: { $lt: 4 } },
|
|
4810
|
+
sort: [{ build_id: 'desc' }],
|
|
4811
|
+
limit: 1,
|
|
4812
|
+
});
|
|
4813
|
+
since_ts = rel.docs?.[0]?.date || 0;
|
|
4814
|
+
} catch (e) {
|
|
4815
|
+
since_ts = 0;
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
// Published programs, projected to a tiny summary (bounds prompt tokens).
|
|
4819
|
+
let progs = [];
|
|
4820
|
+
try {
|
|
4821
|
+
const res = await db_module.find_app_couch_query(app_ref, {
|
|
4822
|
+
selector: { docType: 'studio', stat: 3 },
|
|
4823
|
+
fields: ['ts', 'properties.menuType', 'properties.menuName', 'properties.menuTitle'],
|
|
4824
|
+
limit: 99999,
|
|
4825
|
+
});
|
|
4826
|
+
progs = res.docs || [];
|
|
4827
|
+
} catch (e) {
|
|
4828
|
+
progs = [];
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4831
|
+
const first_build = !since_ts || last_build === 0;
|
|
4832
|
+
const changed = progs.filter((p) => p && (first_build || (p.ts && p.ts > since_ts))).sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
|
4833
|
+
|
|
4834
|
+
// Compact, deduped change list for the prompt (name + type; capped at 80).
|
|
4835
|
+
const seen = new Set();
|
|
4836
|
+
const items = [];
|
|
4837
|
+
for (const p of changed) {
|
|
4838
|
+
const name = p.properties?.menuTitle || p.properties?.menuName || 'Untitled';
|
|
4839
|
+
const type = _human_menu_type(p.properties?.menuType);
|
|
4840
|
+
const key = `${type}:${name}`;
|
|
4841
|
+
if (seen.has(key)) continue;
|
|
4842
|
+
seen.add(key);
|
|
4843
|
+
items.push({ name, type });
|
|
4844
|
+
if (items.length >= 80) break;
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4847
|
+
const by_type = {};
|
|
4848
|
+
for (const it of items) by_type[it.type] = (by_type[it.type] || 0) + 1;
|
|
4849
|
+
const type_summary = Object.entries(by_type)
|
|
4850
|
+
.map(([t, n]) => `${n} ${t}${n > 1 ? 's' : ''}`)
|
|
4851
|
+
.join(', ');
|
|
4852
|
+
|
|
4853
|
+
const parts = [
|
|
4854
|
+
`You are writing end-user release notes for a no-code app called "${app_name}" built on the Xuda platform.`,
|
|
4855
|
+
`The developer is publishing a new build to every deployment of this app. Write short, friendly, user-facing release notes that tell the people who USE the app what is new — in plain language, not technical jargon.`,
|
|
4856
|
+
'',
|
|
4857
|
+
'RULES:',
|
|
4858
|
+
'- Output ONLY 2 to 5 concise bullet points, each a single short line. No title, no preamble, no sign-off — bullets only.',
|
|
4859
|
+
'- Describe outcomes and improvements ("New School Info screen", "Faster class enrollment"), never internal file names or program types.',
|
|
4860
|
+
'- Group related changes into one bullet instead of listing every item.',
|
|
4861
|
+
'- Neutral, professional tone. Never invent features that the change list does not imply.',
|
|
4862
|
+
'- Write in the SAME language as the program names below (e.g. Hebrew names -> Hebrew notes).',
|
|
4863
|
+
steer ? `- The developer added this guidance — follow it: ${steer}` : '',
|
|
4864
|
+
'',
|
|
4865
|
+
`Version: ${prev_version ? prev_version + ' -> ' : ''}${version || '(new)'}${build_type ? ` (${build_type} release)` : ''}`,
|
|
4866
|
+
first_build ? 'This is the FIRST published build. Write a brief "initial release" style note highlighting the main areas of the app.' : `Programs changed since the last build (${type_summary || 'general updates'}):`,
|
|
4867
|
+
items.length ? items.map((it) => `- ${it.name} (${it.type})`).join('\n') : '(No specific program changes were detected — write a brief note about minor improvements and stability fixes.)',
|
|
4868
|
+
].filter(Boolean);
|
|
4869
|
+
|
|
4870
|
+
// account_profile_info is REQUIRED for usage to be metered: record_ai_usage
|
|
4871
|
+
// throws (and skips broadcast_credits, so the nav meter never moves) without
|
|
4872
|
+
// it. Fetch it like classify_external_app_scan / the chat flows do.
|
|
4873
|
+
const account_profile_info = await get_active_account_profile_info(uid);
|
|
4874
|
+
|
|
4875
|
+
const ret = await submit_chat_gpt_prompt({
|
|
4876
|
+
uid,
|
|
4877
|
+
prompt: parts.join('\n'),
|
|
4878
|
+
model: _conf.release_notes?.model || _conf.default_ai_model,
|
|
4879
|
+
metadata: { func: 'generate_release_notes', app_id: app_ref, version },
|
|
4880
|
+
account_profile_info,
|
|
4881
|
+
});
|
|
4882
|
+
if (ret.code < 0) return { code: -1, data: ret.data };
|
|
4883
|
+
|
|
4884
|
+
const notes = String(ret.data || '').trim();
|
|
4885
|
+
if (!notes) return { code: -1, data: 'no notes generated' };
|
|
4886
|
+
return { code: 1, data: { notes, changed_count: changed.length, since_build: last_build, version } };
|
|
4887
|
+
};
|
|
4888
|
+
|
|
4740
4889
|
function getFirstNWords(text, n = 10) {
|
|
4741
4890
|
// Split on whitespace, keep punctuation attached to words
|
|
4742
4891
|
const words = text.match(/\S+/g) || [];
|
|
@@ -5823,17 +5972,44 @@ Rules:
|
|
|
5823
5972
|
- If you do not need clarification, do not emit the block. Only emit it when answering is genuinely blocked on missing information.
|
|
5824
5973
|
`.trim();
|
|
5825
5974
|
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5975
|
+
// Find every top-level balanced [...] span in a string, skipping brackets that
|
|
5976
|
+
// appear inside JSON string literals (and their escapes). Used to recover a
|
|
5977
|
+
// questions payload the model emitted without any wrapper.
|
|
5978
|
+
const _scan_json_arrays = function (text) {
|
|
5979
|
+
const out = [];
|
|
5980
|
+
for (let i = 0; i < text.length; i++) {
|
|
5981
|
+
if (text[i] !== '[') continue;
|
|
5982
|
+
let depth = 0;
|
|
5983
|
+
let in_str = false;
|
|
5984
|
+
let esc = false;
|
|
5985
|
+
for (let j = i; j < text.length; j++) {
|
|
5986
|
+
const ch = text[j];
|
|
5987
|
+
if (in_str) {
|
|
5988
|
+
if (esc) esc = false;
|
|
5989
|
+
else if (ch === '\\') esc = true;
|
|
5990
|
+
else if (ch === '"') in_str = false;
|
|
5991
|
+
continue;
|
|
5992
|
+
}
|
|
5993
|
+
if (ch === '"') {
|
|
5994
|
+
in_str = true;
|
|
5995
|
+
continue;
|
|
5996
|
+
}
|
|
5997
|
+
if (ch === '[' || ch === '{') depth++;
|
|
5998
|
+
else if (ch === ']' || ch === '}') {
|
|
5999
|
+
depth--;
|
|
6000
|
+
if (depth <= 0) {
|
|
6001
|
+
if (depth === 0) out.push({ start: i, end: j + 1, raw: text.slice(i, j + 1) });
|
|
6002
|
+
i = j; // skip past this span so nested arrays are not re-scanned
|
|
6003
|
+
break;
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
5835
6007
|
}
|
|
5836
|
-
|
|
6008
|
+
return out;
|
|
6009
|
+
};
|
|
6010
|
+
|
|
6011
|
+
const _validate_xuda_questions = function (parsed) {
|
|
6012
|
+
if (!Array.isArray(parsed) || !parsed.length) return null;
|
|
5837
6013
|
const validated = parsed
|
|
5838
6014
|
.map((q) => {
|
|
5839
6015
|
if (!q || typeof q.question !== 'string') return null;
|
|
@@ -5847,11 +6023,61 @@ const extract_xuda_questions = function (text) {
|
|
|
5847
6023
|
return { question: q.question.trim(), options };
|
|
5848
6024
|
})
|
|
5849
6025
|
.filter(Boolean);
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
};
|
|
6026
|
+
return validated.length ? validated : null;
|
|
6027
|
+
};
|
|
6028
|
+
|
|
6029
|
+
const extract_xuda_questions = function (text) {
|
|
6030
|
+
if (typeof text !== 'string') return { prose: text, questions: null };
|
|
6031
|
+
|
|
6032
|
+
// Models do NOT reliably wrap the payload. In practice they drop the tags and
|
|
6033
|
+
// emit a bare array, or fence it as ```json, or add a closing remark AFTER the
|
|
6034
|
+
// block. The old pattern demanded the exact tags anchored to end-of-string, so
|
|
6035
|
+
// any of those shipped the raw JSON straight into the chat bubble. Accept the
|
|
6036
|
+
// three real-world shapes, most explicit first, and never anchor to the end:
|
|
6037
|
+
// take the LAST match and cut it out of the prose wherever it sits.
|
|
6038
|
+
const patterns = [
|
|
6039
|
+
/<xuda-questions>\s*([\s\S]+?)\s*<\/xuda-questions>/g,
|
|
6040
|
+
/```(?:xuda-questions|json)?\s*(\[[\s\S]*?\])\s*```/g,
|
|
6041
|
+
];
|
|
6042
|
+
|
|
6043
|
+
for (const re of patterns) {
|
|
6044
|
+
let m;
|
|
6045
|
+
let last = null;
|
|
6046
|
+
while ((m = re.exec(text)) !== null) last = m;
|
|
6047
|
+
if (!last) continue;
|
|
6048
|
+
let parsed;
|
|
6049
|
+
try {
|
|
6050
|
+
parsed = JSON.parse(last[1]);
|
|
6051
|
+
} catch (e) {
|
|
6052
|
+
continue;
|
|
6053
|
+
}
|
|
6054
|
+
const validated = _validate_xuda_questions(parsed);
|
|
6055
|
+
if (!validated) continue;
|
|
6056
|
+
const prose = (text.slice(0, last.index) + text.slice(last.index + last[0].length)).trim();
|
|
6057
|
+
return { prose, questions: validated };
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
// Last resort: an unwrapped array pasted straight into the prose, which is the
|
|
6061
|
+
// most common way this arrives. Regex cannot do this, because the nested
|
|
6062
|
+
// `options` arrays make a lazy match close on the inner "]" and a greedy one
|
|
6063
|
+
// swallow trailing prose, so scan for balanced brackets instead. Gated on the
|
|
6064
|
+
// structural keys plus full validation so ordinary JSON samples are ignored.
|
|
6065
|
+
const cands = _scan_json_arrays(text);
|
|
6066
|
+
for (let k = cands.length - 1; k >= 0; k--) {
|
|
6067
|
+
const cand = cands[k];
|
|
6068
|
+
if (!/"question"\s*:/.test(cand.raw) || !/"options"\s*:/.test(cand.raw)) continue;
|
|
6069
|
+
let parsed;
|
|
6070
|
+
try {
|
|
6071
|
+
parsed = JSON.parse(cand.raw);
|
|
6072
|
+
} catch (e) {
|
|
6073
|
+
continue;
|
|
6074
|
+
}
|
|
6075
|
+
const validated = _validate_xuda_questions(parsed);
|
|
6076
|
+
if (!validated) continue;
|
|
6077
|
+
return { prose: (text.slice(0, cand.start) + text.slice(cand.end)).trim(), questions: validated };
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6080
|
+
return { prose: text, questions: null };
|
|
5855
6081
|
};
|
|
5856
6082
|
|
|
5857
6083
|
// Resolve the structured context object passed by newer clients (ProjectAiPanel
|
package/index_ms.mjs
CHANGED
|
@@ -133,6 +133,10 @@ export const diagnose_vps_snapshot = async function (...args) {
|
|
|
133
133
|
return await broker.send_to_queue("diagnose_vps_snapshot", ...args);
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
export const generate_release_notes = async function (...args) {
|
|
137
|
+
return await broker.send_to_queue("generate_release_notes", ...args);
|
|
138
|
+
};
|
|
139
|
+
|
|
136
140
|
export const create_openai_conversation = async function (...args) {
|
|
137
141
|
return await broker.send_to_queue("create_openai_conversation", ...args);
|
|
138
142
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -133,6 +133,10 @@ export const diagnose_vps_snapshot = function (...args) {
|
|
|
133
133
|
broker.send_to_queue_async("diagnose_vps_snapshot", ...args);
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
export const generate_release_notes = function (...args) {
|
|
137
|
+
broker.send_to_queue_async("generate_release_notes", ...args);
|
|
138
|
+
};
|
|
139
|
+
|
|
136
140
|
export const create_openai_conversation = function (...args) {
|
|
137
141
|
broker.send_to_queue_async("create_openai_conversation", ...args);
|
|
138
142
|
};
|