sync: stale skill detection, sentry filtering, claim removal, review improvements

Stale Detection:
- Detect skills removed/moved from GitHub (3 consecutive 404s threshold)
- Hide stale skills from browse/search, show warning banner on detail page
- Serve cached files with isStale flag when GitHub returns 404
- Add stale-check crawler command for batch verification
- CLI shows warning when installing stale skills from cache

Sentry & Error Handling:
- Filter browser extension errors and add denyUrls
- Anti-inflation measures and sentinel recalibration for curation

Claim & Removal:
- Enhanced ClaimForm with repo-level removal support
- Add repo-removal-request API endpoint with tests
- Improved owner page with bilingual content

Review Pipeline:
- Review version and reviewer tracking in submit API
- Source format filter for pending reviews
- Updated review tests

Other:
- Updated i18n strings (en/fa)
- BrowseFilters improvements
- Dockerfile updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-03-12 05:59:26 +03:30
parent 447f9eff59
commit 4212b5ba47
31 changed files with 1590 additions and 58 deletions

View File

@@ -39,6 +39,7 @@ interface SkillFile {
content: string;
size: number;
isBinary: boolean;
fetchFailed?: boolean;
}
interface CachedFiles {
@@ -136,7 +137,7 @@ export async function GET(request: NextRequest) {
}
// Fetch skill folder contents from GitHub
const files = await fetchSkillFiles(
const { files, fetchFailureCount } = await fetchSkillFiles(
githubOwner,
githubRepo,
skillPath,
@@ -146,19 +147,22 @@ export async function GET(request: NextRequest) {
token
);
// === SAVE TO CACHE ===
// Prepare cached files structure
const filesToCache: CachedFiles = {
fetchedAt: new Date().toISOString(),
commitSha: commitSha || 'unknown',
totalSize: files.reduce((sum, f) => sum + f.size, 0),
items: files,
};
// === SAVE TO CACHE (only if all files fetched successfully) ===
if (fetchFailureCount > 0) {
console.warn(`[skill-files] Skipping cache for ${skillId}: ${fetchFailureCount} file(s) failed to fetch`);
} else {
const filesToCache: CachedFiles = {
fetchedAt: new Date().toISOString(),
commitSha: commitSha || 'unknown',
totalSize: files.reduce((sum, f) => sum + f.size, 0),
items: files.map(({ fetchFailed: _, ...rest }) => rest),
};
// Save to database (async, don't block response)
skillQueries.updateCachedFiles(db, skillId, filesToCache).catch((err) => {
console.error(`[skill-files] Failed to cache files for ${skillId}:`, err);
});
// Save to database (async, don't block response)
skillQueries.updateCachedFiles(db, skillId, filesToCache).catch((err) => {
console.error(`[skill-files] Failed to cache files for ${skillId}:`, err);
});
}
// Note: Download count is NOT incremented here.
// It should only be incremented in /api/skills/install after successful download.
@@ -180,6 +184,7 @@ export async function GET(request: NextRequest) {
downloadUrl: item.isBinary ? `https://raw.githubusercontent.com/${githubOwner}/${githubRepo}/${branch || 'main'}/${skillPath}/${item.path}` : undefined,
})),
fromCache: false,
...(fetchFailureCount > 0 ? { fetchFailures: fetchFailureCount } : {}),
}, {
headers: createRateLimitHeaders(rateLimitResult),
});
@@ -204,6 +209,44 @@ export async function GET(request: NextRequest) {
}
if (errorMessage.includes('404')) {
// Increment stale check (non-blocking) — after 3 consecutive 404s, skill is marked stale
const skillId404 = request.nextUrl.searchParams.get('id');
if (skillId404) {
skillQueries.incrementStaleCheck(db, skillId404).catch((err) => {
console.error(`[skill-files] Failed to increment stale check for ${skillId404}:`, err);
});
}
// If we have cached files in DB, serve them with stale warning instead of 404
if (skillId404) {
const skill404 = await skillQueries.getById(db, skillId404);
if (skill404?.cachedFiles) {
const cached = skill404.cachedFiles as CachedFiles;
return NextResponse.json({
skillId: skillId404,
githubOwner: skill404.githubOwner,
githubRepo: skill404.githubRepo,
skillPath: skill404.skillPath,
branch: skill404.branch || 'main',
sourceFormat: skill404.sourceFormat || 'skill.md',
files: cached.items.map(item => ({
name: item.name,
path: item.path,
type: 'file' as const,
size: item.size,
content: item.isBinary ? undefined : item.content,
downloadUrl: item.isBinary ? `https://raw.githubusercontent.com/${skill404.githubOwner}/${skill404.githubRepo}/${skill404.branch || 'main'}/${skill404.skillPath}/${item.path}` : undefined,
})),
fromCache: true,
cachedAt: cached.fetchedAt,
isStale: true,
staleWarning: 'This skill may have been removed from GitHub. Files served from cache.',
}, {
headers: createRateLimitHeaders(rateLimitResult),
});
}
}
return NextResponse.json(
{ error: 'Skill files not found on GitHub', code: 'GITHUB_NOT_FOUND' },
{ status: 404 }
@@ -314,7 +357,7 @@ async function fetchSkillFiles(
depth: number = 0,
headers: Record<string, string>,
token: string | null
): Promise<SkillFile[]> {
): Promise<{ files: SkillFile[]; fetchFailureCount: number }> {
// Phase 1: Collect all file entries from directory tree
const entries = await collectFileEntries(owner, repo, path, ref, depth, headers, token);
@@ -326,6 +369,7 @@ async function fetchSkillFiles(
// Phase 2: Fetch file contents in parallel
let totalSize = 0;
let fetchFailureCount = 0;
const tasks = entries.map((entry) => async (): Promise<SkillFile> => {
const { item, relativePath } = entry;
const isBinary = !isTextFile(item.name);
@@ -340,15 +384,18 @@ async function fetchSkillFiles(
content,
size: item.size,
isBinary: false,
fetchFailed: false,
};
} catch {
// Content fetch failed — return as binary with download URL
// Content fetch failed — mark as failed so we skip caching
fetchFailureCount++;
return {
name: item.name,
path: relativePath,
content: '',
size: item.size,
isBinary: true,
fetchFailed: true,
};
}
}
@@ -360,10 +407,12 @@ async function fetchSkillFiles(
content: '',
size: item.size,
isBinary: true,
fetchFailed: false,
};
});
return parallelLimit(tasks, PARALLEL_FETCH_LIMIT);
const files = await parallelLimit(tasks, PARALLEL_FETCH_LIMIT);
return { files, fetchFailureCount };
}
/**