mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
* feat: add staged skills.sh mirror storage * ci: allow guarded CLAW-563 Test deploy * ci: expose guarded Test deploy diagnostics * ci: defer branch guard to deploy step * ci: deploy CLAW-563 PR head to Test * ci: admit CLAW-563 PR Test job * fix: make mirror source recovery durable * ci: trigger labeled mirror load * feat: activate mirror search queries * fix: tighten mirror source typing * fix: bypass protected Test mirror proof * feat: attribute skill metrics by source * feat: present stars as bookmarks * style: format mirror proof changes * fix: bypass protected mirror readback * fix: resume mirror past missing scanner pages * fix: fetch skills.sh mirror audits from api * fix: validate structural skills.sh identities * fix: resolve ambiguous skills.sh mirror identities * feat: stabilize skills.sh mirror ingestion * fix: account mirror identity conflicts in proof * fix: quarantine invalid skills.sh detail ids * fix: resume skills.sh mirror proof * fix: preserve skills.sh mirror provenance * fix: recover exact skills.sh mirror runs * fix: recover stale skills.sh mirror runs * fix: normalize skills.sh mirror topic facets * feat: prove complete skills.sh leaderboard mirror * fix: canonicalize skills.sh source page hashes * test: enable skills.sh rollout in mirror tests * ci: skip unrelated Test deploy pull requests * fix: preserve Vercel preview marker in Test deploy * fix: tighten Test deploy and metric reconciliation * fix: bound mirror detail proof pages * fix: delegate controlled mirror rate limits * fix: preserve mirror reconciliation progress * fix: release mirror retry responses * fix: preserve stale mirror replay state * fix: authenticate mirror source starts * fix: delegate mirror identity rate limits * ci: trigger mirror proof when labeled * ci: couple mirror deploy and proof opt-in * fix: admit permanent Vercel Test runtime * fix: pass Test target to Vercel runtime * test: align bookmark sync browser labels * fix: preserve skills.sh source accounting * fix: preflight active mirror runs * fix: bind mirror snapshot accounting * fix: reject truncated replay hashes * fix: preserve live mirror overlay metadata
145 lines
4.7 KiB
TypeScript
145 lines
4.7 KiB
TypeScript
import type { Doc, Id } from "../_generated/dataModel";
|
|
import type { MutationCtx } from "../_generated/server";
|
|
import { toDayKey } from "./leaderboards";
|
|
|
|
type SkillStatDeltas = {
|
|
downloads?: number;
|
|
stars?: number;
|
|
installsCurrent?: number;
|
|
installsAllTime?: number;
|
|
};
|
|
|
|
export type SkillStatReadable = {
|
|
stats: Partial<
|
|
Pick<Doc<"skills">["stats"], "downloads" | "stars" | "installsCurrent" | "installsAllTime">
|
|
>;
|
|
statsDownloads?: number;
|
|
statsStars?: number;
|
|
statsInstallsCurrent?: number;
|
|
statsInstallsAllTime?: number;
|
|
statsSkillsShInstalls?: number;
|
|
statsGithubStars?: number;
|
|
};
|
|
|
|
type ExternalSkillMetricSnapshot = {
|
|
skillsShInstalls?: number;
|
|
githubStars?: number;
|
|
};
|
|
|
|
function nonNegativeCount(value: number | undefined): number {
|
|
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
return Math.max(0, Math.trunc(value));
|
|
}
|
|
|
|
/**
|
|
* Read the canonical value of a migrated stat field from a skill document.
|
|
*
|
|
* Top-level fields (`statsDownloads`, etc.) are the source of truth — they are
|
|
* indexable and kept up-to-date by the event pipeline. The nested `stats.*`
|
|
* fields are only used as a fallback for pre-migration documents where the
|
|
* top-level field is still `undefined`.
|
|
*
|
|
* All code that reads a migrated stat value should go through this function
|
|
* rather than accessing `skill.stats.*` directly.
|
|
*/
|
|
export function readCanonicalStat(
|
|
skill: SkillStatReadable,
|
|
field: "downloads" | "stars" | "installsCurrent" | "installsAllTime",
|
|
): number {
|
|
const topLevelKey = `stats${field[0].toUpperCase()}${field.slice(1)}` as
|
|
| "statsDownloads"
|
|
| "statsStars"
|
|
| "statsInstallsCurrent"
|
|
| "statsInstallsAllTime";
|
|
return typeof skill[topLevelKey] === "number" ? skill[topLevelKey]! : (skill.stats[field] ?? 0);
|
|
}
|
|
|
|
export function readSkillMetricSources(skill: SkillStatReadable) {
|
|
return {
|
|
clawHubDownloads: readCanonicalStat(skill, "downloads"),
|
|
skillsShInstalls: nonNegativeCount(skill.statsSkillsShInstalls),
|
|
openClawInstallsCurrent: readCanonicalStat(skill, "installsCurrent"),
|
|
openClawInstallsAllTime: readCanonicalStat(skill, "installsAllTime"),
|
|
githubStars: nonNegativeCount(skill.statsGithubStars),
|
|
bookmarks: readCanonicalStat(skill, "stars"),
|
|
};
|
|
}
|
|
|
|
export function readPublicDownloads(skill: SkillStatReadable): number {
|
|
return readCanonicalStat(skill, "downloads") + nonNegativeCount(skill.statsSkillsShInstalls);
|
|
}
|
|
|
|
export function buildExternalSkillMetricPatch(snapshot: ExternalSkillMetricSnapshot) {
|
|
return {
|
|
...(snapshot.skillsShInstalls === undefined
|
|
? {}
|
|
: { statsSkillsShInstalls: nonNegativeCount(snapshot.skillsShInstalls) }),
|
|
...(snapshot.githubStars === undefined
|
|
? {}
|
|
: { statsGithubStars: nonNegativeCount(snapshot.githubStars) }),
|
|
};
|
|
}
|
|
|
|
export function applySkillStatDeltas(skill: Doc<"skills">, deltas: SkillStatDeltas) {
|
|
const currentDownloads = readCanonicalStat(skill, "downloads");
|
|
const currentStars = readCanonicalStat(skill, "stars");
|
|
const currentInstallsCurrent = readCanonicalStat(skill, "installsCurrent");
|
|
const currentInstallsAllTime = readCanonicalStat(skill, "installsAllTime");
|
|
|
|
const nextDownloads = Math.max(0, currentDownloads + (deltas.downloads ?? 0));
|
|
const nextStars = Math.max(0, currentStars + (deltas.stars ?? 0));
|
|
const nextInstallsCurrent = Math.max(0, currentInstallsCurrent + (deltas.installsCurrent ?? 0));
|
|
const nextInstallsAllTime = Math.max(0, currentInstallsAllTime + (deltas.installsAllTime ?? 0));
|
|
|
|
return {
|
|
statsDownloads: nextDownloads,
|
|
statsStars: nextStars,
|
|
statsInstallsCurrent: nextInstallsCurrent,
|
|
statsInstallsAllTime: nextInstallsAllTime,
|
|
stats: {
|
|
...skill.stats,
|
|
downloads: nextDownloads,
|
|
stars: nextStars,
|
|
installsCurrent: nextInstallsCurrent,
|
|
installsAllTime: nextInstallsAllTime,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function bumpDailySkillStats(
|
|
ctx: MutationCtx,
|
|
params: {
|
|
skillId: Id<"skills">;
|
|
now: number;
|
|
downloads?: number;
|
|
installs?: number;
|
|
},
|
|
) {
|
|
const downloads = params.downloads ?? 0;
|
|
const installs = params.installs ?? 0;
|
|
if (downloads === 0 && installs === 0) return;
|
|
|
|
const day = toDayKey(params.now);
|
|
const existing = await ctx.db
|
|
.query("skillDailyStats")
|
|
.withIndex("by_skill_day", (q) => q.eq("skillId", params.skillId).eq("day", day))
|
|
.unique();
|
|
|
|
if (existing) {
|
|
await ctx.db.patch(existing._id, {
|
|
downloads: Math.max(0, existing.downloads + downloads),
|
|
installs: Math.max(0, existing.installs + installs),
|
|
updatedAt: params.now,
|
|
});
|
|
return;
|
|
}
|
|
|
|
await ctx.db.insert("skillDailyStats", {
|
|
skillId: params.skillId,
|
|
day,
|
|
downloads: Math.max(0, downloads),
|
|
installs: Math.max(0, installs),
|
|
updatedAt: params.now,
|
|
});
|
|
}
|