feat: M4-06 — project center with Gitea API integration
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 41s

- GiteaService: query repos, milestones, issues, releases, runners
- ProjectCenterController: 5 AAPI endpoints
- Replace iframe-only GiteaEmbed with full ProjectCenter page
  (repos table, issues, milestones cards, releases, runners, Gitea embed tab)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 18:06:19 +08:00
parent b188988e82
commit 8abf94872a
4 changed files with 118 additions and 0 deletions

View File

@ -54,6 +54,7 @@ import { CacheModule } from './common/cache/cache.module';
import { AdminCacheModule } from './modules/admin-cache/admin-cache.module'; import { AdminCacheModule } from './modules/admin-cache/admin-cache.module';
import { BackupModule } from './modules/backup/backup.module'; import { BackupModule } from './modules/backup/backup.module';
import { ReportingModule } from './modules/reporting/reporting.module'; import { ReportingModule } from './modules/reporting/reporting.module';
import { ProjectCenterModule } from './modules/project-center/project-center.module';
import { JwtAuthGuard } from './common/guards/jwt-auth.guard'; import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
import { RolesGuard } from './common/guards/roles.guard'; import { RolesGuard } from './common/guards/roles.guard';
@ -153,6 +154,7 @@ import appleConfig from './config/apple.config';
AdminCacheModule, AdminCacheModule,
BackupModule, BackupModule,
ReportingModule, ReportingModule,
ProjectCenterModule,
], ],
providers: [ providers: [
{ provide: APP_GUARD, useClass: RateLimitGuard }, { provide: APP_GUARD, useClass: RateLimitGuard },

View File

@ -0,0 +1,58 @@
import { Injectable, Logger } from '@nestjs/common';
const GITEA_BASE = 'https://git.admin.longde.cloud/api/v1';
const GITEA_TOKEN = process.env.GITEA_TOKEN || '0b190ab69b751c6f4ce3f92cbd3ed762e5cd5bfa';
@Injectable()
export class GiteaService {
private readonly logger = new Logger(GiteaService.name);
private async giteaGet<T>(path: string): Promise<T | null> {
try {
const res = await fetch(`${GITEA_BASE}${path}`, {
headers: { Authorization: `token ${GITEA_TOKEN}`, Accept: 'application/json' },
});
if (!res.ok) { this.logger.warn(`Gitea GET ${path}: ${res.status}`); return null; }
return await res.json() as T;
} catch (err: any) {
this.logger.warn(`Gitea GET ${path} failed: ${err.message}`);
return null;
}
}
async getRepos() {
const repos = await this.giteaGet<any[]>('/repos/search?limit=50');
if (!repos) return [];
return Promise.all(repos.map(async (r: any) => {
const [issues, pulls, milestones] = await Promise.all([
this.giteaGet<any[]>(`/repos/${r.full_name}/issues?state=all&limit=1`).then(d => d?.length ?? 0).catch(() => 0),
this.giteaGet<any[]>(`/repos/${r.full_name}/pulls?state=all&limit=1`).then(d => d?.length ?? 0).catch(() => 0),
this.giteaGet<any[]>(`/repos/${r.full_name}/milestones?state=open`).catch(() => []),
]);
return {
id: r.id, name: r.name, fullName: r.full_name, description: r.description,
owner: r.owner?.login, stars: r.stars_count, forks: r.forks_count,
openIssues: r.open_issues_count, openPulls: pulls, milestones: milestones?.length ?? 0,
defaultBranch: r.default_branch, updatedAt: r.updated_at,
};
}));
}
async getMilestones(owner: string, repo: string) {
return this.giteaGet<any[]>(`/repos/${owner}/${repo}/milestones?state=all`) ?? [];
}
async getIssues(owner: string, repo: string, milestone?: string, state = 'open') {
let path = `/repos/${owner}/${repo}/issues?state=${state}&limit=50`;
if (milestone) path += `&milestone=${milestone}`;
return this.giteaGet<any[]>(path) ?? [];
}
async getRunners() {
return this.giteaGet<any[]>('/admin/runners') ?? [];
}
async getReleases(owner: string, repo: string) {
return this.giteaGet<any[]>(`/repos/${owner}/${repo}/releases?limit=20`) ?? [];
}
}

View File

@ -0,0 +1,48 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { GiteaService } from './gitea.service';
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
@ApiTags('admin-project-center')
@ApiBearerAuth()
@Controller('admin-api/projects')
@UseGuards(AdminAuthGuard, AdminRolesGuard)
export class ProjectCenterController {
constructor(private readonly gitea: GiteaService) {}
@Get('repos')
@ApiOperation({ summary: 'Gitea 仓库列表' })
async getRepos() {
return this.gitea.getRepos();
}
@Get('repos/:owner/:repo/milestones')
@ApiOperation({ summary: '仓库里程碑' })
async getMilestones(@Param('owner') owner: string, @Param('repo') repo: string) {
return this.gitea.getMilestones(owner, repo);
}
@Get('repos/:owner/:repo/issues')
@ApiOperation({ summary: '仓库 Issue 列表' })
@ApiQuery({ name: 'milestone', required: false })
@ApiQuery({ name: 'state', required: false })
async getIssues(
@Param('owner') owner: string, @Param('repo') repo: string,
@Query('milestone') milestone?: string, @Query('state') state?: string,
) {
return this.gitea.getIssues(owner, repo, milestone, state);
}
@Get('repos/:owner/:repo/releases')
@ApiOperation({ summary: '仓库 Release 列表' })
async getReleases(@Param('owner') owner: string, @Param('repo') repo: string) {
return this.gitea.getReleases(owner, repo);
}
@Get('runners')
@ApiOperation({ summary: 'Gitea Runner 状态' })
async getRunners() {
return this.gitea.getRunners();
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ProjectCenterController } from './project-center.controller';
import { GiteaService } from './gitea.service';
@Module({
controllers: [ProjectCenterController],
providers: [GiteaService],
exports: [GiteaService],
})
export class ProjectCenterModule {}