@xuda.io/ai_module 1.1.5660 → 1.1.5661

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
@@ -141,7 +141,7 @@ const xucode_verify = await import('./xucode_verify.mjs');
141
141
  // Where the provider credential lives. The machine gets a run token; this box keeps the key.
142
142
  const xucode_proxy = await import('./xucode_proxy.mjs');
143
143
  // The last gate of that loop: is the app still actually running. See xucode_preview.mjs.
144
- const { create_preview: _create_xucode_preview } = await import('./xucode_preview.mjs');
144
+ const { create_preview: _create_xucode_preview, preview_host: xucode_preview_host } = await import('./xucode_preview.mjs');
145
145
  const xucode_preview = _create_xucode_preview({});
146
146
  const xucode_runtime = _create_xucode_runtime({ run_process });
147
147
 
@@ -3904,6 +3904,54 @@ const xucode_prepare_engine = async function ({ uid, profile_id, engine, model,
3904
3904
  return { code: 1, launch, has_key: !!api_key, proxied: false, byok: key_ref.has_key };
3905
3905
  };
3906
3906
 
3907
+ // What the Code tab shows for a project's preview (plan 7).
3908
+ //
3909
+ // Deliberately does NOT wake a sleeping machine. A panel that polls would otherwise keep every
3910
+ // machine awake forever, which is the opposite of what sleep is for and would bill the customer
3911
+ // for a screen they left open. So a sleeping machine reports 'asleep' with its URL, and the next
3912
+ // run wakes it in the normal way.
3913
+ //
3914
+ // The URL is derived rather than stored, so it is always the one the router will resolve; there is
3915
+ // no second copy to drift. `listening` is the only part that needs the machine, and it is asked
3916
+ // for only when the machine is already up.
3917
+ export const xucode_preview_status = async function (req) {
3918
+ const { uid, profile_id, app_id } = req || {};
3919
+ try {
3920
+ if (!app_id) return { code: -1, data: 'app_id is required' };
3921
+ const host = xucode_preview_host(app_id);
3922
+ const port = xucode_preview.preview_port(app_id);
3923
+ const base = { url: host ? `https://${host}/` : null, port };
3924
+
3925
+ // `no_wake`: a status read must never resume a machine, or a panel left open keeps every
3926
+ // machine awake and bills the customer for a screen nobody is watching.
3927
+ const machine = await deploy_ms.xucode_machine_for({ uid, app_id, no_wake: true }).catch(() => null);
3928
+ if (!machine || machine.code < 0) {
3929
+ // Asleep, quarantined and "no machine at all" are three different answers, and the customer
3930
+ // deserves the right one: only the middle is a fault.
3931
+ const state = machine?.asleep ? 'asleep' : machine?.quarantined ? 'quarantined' : 'unavailable';
3932
+ return { code: 1, data: { ...base, state, why: String(machine?.data || 'this project has no machine yet') } };
3933
+ }
3934
+
3935
+ // The machine is up (xucode_machine_for refuses a quarantined one and wakes nothing that is
3936
+ // merely asleep beyond its own resume path), so it is safe to ask whether anything is serving.
3937
+ const dir = path.join(_conf.xucode?.projects_root || '/srv/xucode', String(app_id));
3938
+ let listening = false;
3939
+ try {
3940
+ const workspace = await xucode_runtime.acquire({ uid, project_id: app_id, app_id, dir });
3941
+ const status = await xucode_preview.status(workspace, app_id, port);
3942
+ listening = status?.listening === true;
3943
+ await workspace.release().catch(() => {});
3944
+ } catch (e) {
3945
+ return { code: 1, data: { ...base, state: 'unavailable', why: 'Xuda could not reach this project just now.' } };
3946
+ }
3947
+
3948
+ return { code: 1, data: { ...base, state: listening ? 'running' : 'stopped' } };
3949
+ } catch (err) {
3950
+ console.error(`[xucode] preview status failed: ${err?.message || err}`);
3951
+ return { code: -1, data: 'could not read the preview' };
3952
+ }
3953
+ };
3954
+
3907
3955
  export const xucode_engine_list = async function () {
3908
3956
  return { code: 1, data: { engines: xucode_engines.list_engines() } };
3909
3957
  };
@@ -4192,7 +4240,7 @@ export const xucode_run = async function (req) {
4192
4240
  // them. Only ever reached with `tracker.enforce` on, so the default remains observe.
4193
4241
  if (ret.record.enforced === true) {
4194
4242
  try {
4195
- const q = await deploy_msa.xucode_quarantine({ app_id, reasons: ret.record.reasons || [], run_id });
4243
+ const q = await deploy_msa.xucode_quarantine({ app_id, uid, reasons: ret.record.reasons || [], run_id });
4196
4244
  if (q?.code < 0) console.error(`[xucode] quarantine failed for ${app_id}: ${q.data}`);
4197
4245
  } catch (e) {
4198
4246
  console.error(`[xucode] quarantine threw for ${app_id}: ${e?.message || e}`);
@@ -4204,8 +4252,13 @@ export const xucode_run = async function (req) {
4204
4252
  // stopped: touching it would reset the idle clock on a machine we want left alone.
4205
4253
  if (!ret.killed && ret.record?.enforced !== true) {
4206
4254
  try {
4207
- await deploy_msa.xucode_touch({ app_id });
4208
- } catch (e) {}
4255
+ // `uid` matters here: `app_id` is the PROJECT, and touch operates on the MACHINE. Passing
4256
+ // the project alone is why this silently did nothing on every run until now.
4257
+ const touched = await deploy_msa.xucode_touch({ app_id, uid });
4258
+ if (touched?.code < 0) console.warn(`[xucode] could not refresh activity for ${app_id}: ${touched.data}`);
4259
+ } catch (e) {
4260
+ console.warn(`[xucode] touch threw for ${app_id}: ${e?.message || e}`);
4261
+ }
4209
4262
  }
4210
4263
 
4211
4264
  return ret;
package/index_ms.mjs CHANGED
@@ -81,6 +81,10 @@ export const xucode_key_delete = async function (...args) {
81
81
  return await broker.send_to_queue("xucode_key_delete", ...args);
82
82
  };
83
83
 
84
+ export const xucode_preview_status = async function (...args) {
85
+ return await broker.send_to_queue("xucode_preview_status", ...args);
86
+ };
87
+
84
88
  export const xucode_engine_list = async function (...args) {
85
89
  return await broker.send_to_queue("xucode_engine_list", ...args);
86
90
  };
package/index_msa.mjs CHANGED
@@ -81,6 +81,10 @@ export const xucode_key_delete = function (...args) {
81
81
  broker.send_to_queue_async("xucode_key_delete", ...args);
82
82
  };
83
83
 
84
+ export const xucode_preview_status = function (...args) {
85
+ broker.send_to_queue_async("xucode_preview_status", ...args);
86
+ };
87
+
84
88
  export const xucode_engine_list = function (...args) {
85
89
  broker.send_to_queue_async("xucode_engine_list", ...args);
86
90
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/ai_module",
3
- "version": "1.1.5660",
3
+ "version": "1.1.5661",
4
4
  "description": "Xuda AI Module",
5
5
  "main": "index.mjs",
6
6
  "type": "module",
@@ -39,6 +39,44 @@ export const preview_port = function (project_id, base = num('base_port', 4300),
39
39
  return base + (h % span);
40
40
  };
41
41
 
42
+ // The public hostname a project's preview answers on, and the reason it is a TEMPLATE.
43
+ //
44
+ // The obvious scheme is `<project>.preview.xuda.app`, which is what was asked for and what reads
45
+ // best. It does not work today, and the reason is worth writing down so nobody rediscovers it:
46
+ // Cloudflare's certificate for this zone covers `xuda.app` and `*.xuda.app`, and a DNS wildcard
47
+ // matches exactly ONE label. So `<project>.preview.xuda.app` fails the TLS handshake before any
48
+ // routing happens. Verified against the live edge: the single-label form answers, the two-label
49
+ // form dies with a handshake failure. Issuing a second-level wildcard needs Cloudflare's Advanced
50
+ // Certificate Manager, which is a paid per-zone add-on.
51
+ //
52
+ // So the default is a single label that the existing certificate already covers, and the scheme
53
+ // lives in config: the day that certificate exists, this becomes one config edit rather than a
54
+ // change to the router, the runtime and the tests.
55
+ //
56
+ // The project id is sanitized because it becomes a DNS label, and a label may hold only letters,
57
+ // digits and hyphens.
58
+ export const preview_host = function (project_id) {
59
+ const template = preview_conf().host_template;
60
+ if (!template) return null;
61
+ const label = String(project_id || '').replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
62
+ if (!label) return null;
63
+ return template.replace('{project}', label).toLowerCase();
64
+ };
65
+
66
+ // The inverse, for the router: given a hostname, which project is this a preview of. Returns null
67
+ // unless the host matches the configured template exactly, so a preview host can never be confused
68
+ // with the vps_web index, which resolves on the first label alone and would otherwise collide.
69
+ export const preview_project_from_host = function (hostname) {
70
+ const template = preview_conf().host_template;
71
+ if (!template || !hostname) return null;
72
+ const host = String(hostname).split(':')[0].trim().toLowerCase();
73
+ const [prefix, suffix] = template.toLowerCase().split('{project}');
74
+ if (!host.startsWith(prefix) || !host.endsWith(suffix)) return null;
75
+ const label = host.slice(prefix.length, host.length - suffix.length);
76
+ // One label only. Without this, `a.b-preview.xuda.app` would resolve as project `a.b`.
77
+ return /^[a-z0-9]+$/.test(label) ? label : null;
78
+ };
79
+
42
80
  // What starts this project. `dev` first because that is the one with hot reload, which is the
43
81
  // difference between a preview and a screenshot.
44
82
  export const detect_dev_command = function (manifest, package_manager = 'npm') {
package/xucode_vm.mjs CHANGED
@@ -37,6 +37,11 @@ export const shq = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`;
37
37
 
38
38
  const vm_conf = () => global._conf.xucode?.vm || {};
39
39
 
40
+ // The preview hostname scheme lives with the preview code, not here: this file only needs to hand
41
+ // back a URL, and duplicating the template is how the router and the runtime end up disagreeing
42
+ // about what a preview is called.
43
+ const { preview_host: _preview_host } = await import('./xucode_preview.mjs');
44
+
40
45
  export const create_vm_substrate = function ({ pve_request, resolve_machine, delay = (ms) => new Promise((r) => setTimeout(r, ms)), now = () => Date.now() }) {
41
46
  if (typeof pve_request !== 'function') throw new Error('create_vm_substrate needs pve_request');
42
47
  if (typeof resolve_machine !== 'function') throw new Error('create_vm_substrate needs resolve_machine');
@@ -249,9 +254,8 @@ export const create_vm_substrate = function ({ pve_request, resolve_machine, del
249
254
  // the one port that is ever reachable, and it goes through the platform's existing web
250
255
  // route for the app rather than a second proxy invented here.
251
256
  async expose_port(port) {
252
- const base = global._conf.xucode?.preview_domain;
253
- if (!base) return null;
254
- return { url: `https://${String(project_id).replace(/[^a-zA-Z0-9]/g, '')}.${base}`, port };
257
+ const host = _preview_host(project_id);
258
+ return host ? { url: `https://${host}/`, port } : null;
255
259
  },
256
260
 
257
261
  // The machine persists between runs, which is the whole point of the paid tiers, so