@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.
- package/connection/upgrade.js +102 -0
- package/package.json +1 -1
package/connection/upgrade.js
CHANGED
|
@@ -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,源码运行则不包含
|