mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
* fix(og): render org profile images in publisher OG cards Pass publisher avatar, kind, and installs into OG meta URLs and allow safely fetching public HTTPS org logos when generating profile images. * fix: render org profile images in publisher OG cards Org logos use public HTTPS URLs outside the GitHub/gravatar allowlist, so OG generation fell back to the default mark. Allow SSRF-safe public fetches for publisher profile avatars and embed avatar/kind metadata in OG URLs. * fix(og): show Downloads instead of Installs on OG cards Switch skill, plugin, and publisher OG image generators to read and label download counts, with legacy installs query param fallback. * fix(og): type skill API payload for canonical stat reads Export SkillStatReadable so fetchSkillOgMeta can pass API skill stats through readCanonicalStat without a TypeScript error. * fix(og): format compact downloads in OG cards Query-param download counts were rendered as raw integers on publisher OG images. Reuse formatCompactStat, add download icon + lowercase label, bump layout versions, and refresh org profile visual proof. * fix(og): use Downloads label without icon on OG cards Remove the download SVG from OG stat blocks and show only a muted "Downloads" label above compact values. Bump skill/plugin/publisher layout versions to bust cached social previews.
107 lines
3.4 KiB
TypeScript
107 lines
3.4 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;
|
|
};
|
|
|
|
/**
|
|
* 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 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,
|
|
});
|
|
}
|