mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
fix(repo): add structured diagnostics for repository list failures
Capture request/runtime context plus nested error metadata so docker-only repository-list issues can be diagnosed quickly. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
This commit is contained in:
@@ -4,6 +4,7 @@ import config from '../config';
|
|||||||
import { repositoryReviewPromptRepo } from '../db/repositories/repository-review-prompt-repo';
|
import { repositoryReviewPromptRepo } from '../db/repositories/repository-review-prompt-repo';
|
||||||
import { reviewEngine } from '../review/engine';
|
import { reviewEngine } from '../review/engine';
|
||||||
import { giteaService } from '../services/gitea';
|
import { giteaService } from '../services/gitea';
|
||||||
|
import { toErrorLogMeta } from '../utils/error-log';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
const publicRoutes = new Hono();
|
const publicRoutes = new Hono();
|
||||||
@@ -31,17 +32,55 @@ publicRoutes.post('/login', async (c) => {
|
|||||||
|
|
||||||
// 获取仓库列表及 Webhook 状态
|
// 获取仓库列表及 Webhook 状态
|
||||||
protectedRoutes.get('/repositories', async (c) => {
|
protectedRoutes.get('/repositories', async (c) => {
|
||||||
try {
|
|
||||||
const page = Number.parseInt(c.req.query('page') || '1', 10);
|
const page = Number.parseInt(c.req.query('page') || '1', 10);
|
||||||
const query = c.req.query('q');
|
const query = c.req.query('q');
|
||||||
const limit = 30; // 每页数量固定,或也可从查询参数获取
|
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 {
|
||||||
|
logger.debug('开始获取仓库列表', requestContext);
|
||||||
|
|
||||||
const { repos, totalCount } = await giteaService.listAllRepositories(page, limit, query);
|
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 webhookUrl = c.req.url.replace(/\/admin\/api\/repositories.*$/, '/webhook/gitea');
|
||||||
const fullNames = repos
|
const fullNames = repos
|
||||||
.map((repo) => (typeof repo.full_name === 'string' ? repo.full_name : null))
|
.map((repo) => (typeof repo.full_name === 'string' ? repo.full_name : null))
|
||||||
.filter((name): name is string => 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<string, string>;
|
||||||
|
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(
|
const reposWithStatus = await Promise.all(
|
||||||
repos.map(async (repo) => {
|
repos.map(async (repo) => {
|
||||||
@@ -70,9 +109,13 @@ protectedRoutes.get('/repositories', async (c) => {
|
|||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
logger.error('获取仓库列表失败:', error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
return c.json({ message: 'Failed to fetch repositories', error: error.message }, 500);
|
logger.error('获取仓库列表失败:', {
|
||||||
|
...requestContext,
|
||||||
|
error: toErrorLogMeta(error),
|
||||||
|
});
|
||||||
|
return c.json({ message: 'Failed to fetch repositories', error: errorMessage }, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { toErrorLogMeta } from '../utils/error-log';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
export interface LineComment {
|
export interface LineComment {
|
||||||
@@ -32,6 +33,35 @@ giteaAdminClient.interceptors.request.use((req) => {
|
|||||||
return 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<string, unknown> | 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服务接口定义
|
// Gitea服务接口定义
|
||||||
export interface GiteaService {
|
export interface GiteaService {
|
||||||
// 获取PR的文件差异
|
// 获取PR的文件差异
|
||||||
@@ -382,7 +412,19 @@ export const giteaService: GiteaService = {
|
|||||||
limit = 30,
|
limit = 30,
|
||||||
query?: string
|
query?: string
|
||||||
): Promise<{ repos: any[]; totalCount: number }> {
|
): 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 {
|
try {
|
||||||
|
logger.debug('开始请求 Gitea 仓库搜索接口', requestContext);
|
||||||
const response = await giteaAdminClient.get('/repos/search', {
|
const response = await giteaAdminClient.get('/repos/search', {
|
||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
@@ -390,11 +432,51 @@ export const giteaService: GiteaService = {
|
|||||||
q: query,
|
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);
|
const totalCount = Number.parseInt(response.headers['x-total-count'] || '0', 10);
|
||||||
return { repos: response.data.data, totalCount };
|
return { repos: response.data.data, totalCount };
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
logger.error('获取所有仓库列表失败:', error);
|
let rawResponseProbe: Record<string, unknown> | null = null;
|
||||||
throw new Error(`获取所有仓库列表失败: ${error.message}`);
|
|
||||||
|
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)}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
30
src/utils/error-log.ts
Normal file
30
src/utils/error-log.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
type UnknownRecord = Record<string, unknown>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user