@yeaft/webchat-agent 0.1.614 → 0.1.616

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.
@@ -59,6 +59,86 @@ export function handleRestartAgent() {
59
59
  cleanupAndExit(1);
60
60
  }
61
61
 
62
+ /**
63
+ * Fetch the `engines.node` SemVer range for a specific published version of
64
+ * a package. Returns the range string (e.g. ">=22.5.0") or `null` if the
65
+ * field is absent / the lookup fails. Failure is non-fatal — callers fall
66
+ * back to running the upgrade unconditionally rather than blocking on a
67
+ * registry hiccup.
68
+ */
69
+ async function fetchRequiredNodeRange(pkgName, version) {
70
+ try {
71
+ const stdout = await new Promise((resolve, reject) => {
72
+ execFile(
73
+ npmPath,
74
+ ['view', `${pkgName}@${version}`, 'engines.node'],
75
+ { stdio: 'pipe', env: safeEnv, ...shellOpt },
76
+ (err, out) => { if (err) reject(err); else resolve(out.toString().trim()); },
77
+ );
78
+ });
79
+ return stdout || null;
80
+ } catch (e) {
81
+ console.warn(`[Agent] Could not fetch engines.node for ${pkgName}@${version}:`, e.message);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Minimal SemVer range checker — supports the subset of operators that
88
+ * appear in real-world `engines.node` fields:
89
+ * - exact: "22.5.0"
90
+ * - comparator: ">=22.5.0", ">22", "<=24", "<25.0.0"
91
+ * - whitespace AND: ">=18.0.0 <23.0.0"
92
+ * - "||" OR: ">=18 <19 || >=20"
93
+ * - "*" / "" / "x": always satisfied
94
+ *
95
+ * We deliberately avoid pulling in the `semver` npm package — the agent has
96
+ * a minimal dep set and this gate only needs to reject obviously-wrong Node
97
+ * versions. Anything we can't parse is treated as "satisfied" (fail-open)
98
+ * so a weird range never blocks a legitimate upgrade.
99
+ */
100
+ export function nodeRangeSatisfied(current, range) {
101
+ if (!range || range === '*' || range === 'x' || range === 'X') return true;
102
+ const cur = parseSemver(current);
103
+ if (!cur) return true;
104
+ const orParts = String(range).split('||').map(s => s.trim()).filter(Boolean);
105
+ if (orParts.length === 0) return true;
106
+ return orParts.some(part => part.split(/\s+/).filter(Boolean).every(cmp => compareCmp(cur, cmp)));
107
+ }
108
+
109
+ function parseSemver(v) {
110
+ if (!v) return null;
111
+ const m = String(v).replace(/^v/, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
112
+ if (!m) return null;
113
+ return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
114
+ }
115
+
116
+ function cmpTuple(a, b) {
117
+ for (let i = 0; i < 3; i++) {
118
+ if (a[i] !== b[i]) return a[i] - b[i];
119
+ }
120
+ return 0;
121
+ }
122
+
123
+ function compareCmp(cur, cmp) {
124
+ const m = cmp.match(/^(>=|<=|>|<|=|\^|~)?\s*v?(.+)$/);
125
+ if (!m) return true; // unparseable → fail-open
126
+ const op = m[1] || '=';
127
+ const target = parseSemver(m[2]);
128
+ if (!target) return true;
129
+ const d = cmpTuple(cur, target);
130
+ switch (op) {
131
+ case '>=': return d >= 0;
132
+ case '<=': return d <= 0;
133
+ case '>': return d > 0;
134
+ case '<': return d < 0;
135
+ case '=': return d === 0;
136
+ case '^': return d >= 0 && cur[0] === target[0];
137
+ case '~': return d >= 0 && cur[0] === target[0] && cur[1] === target[1];
138
+ default: return true;
139
+ }
140
+ }
141
+
62
142
  export async function handleUpgradeAgent() {
63
143
  console.log('[Agent] Upgrade requested, checking for updates...');
64
144
  try {
@@ -74,6 +154,28 @@ export async function handleUpgradeAgent() {
74
154
  sendToServer({ type: 'upgrade_agent_ack', success: true, alreadyLatest: true, version: ctx.agentVersion });
75
155
  return;
76
156
  }
157
+
158
+ // Node.js compatibility gate: fetch engines.node of the *target* version
159
+ // and refuse to upgrade if the running Node is too old. Without this,
160
+ // npm install would replace files and the agent would crash on next
161
+ // restart with no actionable signal.
162
+ const requiredNode = await fetchRequiredNodeRange(pkgName, latestVersion);
163
+ const currentNode = process.versions.node;
164
+ if (requiredNode && !nodeRangeSatisfied(currentNode, requiredNode)) {
165
+ const msg = `Node ${currentNode} does not satisfy required ${requiredNode} for ${pkgName}@${latestVersion}`;
166
+ console.warn(`[Agent] Upgrade aborted: ${msg}`);
167
+ sendToServer({
168
+ type: 'upgrade_agent_ack',
169
+ success: false,
170
+ reason: 'node_incompatible',
171
+ error: msg,
172
+ currentNode,
173
+ requiredNode,
174
+ version: latestVersion,
175
+ });
176
+ return;
177
+ }
178
+
77
179
  console.log(`[Agent] Upgrading from ${ctx.agentVersion} to latest (${latestVersion})...`);
78
180
 
79
181
  // 检测安装方式:npm install 的路径包含 node_modules,源码运行则不包含
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.614",
3
+ "version": "0.1.616",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",