mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
feat: add VT pending repair command (#2441)
This commit is contained in:
@@ -4999,6 +4999,75 @@ describe("httpApiV1 handlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("VT pending repair requires admin role", async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
||||
userId: "users:moderator",
|
||||
user: { _id: "users:moderator", role: "moderator" },
|
||||
} as never);
|
||||
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
|
||||
if (isRateLimitArgs(args)) return okRate();
|
||||
throw new Error("should not repair");
|
||||
});
|
||||
|
||||
const response = await __handlers.skillsPostRouterV1Handler(
|
||||
makeCtx({ runMutation }),
|
||||
new Request("https://example.com/api/v1/skills/-/repair-vt-pending", {
|
||||
method: "POST",
|
||||
headers: { Authorization: "Bearer clh_test" },
|
||||
body: JSON.stringify({ batchSize: 25, dryRun: true }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
await expect(response.text()).resolves.toBe("Admin role required.");
|
||||
expect(runMutation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("VT pending repair invokes the internal repair action via admin API", async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
||||
userId: "users:admin",
|
||||
user: { _id: "users:admin", role: "admin" },
|
||||
} as never);
|
||||
const runAction = vi.fn(async () => ({
|
||||
dryRun: true,
|
||||
total: 2,
|
||||
wouldUpdate: 2,
|
||||
updated: 0,
|
||||
noResults: 0,
|
||||
noDecisiveStats: 0,
|
||||
errors: 0,
|
||||
done: false,
|
||||
cursor: "cursor-2",
|
||||
statusCounts: { clean: 2 },
|
||||
sampleUpdated: [{ slug: "demo", status: "clean" }],
|
||||
}));
|
||||
|
||||
const response = await __handlers.skillsPostRouterV1Handler(
|
||||
makeCtx({ runAction }),
|
||||
new Request("https://example.com/api/v1/skills/-/repair-vt-pending", {
|
||||
method: "POST",
|
||||
headers: { Authorization: "Bearer clh_test" },
|
||||
body: JSON.stringify({ batchSize: 25, cursor: null, dryRun: true }),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
wouldUpdate: 2,
|
||||
cursor: "cursor-2",
|
||||
});
|
||||
expect(runAction).toHaveBeenCalledWith(
|
||||
(internal as unknown as { vt: Record<string, unknown> }).vt.repairPendingSkillVtAnalysis,
|
||||
{
|
||||
dryRun: true,
|
||||
cursor: null,
|
||||
batchSize: 25,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("package rescan enqueues owner-authorized ClawScan jobs", async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
||||
userId: "users:1",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ApiV1SkillBulkRescanBatchRequestSchema,
|
||||
ApiV1SkillBulkRescanStatusRequestSchema,
|
||||
ApiV1SkillRepairVtPendingRequestSchema,
|
||||
SkillAppealRequestSchema,
|
||||
SkillAppealResolveRequestSchema,
|
||||
SkillReportTriageRequestSchema,
|
||||
@@ -282,6 +283,9 @@ const internalRefs = internal as unknown as {
|
||||
getBulkSkillRescanBatchStatusForAdminInternal: unknown;
|
||||
requestSkillRescanForUserInternal: unknown;
|
||||
};
|
||||
vt: {
|
||||
repairPendingSkillVtAnalysis: unknown;
|
||||
};
|
||||
skills: {
|
||||
getSecurityVerdictTargetInternal: unknown;
|
||||
reportSkillForUserInternal: unknown;
|
||||
@@ -301,6 +305,10 @@ async function runMutationRef<T>(ctx: ActionCtx, ref: unknown, args: unknown): P
|
||||
return (await ctx.runMutation(ref as never, args as never)) as T;
|
||||
}
|
||||
|
||||
async function runActionRef<T>(ctx: ActionCtx, ref: unknown, args: unknown): Promise<T> {
|
||||
return (await ctx.runAction(ref as never, args as never)) as T;
|
||||
}
|
||||
|
||||
function isDefinitiveSecurityStatus(
|
||||
status: NormalizedSecurityStatus | null | undefined,
|
||||
): status is "clean" | "suspicious" | "malicious" {
|
||||
@@ -2059,6 +2067,55 @@ export async function skillsPostRouterV1Handler(ctx: ActionCtx, request: Request
|
||||
const action = segments[1] ?? "";
|
||||
const slug = segments[0]?.trim().toLowerCase() ?? "";
|
||||
|
||||
if (segments[0] === "-" && segments[1] === "repair-vt-pending" && segments.length === 2) {
|
||||
const auth = await requireApiTokenUserOrResponse(ctx, request, rate.headers);
|
||||
if (!auth.ok) return auth.response;
|
||||
const admin = requireAdminOrResponse(auth.user, rate.headers);
|
||||
if (!admin.ok) return admin.response;
|
||||
try {
|
||||
const body = parseArk(
|
||||
ApiV1SkillRepairVtPendingRequestSchema,
|
||||
await request.json(),
|
||||
"Skill VT pending repair payload",
|
||||
) as {
|
||||
cursor?: string | null;
|
||||
batchSize?: number;
|
||||
concurrency?: number;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
const result = await runActionRef<
|
||||
| {
|
||||
dryRun: boolean;
|
||||
total: number;
|
||||
wouldUpdate: number;
|
||||
updated: number;
|
||||
noResults: number;
|
||||
noDecisiveStats: number;
|
||||
errors: number;
|
||||
done: boolean;
|
||||
cursor: string | null;
|
||||
statusCounts: Record<string, number>;
|
||||
sampleUpdated: Array<{ slug: string; status: string }>;
|
||||
}
|
||||
| { error: string }
|
||||
>(ctx, internalRefs.vt.repairPendingSkillVtAnalysis, {
|
||||
dryRun: body.dryRun !== false,
|
||||
cursor: body.cursor ?? null,
|
||||
...(body.batchSize !== undefined ? { batchSize: body.batchSize } : {}),
|
||||
...(body.concurrency !== undefined ? { concurrency: body.concurrency } : {}),
|
||||
});
|
||||
if ("error" in result) return text(result.error, 400, rate.headers);
|
||||
return json({ ok: true, ...result }, 200, rate.headers);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) return text("Invalid JSON", 400, rate.headers);
|
||||
return text(
|
||||
error instanceof Error ? error.message : "Skill VT pending repair failed",
|
||||
400,
|
||||
rate.headers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (segments[0] === "-" && segments[1] === "rescan-batch" && segments.length === 2) {
|
||||
const auth = await requireApiTokenUserOrResponse(ctx, request, rate.headers);
|
||||
if (!auth.ok) return auth.response;
|
||||
|
||||
+12
-5
@@ -1030,6 +1030,7 @@ export const repairPendingSkillVtAnalysis = internalAction({
|
||||
args: {
|
||||
dryRun: v.boolean(),
|
||||
batchSize: v.optional(v.number()),
|
||||
concurrency: v.optional(v.number()),
|
||||
cursor: v.optional(v.union(v.string(), v.null())),
|
||||
},
|
||||
handler: async (ctx, args): Promise<RepairPendingSkillVtAnalysisResult> => {
|
||||
@@ -1038,8 +1039,10 @@ export const repairPendingSkillVtAnalysis = internalAction({
|
||||
console.log("[vt:repairPendingSkillVt] VT_API_KEY not configured");
|
||||
return { error: "VT_API_KEY not configured" };
|
||||
}
|
||||
const vtApiKey = apiKey;
|
||||
|
||||
const batchSize = Math.max(1, Math.min(Math.floor(args.batchSize ?? 100), 500));
|
||||
const concurrency = Math.max(1, Math.min(Math.floor(args.concurrency ?? 16), 32));
|
||||
const pendingPage: {
|
||||
skills: PendingVTSkill[];
|
||||
cursor: string | null;
|
||||
@@ -1058,25 +1061,25 @@ export const repairPendingSkillVtAnalysis = internalAction({
|
||||
const statusCounts: Record<string, number> = {};
|
||||
const sampleUpdated: Array<{ slug: string; status: string }> = [];
|
||||
|
||||
for (const { skillId, versionId, sha256hash, slug } of skills) {
|
||||
async function repairSkill({ skillId, versionId, sha256hash, slug }: PendingVTSkill) {
|
||||
try {
|
||||
const vtResult = await checkExistingFile(apiKey, sha256hash);
|
||||
const vtResult = await checkExistingFile(vtApiKey, sha256hash);
|
||||
if (!vtResult) {
|
||||
noResults++;
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = vtResult.data.attributes.last_analysis_stats;
|
||||
const status = statusFromAvStats(stats);
|
||||
if (!status) {
|
||||
noDecisiveStats++;
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
wouldUpdate++;
|
||||
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
||||
if (sampleUpdated.length < 20) sampleUpdated.push({ slug, status });
|
||||
if (args.dryRun) continue;
|
||||
if (args.dryRun) return;
|
||||
|
||||
await ctx.runMutation(internal.skills.updateVersionScanResultsInternal, {
|
||||
versionId,
|
||||
@@ -1102,6 +1105,10 @@ export const repairPendingSkillVtAnalysis = internalAction({
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < skills.length; index += concurrency) {
|
||||
await Promise.all(skills.slice(index, index + concurrency).map(repairSkill));
|
||||
}
|
||||
|
||||
return {
|
||||
dryRun: args.dryRun,
|
||||
total: skills.length,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
cmdBanUser,
|
||||
cmdReclassifyBan,
|
||||
cmdRemediateAutobans,
|
||||
cmdRepairVtPendingSkills,
|
||||
cmdRescanAllSkills,
|
||||
cmdRescanSkill,
|
||||
cmdSetRole,
|
||||
@@ -642,6 +643,27 @@ function registerSkillModerationCommands(command: Command) {
|
||||
await cmdRescanAllSkills(opts, options, isInputAllowed());
|
||||
});
|
||||
|
||||
command
|
||||
.command("repair-vt-pending")
|
||||
.description("Repair stale pending VirusTotal skill cache by rechecking hashes")
|
||||
.option("--batch-size <n>", "Batch size; backend caps at 500", (value) =>
|
||||
Number.parseInt(value, 10),
|
||||
)
|
||||
.option(
|
||||
"--concurrency <n>",
|
||||
"Per-batch VirusTotal lookup concurrency; backend caps at 32",
|
||||
(value) => Number.parseInt(value, 10),
|
||||
)
|
||||
.option("--cursor <cursor>", "Resume from a backend pagination cursor")
|
||||
.option("--dry-run", "Check pending rows without writing VT cache updates")
|
||||
.option("--all", "Continue paging until the backend reports done")
|
||||
.option("--yes", "Skip confirmation for write runs")
|
||||
.option("--json", "Output JSON progress events")
|
||||
.action(async (options) => {
|
||||
const opts = await resolveGlobalOpts();
|
||||
await cmdRepairVtPendingSkills(opts, options, isInputAllowed());
|
||||
});
|
||||
|
||||
command
|
||||
.command("reports")
|
||||
.description("List skill reports for moderator review")
|
||||
|
||||
@@ -23,6 +23,7 @@ const {
|
||||
cmdBanUser,
|
||||
cmdReclassifyBan,
|
||||
cmdRemediateAutobans,
|
||||
cmdRepairVtPendingSkills,
|
||||
cmdRescanAllSkills,
|
||||
cmdRescanSkill,
|
||||
cmdSetRole,
|
||||
@@ -408,6 +409,142 @@ describe("cmdRescanAllSkills", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdRepairVtPendingSkills", () => {
|
||||
it("requires --yes for real runs when input is disabled", async () => {
|
||||
await expect(cmdRepairVtPendingSkills(makeGlobalOpts(), {}, false)).rejects.toThrow(/--yes/i);
|
||||
expect(httpMocks.apiRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("pages dry-runs without confirmation", async () => {
|
||||
httpMocks.apiRequest
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
total: 2,
|
||||
wouldUpdate: 2,
|
||||
updated: 0,
|
||||
noResults: 0,
|
||||
noDecisiveStats: 0,
|
||||
errors: 0,
|
||||
done: false,
|
||||
cursor: "cursor-2",
|
||||
statusCounts: { clean: 2 },
|
||||
sampleUpdated: [{ slug: "one", status: "clean" }],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
total: 1,
|
||||
wouldUpdate: 1,
|
||||
updated: 0,
|
||||
noResults: 0,
|
||||
noDecisiveStats: 0,
|
||||
errors: 0,
|
||||
done: true,
|
||||
cursor: null,
|
||||
statusCounts: { suspicious: 1 },
|
||||
sampleUpdated: [{ slug: "two", status: "suspicious" }],
|
||||
});
|
||||
|
||||
const result = await cmdRepairVtPendingSkills(
|
||||
makeGlobalOpts(),
|
||||
{ dryRun: true, batchSize: 2, all: true },
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
dryRun: true,
|
||||
batches: 2,
|
||||
total: 3,
|
||||
wouldUpdate: 3,
|
||||
updated: 0,
|
||||
statusCounts: { clean: 2, suspicious: 1 },
|
||||
});
|
||||
expect(httpMocks.apiRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"https://clawhub.ai",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
path: "/api/v1/skills/-/repair-vt-pending",
|
||||
token: "tkn",
|
||||
body: {
|
||||
cursor: null,
|
||||
batchSize: 2,
|
||||
dryRun: true,
|
||||
},
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(httpMocks.apiRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://clawhub.ai",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
path: "/api/v1/skills/-/repair-vt-pending",
|
||||
token: "tkn",
|
||||
body: {
|
||||
cursor: "cursor-2",
|
||||
batchSize: 2,
|
||||
dryRun: true,
|
||||
},
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("writes one batch when confirmed", async () => {
|
||||
httpMocks.apiRequest.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
dryRun: false,
|
||||
total: 2,
|
||||
wouldUpdate: 2,
|
||||
updated: 2,
|
||||
noResults: 0,
|
||||
noDecisiveStats: 0,
|
||||
errors: 0,
|
||||
done: false,
|
||||
cursor: "cursor-2",
|
||||
statusCounts: { clean: 1, malicious: 1 },
|
||||
sampleUpdated: [
|
||||
{ slug: "one", status: "clean" },
|
||||
{ slug: "two", status: "malicious" },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await cmdRepairVtPendingSkills(
|
||||
makeGlobalOpts(),
|
||||
{ yes: true, batchSize: 2 },
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
dryRun: false,
|
||||
batches: 1,
|
||||
total: 2,
|
||||
wouldUpdate: 2,
|
||||
updated: 2,
|
||||
nextCursor: "cursor-2",
|
||||
done: false,
|
||||
});
|
||||
expect(httpMocks.apiRequest).toHaveBeenCalledWith(
|
||||
"https://clawhub.ai",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
path: "/api/v1/skills/-/repair-vt-pending",
|
||||
token: "tkn",
|
||||
body: {
|
||||
cursor: null,
|
||||
batchSize: 2,
|
||||
dryRun: false,
|
||||
},
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdSetRole", () => {
|
||||
it("requires --yes when input is disabled", async () => {
|
||||
await expect(cmdSetRole(makeGlobalOpts(), "demo", "moderator", {}, false)).rejects.toThrow(
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ApiV1SetRoleResponseSchema,
|
||||
ApiV1SkillBulkRescanBatchResponseSchema,
|
||||
ApiV1SkillBulkRescanStatusResponseSchema,
|
||||
ApiV1SkillRepairVtPendingResponseSchema,
|
||||
ApiV1SkillRescanResponseSchema,
|
||||
ApiV1UnbanUserResponseSchema,
|
||||
ApiV1UserSearchResponseSchema,
|
||||
@@ -346,6 +347,121 @@ export async function cmdRescanAllSkills(
|
||||
return summary;
|
||||
}
|
||||
|
||||
export async function cmdRepairVtPendingSkills(
|
||||
opts: GlobalOpts,
|
||||
options: {
|
||||
batchSize?: number;
|
||||
concurrency?: number;
|
||||
cursor?: string;
|
||||
dryRun?: boolean;
|
||||
all?: boolean;
|
||||
yes?: boolean;
|
||||
json?: boolean;
|
||||
},
|
||||
inputAllowed: boolean,
|
||||
) {
|
||||
const batchSize = normalizePositiveInt(options.batchSize, 500);
|
||||
const concurrency =
|
||||
options.concurrency === undefined ? undefined : normalizePositiveInt(options.concurrency, 16);
|
||||
const dryRun = options.dryRun === true;
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false;
|
||||
|
||||
if (!dryRun && !options.yes) {
|
||||
if (!allowPrompt) fail("Pass --yes (no input)");
|
||||
const ok = await promptConfirm(
|
||||
`Repair pending VirusTotal cache in batches of ${batchSize} with concurrency ${concurrency ?? 16}? (admin)`,
|
||||
);
|
||||
if (!ok) return undefined;
|
||||
}
|
||||
|
||||
const token = await requireAuthToken();
|
||||
const registry = await getRegistry(opts, { cache: true });
|
||||
let cursor = options.cursor?.trim() || null;
|
||||
let batches = 0;
|
||||
let total = 0;
|
||||
let wouldUpdate = 0;
|
||||
let updated = 0;
|
||||
let noResults = 0;
|
||||
let noDecisiveStats = 0;
|
||||
let errors = 0;
|
||||
const statusCounts: Record<string, number> = {};
|
||||
const sampleUpdated: Array<{ slug: string; status: string }> = [];
|
||||
let done = false;
|
||||
|
||||
while (!done) {
|
||||
const result = await apiRequest(
|
||||
registry,
|
||||
{
|
||||
method: "POST",
|
||||
path: `${ApiRoutes.skills}/-/repair-vt-pending`,
|
||||
token,
|
||||
body: {
|
||||
cursor,
|
||||
batchSize,
|
||||
...(concurrency !== undefined ? { concurrency } : {}),
|
||||
dryRun,
|
||||
},
|
||||
},
|
||||
ApiV1SkillRepairVtPendingResponseSchema,
|
||||
);
|
||||
const batch = parseArk(
|
||||
ApiV1SkillRepairVtPendingResponseSchema,
|
||||
result,
|
||||
"Skill VT pending repair response",
|
||||
);
|
||||
batches++;
|
||||
total += batch.total;
|
||||
wouldUpdate += batch.wouldUpdate;
|
||||
updated += batch.updated;
|
||||
noResults += batch.noResults;
|
||||
noDecisiveStats += batch.noDecisiveStats;
|
||||
errors += batch.errors;
|
||||
for (const [status, count] of Object.entries(batch.statusCounts)) {
|
||||
statusCounts[status] = (statusCounts[status] ?? 0) + count;
|
||||
}
|
||||
for (const sample of batch.sampleUpdated) {
|
||||
if (sampleUpdated.length < 20) sampleUpdated.push(sample);
|
||||
}
|
||||
done = batch.done;
|
||||
emitVtRepairProgress(options, {
|
||||
type: "batch",
|
||||
batch: batches,
|
||||
cursor,
|
||||
nextCursor: batch.cursor,
|
||||
total: batch.total,
|
||||
wouldUpdate: batch.wouldUpdate,
|
||||
updated: batch.updated,
|
||||
noResults: batch.noResults,
|
||||
noDecisiveStats: batch.noDecisiveStats,
|
||||
errors: batch.errors,
|
||||
statusCounts: batch.statusCounts,
|
||||
done: batch.done,
|
||||
dryRun,
|
||||
});
|
||||
cursor = batch.cursor;
|
||||
if (!options.all || !cursor) break;
|
||||
}
|
||||
|
||||
const summary = {
|
||||
ok: errors === 0,
|
||||
dryRun,
|
||||
batches,
|
||||
total,
|
||||
wouldUpdate,
|
||||
updated,
|
||||
noResults,
|
||||
noDecisiveStats,
|
||||
errors,
|
||||
statusCounts,
|
||||
sampleUpdated,
|
||||
nextCursor: cursor,
|
||||
done,
|
||||
};
|
||||
emitVtRepairProgress(options, { type: "summary", ...summary });
|
||||
if (errors > 0) fail(`VT pending repair finished with ${errors} error(s)`);
|
||||
return summary;
|
||||
}
|
||||
|
||||
async function pollBulkRescanStatus(
|
||||
registry: string,
|
||||
token: string,
|
||||
@@ -410,6 +526,32 @@ function emitBulkRescanProgress(options: { json?: boolean }, event: Record<strin
|
||||
}
|
||||
}
|
||||
|
||||
function emitVtRepairProgress(options: { json?: boolean }, event: Record<string, unknown>) {
|
||||
if (options.json) {
|
||||
process.stdout.write(`${JSON.stringify(event)}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "batch") {
|
||||
const nextCursor = typeof event.nextCursor === "string" ? event.nextCursor : null;
|
||||
const suffix = nextCursor ? ` Next cursor: ${nextCursor}.` : "";
|
||||
console.log(
|
||||
`VT repair batch ${readEventNumber(event, "batch")}: scanned ${readEventNumber(event, "total")}, ${event.dryRun ? "would update" : "updated"} ${readEventNumber(event, event.dryRun ? "wouldUpdate" : "updated")}, no results ${readEventNumber(event, "noResults")}, errors ${readEventNumber(event, "errors")}.${suffix}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "summary") {
|
||||
const label = event.dryRun ? "VT repair dry run" : "VT repair";
|
||||
console.log(
|
||||
`${label} finished: ${readEventNumber(event, "batches")} batch(es), scanned ${readEventNumber(event, "total")}, ${event.dryRun ? "would update" : "updated"} ${readEventNumber(event, event.dryRun ? "wouldUpdate" : "updated")}, no results ${readEventNumber(event, "noResults")}, errors ${readEventNumber(event, "errors")}.`,
|
||||
);
|
||||
if (typeof event.nextCursor === "string" && event.nextCursor) {
|
||||
console.log(`Resume cursor: ${event.nextCursor}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readEventNumber(event: Record<string, unknown>, key: string) {
|
||||
const value = event[key];
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||
|
||||
@@ -470,6 +470,35 @@ export const ApiV1SkillBulkRescanStatusResponseSchema = type({
|
||||
export type ApiV1SkillBulkRescanStatusResponse =
|
||||
(typeof ApiV1SkillBulkRescanStatusResponseSchema)[inferred];
|
||||
|
||||
export const ApiV1SkillRepairVtPendingRequestSchema = type({
|
||||
cursor: "string|null?",
|
||||
batchSize: "number?",
|
||||
concurrency: "number?",
|
||||
dryRun: "boolean?",
|
||||
});
|
||||
export type ApiV1SkillRepairVtPendingRequest =
|
||||
(typeof ApiV1SkillRepairVtPendingRequestSchema)[inferred];
|
||||
|
||||
export const ApiV1SkillRepairVtPendingResponseSchema = type({
|
||||
ok: "true",
|
||||
dryRun: "boolean",
|
||||
total: "number",
|
||||
wouldUpdate: "number",
|
||||
updated: "number",
|
||||
noResults: "number",
|
||||
noDecisiveStats: "number",
|
||||
errors: "number",
|
||||
done: "boolean",
|
||||
cursor: "string|null",
|
||||
statusCounts: { "[string]": "number" },
|
||||
sampleUpdated: type({
|
||||
slug: "string",
|
||||
status: "string",
|
||||
}).array(),
|
||||
});
|
||||
export type ApiV1SkillRepairVtPendingResponse =
|
||||
(typeof ApiV1SkillRepairVtPendingResponseSchema)[inferred];
|
||||
|
||||
export const ApiV1SkillVersionListResponseSchema = type({
|
||||
items: type({
|
||||
version: "string",
|
||||
|
||||
Vendored
+27
@@ -406,6 +406,33 @@ export declare const ApiV1SkillBulkRescanStatusResponseSchema: import("arktype/i
|
||||
failedJobIds: string[];
|
||||
}, {}>;
|
||||
export type ApiV1SkillBulkRescanStatusResponse = (typeof ApiV1SkillBulkRescanStatusResponseSchema)[inferred];
|
||||
export declare const ApiV1SkillRepairVtPendingRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
cursor?: string | null | undefined;
|
||||
batchSize?: number | undefined;
|
||||
concurrency?: number | undefined;
|
||||
dryRun?: boolean | undefined;
|
||||
}, {}>;
|
||||
export type ApiV1SkillRepairVtPendingRequest = (typeof ApiV1SkillRepairVtPendingRequestSchema)[inferred];
|
||||
export declare const ApiV1SkillRepairVtPendingResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
ok: true;
|
||||
dryRun: boolean;
|
||||
total: number;
|
||||
wouldUpdate: number;
|
||||
updated: number;
|
||||
noResults: number;
|
||||
noDecisiveStats: number;
|
||||
errors: number;
|
||||
done: boolean;
|
||||
cursor: string | null;
|
||||
statusCounts: {
|
||||
[x: string]: number;
|
||||
};
|
||||
sampleUpdated: {
|
||||
slug: string;
|
||||
status: string;
|
||||
}[];
|
||||
}, {}>;
|
||||
export type ApiV1SkillRepairVtPendingResponse = (typeof ApiV1SkillRepairVtPendingResponseSchema)[inferred];
|
||||
export declare const ApiV1SkillVersionListResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
items: {
|
||||
version: string;
|
||||
|
||||
Vendored
+23
@@ -365,6 +365,29 @@ export const ApiV1SkillBulkRescanStatusResponseSchema = type({
|
||||
done: "boolean",
|
||||
failedJobIds: "string[]",
|
||||
});
|
||||
export const ApiV1SkillRepairVtPendingRequestSchema = type({
|
||||
cursor: "string|null?",
|
||||
batchSize: "number?",
|
||||
concurrency: "number?",
|
||||
dryRun: "boolean?",
|
||||
});
|
||||
export const ApiV1SkillRepairVtPendingResponseSchema = type({
|
||||
ok: "true",
|
||||
dryRun: "boolean",
|
||||
total: "number",
|
||||
wouldUpdate: "number",
|
||||
updated: "number",
|
||||
noResults: "number",
|
||||
noDecisiveStats: "number",
|
||||
errors: "number",
|
||||
done: "boolean",
|
||||
cursor: "string|null",
|
||||
statusCounts: { "[string]": "number" },
|
||||
sampleUpdated: type({
|
||||
slug: "string",
|
||||
status: "string",
|
||||
}).array(),
|
||||
});
|
||||
export const ApiV1SkillVersionListResponseSchema = type({
|
||||
items: type({
|
||||
version: "string",
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -442,6 +442,35 @@ export const ApiV1SkillBulkRescanStatusResponseSchema = type({
|
||||
export type ApiV1SkillBulkRescanStatusResponse =
|
||||
(typeof ApiV1SkillBulkRescanStatusResponseSchema)[inferred];
|
||||
|
||||
export const ApiV1SkillRepairVtPendingRequestSchema = type({
|
||||
cursor: "string|null?",
|
||||
batchSize: "number?",
|
||||
concurrency: "number?",
|
||||
dryRun: "boolean?",
|
||||
});
|
||||
export type ApiV1SkillRepairVtPendingRequest =
|
||||
(typeof ApiV1SkillRepairVtPendingRequestSchema)[inferred];
|
||||
|
||||
export const ApiV1SkillRepairVtPendingResponseSchema = type({
|
||||
ok: "true",
|
||||
dryRun: "boolean",
|
||||
total: "number",
|
||||
wouldUpdate: "number",
|
||||
updated: "number",
|
||||
noResults: "number",
|
||||
noDecisiveStats: "number",
|
||||
errors: "number",
|
||||
done: "boolean",
|
||||
cursor: "string|null",
|
||||
statusCounts: { "[string]": "number" },
|
||||
sampleUpdated: type({
|
||||
slug: "string",
|
||||
status: "string",
|
||||
}).array(),
|
||||
});
|
||||
export type ApiV1SkillRepairVtPendingResponse =
|
||||
(typeof ApiV1SkillRepairVtPendingResponseSchema)[inferred];
|
||||
|
||||
export const ApiV1SkillVersionListResponseSchema = type({
|
||||
items: type({
|
||||
version: "string",
|
||||
|
||||
Reference in New Issue
Block a user