diff --git a/src/controllers/admin.ts b/src/controllers/admin.ts index 973fc46..7a4bcf0 100644 --- a/src/controllers/admin.ts +++ b/src/controllers/admin.ts @@ -4,6 +4,7 @@ import config from '../config'; import { repositoryReviewPromptRepo } from '../db/repositories/repository-review-prompt-repo'; import { reviewEngine } from '../review/engine'; import { giteaService } from '../services/gitea'; +import { toErrorLogMeta } from '../utils/error-log'; import { logger } from '../utils/logger'; const publicRoutes = new Hono(); @@ -31,17 +32,55 @@ publicRoutes.post('/login', async (c) => { // 获取仓库列表及 Webhook 状态 protectedRoutes.get('/repositories', async (c) => { + const page = Number.parseInt(c.req.query('page') || '1', 10); + const query = c.req.query('q'); + const limit = 30; // 每页数量固定,或也可从查询参数获取 + const requestContext = { + page, + limit, + query: query ?? null, + requestUrl: c.req.url, + method: c.req.method, + runtime: process.versions.bun ? `bun-${process.versions.bun}` : process.version, + nodeEnv: process.env.NODE_ENV ?? null, + databasePath: process.env.DATABASE_PATH || './data/assistant.db', + }; + try { - const page = Number.parseInt(c.req.query('page') || '1', 10); - const query = c.req.query('q'); - const limit = 30; // 每页数量固定,或也可从查询参数获取 + logger.debug('开始获取仓库列表', requestContext); const { repos, totalCount } = await giteaService.listAllRepositories(page, limit, query); + logger.debug('仓库搜索接口返回成功', { + ...requestContext, + reposCount: repos.length, + totalCount, + sampleRepos: repos + .slice(0, 3) + .map((repo) => (typeof repo.full_name === 'string' ? repo.full_name : null)), + }); + const webhookUrl = c.req.url.replace(/\/admin\/api\/repositories.*$/, '/webhook/gitea'); const fullNames = repos .map((repo) => (typeof repo.full_name === 'string' ? repo.full_name : null)) .filter((name): name is string => name !== null); - const promptMap = repositoryReviewPromptRepo.listProjectPrompts(fullNames); + logger.debug('准备批量读取项目级提示词', { + ...requestContext, + fullNamesCount: fullNames.length, + fullNamesSample: fullNames.slice(0, 5), + }); + + let promptMap: Record; + try { + promptMap = repositoryReviewPromptRepo.listProjectPrompts(fullNames); + } catch (error: unknown) { + logger.error('批量读取项目级提示词失败', { + ...requestContext, + fullNamesCount: fullNames.length, + fullNamesSample: fullNames.slice(0, 5), + error: toErrorLogMeta(error), + }); + throw error; + } const reposWithStatus = await Promise.all( repos.map(async (repo) => { @@ -70,9 +109,13 @@ protectedRoutes.get('/repositories', async (c) => { page, limit, }); - } catch (error: any) { - logger.error('获取仓库列表失败:', error); - return c.json({ message: 'Failed to fetch repositories', error: error.message }, 500); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error('获取仓库列表失败:', { + ...requestContext, + error: toErrorLogMeta(error), + }); + return c.json({ message: 'Failed to fetch repositories', error: errorMessage }, 500); } }); diff --git a/src/services/gitea.ts b/src/services/gitea.ts index 3fbbd7b..fe968ea 100644 --- a/src/services/gitea.ts +++ b/src/services/gitea.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import config from '../config'; +import { toErrorLogMeta } from '../utils/error-log'; import { logger } from '../utils/logger'; export interface LineComment { @@ -32,6 +33,35 @@ giteaAdminClient.interceptors.request.use((req) => { return req; }); +function getErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function getResponseDataPreview(data: unknown): unknown { + if (typeof data === 'string') { + return data.length > 1000 ? `${data.slice(0, 1000)}...(truncated)` : data; + } + return data; +} + +function getAxiosErrorMeta(error: unknown): Record | null { + if (!axios.isAxiosError(error)) { + return null; + } + + return { + code: error.code ?? null, + status: error.response?.status ?? null, + statusText: error.response?.statusText ?? null, + method: error.config?.method ?? null, + baseURL: error.config?.baseURL ?? null, + url: error.config?.url ?? null, + params: error.config?.params ?? null, + responseHeaders: error.response?.headers ?? null, + responseDataPreview: getResponseDataPreview(error.response?.data), + }; +} + // Gitea服务接口定义 export interface GiteaService { // 获取PR的文件差异 @@ -382,7 +412,19 @@ export const giteaService: GiteaService = { limit = 30, query?: string ): Promise<{ repos: any[]; totalCount: number }> { + const requestContext = { + page, + limit, + query: query ?? null, + apiUrl: config.gitea.apiUrl, + hasAdminToken: Boolean(config.admin.giteaAdminToken), + hasAccessToken: Boolean(config.gitea.accessToken), + runtime: process.versions.bun ? `bun-${process.versions.bun}` : process.version, + nodeEnv: process.env.NODE_ENV ?? null, + }; + try { + logger.debug('开始请求 Gitea 仓库搜索接口', requestContext); const response = await giteaAdminClient.get('/repos/search', { params: { page, @@ -390,11 +432,51 @@ export const giteaService: GiteaService = { q: query, }, }); + + logger.debug('Gitea 仓库搜索接口返回成功', { + ...requestContext, + status: response.status, + contentType: response.headers['content-type'] ?? null, + dataCount: Array.isArray(response.data?.data) ? response.data.data.length : null, + headerTotalCount: response.headers['x-total-count'] ?? null, + }); + const totalCount = Number.parseInt(response.headers['x-total-count'] || '0', 10); return { repos: response.data.data, totalCount }; - } catch (error: any) { - logger.error('获取所有仓库列表失败:', error); - throw new Error(`获取所有仓库列表失败: ${error.message}`); + } catch (error: unknown) { + let rawResponseProbe: Record | null = null; + + try { + const probeResponse = await giteaAdminClient.get('/repos/search', { + params: { + page, + limit, + q: query, + }, + responseType: 'text', + transformResponse: [(data) => data], + }); + + rawResponseProbe = { + status: probeResponse.status, + contentType: probeResponse.headers['content-type'] ?? null, + bodyLength: typeof probeResponse.data === 'string' ? probeResponse.data.length : null, + bodyPreview: getResponseDataPreview(probeResponse.data), + }; + } catch (probeError: unknown) { + rawResponseProbe = { + probeError: toErrorLogMeta(probeError), + }; + } + + logger.error('获取所有仓库列表失败:', { + ...requestContext, + error: toErrorLogMeta(error), + axiosError: getAxiosErrorMeta(error), + rawResponseProbe, + }); + + throw new Error(`获取所有仓库列表失败: ${getErrorMessage(error)}`); } }, diff --git a/src/utils/error-log.ts b/src/utils/error-log.ts new file mode 100644 index 0000000..95ed0aa --- /dev/null +++ b/src/utils/error-log.ts @@ -0,0 +1,30 @@ +type UnknownRecord = Record; + +function toUnknownRecord(value: unknown): UnknownRecord { + if (typeof value === 'object' && value !== null) { + return value as UnknownRecord; + } + return { value }; +} + +export function toErrorLogMeta(error: unknown): UnknownRecord { + if (error instanceof Error) { + const base: UnknownRecord = { + name: error.name, + message: error.message, + stack: error.stack, + }; + + const ownProps = Object.getOwnPropertyNames(error); + for (const prop of ownProps) { + if (prop in base) { + continue; + } + base[prop] = (error as unknown as UnknownRecord)[prop]; + } + + return base; + } + + return toUnknownRecord(error); +}