
























@@ -2,8 +2,23 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
2233const EXACT_SEMVER_VERSION_RE =
44/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
5+const OPENCLAW_STABLE_CORRECTION_VERSION_RE =
6+/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-(?<correction>[1-9]\d*)$/;
7+const OPENCLAW_STABLE_VERSION_RE = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/;
8+const OPENCLAW_ALPHA_VERSION_RE =
9+/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-alpha\.(?<alpha>[1-9]\d*)$/;
10+const OPENCLAW_BETA_VERSION_RE =
11+/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/;
512const DIST_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
61314+type OpenClawReleaseVersion = {
15+channel: "alpha" | "beta" | "stable";
16+dateTime: number;
17+alphaNumber?: number;
18+betaNumber?: number;
19+correctionNumber?: number;
20+};
21+722export type ParsedRegistryNpmSpec = {
823name: string;
924raw: string;
@@ -74,7 +89,8 @@ function parseRegistryNpmSpecInternal(
7489raw: spec,
7590 selector,
7691selectorKind: "exact-version",
77-selectorIsPrerelease: Boolean(exactVersionMatch[4]),
92+selectorIsPrerelease:
93+Boolean(exactVersionMatch[4]) && !isOpenClawStableCorrectionVersion(selector),
7894},
7995};
8096}
@@ -110,9 +126,87 @@ export function isExactSemverVersion(value: string): boolean {
110126return EXACT_SEMVER_VERSION_RE.test(value.trim());
111127}
112128129+function parseOpenClawReleaseVersion(value: string): OpenClawReleaseVersion | null {
130+const trimmed = value.trim();
131+const candidates = [
132+{ match: OPENCLAW_STABLE_VERSION_RE.exec(trimmed), channel: "stable" as const },
133+{ match: OPENCLAW_STABLE_CORRECTION_VERSION_RE.exec(trimmed), channel: "stable" as const },
134+{ match: OPENCLAW_ALPHA_VERSION_RE.exec(trimmed), channel: "alpha" as const },
135+{ match: OPENCLAW_BETA_VERSION_RE.exec(trimmed), channel: "beta" as const },
136+];
137+const candidate = candidates.find((entry) => entry.match?.groups);
138+if (!candidate?.match?.groups) {
139+return null;
140+}
141+142+const year = Number.parseInt(candidate.match.groups.year ?? "", 10);
143+const month = Number.parseInt(candidate.match.groups.month ?? "", 10);
144+const day = Number.parseInt(candidate.match.groups.day ?? "", 10);
145+if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
146+return null;
147+}
148+const date = new Date(Date.UTC(year, month - 1, day));
149+if (
150+date.getUTCFullYear() !== year ||
151+date.getUTCMonth() !== month - 1 ||
152+date.getUTCDate() !== day
153+) {
154+return null;
155+}
156+157+const correctionNumber =
158+candidate.channel === "stable" && candidate.match.groups.correction
159+ ? Number.parseInt(candidate.match.groups.correction, 10)
160+ : undefined;
161+const alphaNumber =
162+candidate.channel === "alpha"
163+ ? Number.parseInt(candidate.match.groups.alpha ?? "", 10)
164+ : undefined;
165+const betaNumber =
166+candidate.channel === "beta"
167+ ? Number.parseInt(candidate.match.groups.beta ?? "", 10)
168+ : undefined;
169+170+return {
171+channel: candidate.channel,
172+dateTime: date.getTime(),
173+ correctionNumber,
174+ alphaNumber,
175+ betaNumber,
176+};
177+}
178+179+export function isOpenClawStableCorrectionVersion(value: string): boolean {
180+const parsed = parseOpenClawReleaseVersion(value);
181+return parsed?.channel === "stable" && parsed.correctionNumber !== undefined;
182+}
183+184+export function compareOpenClawReleaseVersions(left: string, right: string): number | null {
185+const parsedLeft = parseOpenClawReleaseVersion(left);
186+const parsedRight = parseOpenClawReleaseVersion(right);
187+if (!parsedLeft || !parsedRight) {
188+return null;
189+}
190+if (parsedLeft.dateTime !== parsedRight.dateTime) {
191+return parsedLeft.dateTime < parsedRight.dateTime ? -1 : 1;
192+}
193+if (parsedLeft.channel !== parsedRight.channel) {
194+const rank = { alpha: 0, beta: 1, stable: 2 };
195+return rank[parsedLeft.channel] < rank[parsedRight.channel] ? -1 : 1;
196+}
197+if (parsedLeft.channel === "alpha") {
198+return Math.sign((parsedLeft.alphaNumber ?? 0) - (parsedRight.alphaNumber ?? 0));
199+}
200+if (parsedLeft.channel === "beta") {
201+return Math.sign((parsedLeft.betaNumber ?? 0) - (parsedRight.betaNumber ?? 0));
202+}
203+return Math.sign((parsedLeft.correctionNumber ?? 0) - (parsedRight.correctionNumber ?? 0));
204+}
205+113206export function isPrereleaseSemverVersion(value: string): boolean {
114-const match = EXACT_SEMVER_VERSION_RE.exec(value.trim());
115-return Boolean(match?.[4]);
207+const trimmed = value.trim();
208+const match = EXACT_SEMVER_VERSION_RE.exec(trimmed);
209+return Boolean(match?.[4]) && !isOpenClawStableCorrectionVersion(trimmed);
116210}
117211118212export function isPrereleaseResolutionAllowed(params: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。