mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-06-07 23:16:46 +00:00
feat(config): add Codex engine configuration fields
Add CODEX_API_URL, CODEX_API_KEY, CODEX_MODEL, CODEX_TIMEOUT_MS, and CODEX_REVIEW_PROMPT to config schema and manager. Wire Codex engine dispatch in review controller alongside agent/legacy engines. Register MCP Streamable HTTP endpoint at /mcp/gitea-review in app entry point. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -41,6 +41,13 @@ export interface AppConfig {
|
|||||||
llmRetryMaxAttempts: number;
|
llmRetryMaxAttempts: number;
|
||||||
llmRetryBaseDelayMs: number;
|
llmRetryBaseDelayMs: number;
|
||||||
enableTriage: boolean;
|
enableTriage: boolean;
|
||||||
|
// Codex engine
|
||||||
|
codexApiUrl: string;
|
||||||
|
codexApiKey: string | undefined;
|
||||||
|
codexModel: string;
|
||||||
|
codexTimeoutMs: number;
|
||||||
|
codexReviewPrompt: string | undefined;
|
||||||
|
// Memory (shared)
|
||||||
qdrantUrl: string | undefined;
|
qdrantUrl: string | undefined;
|
||||||
enableMemory: boolean;
|
enableMemory: boolean;
|
||||||
fewShotExamplesCount: number;
|
fewShotExamplesCount: number;
|
||||||
@@ -161,6 +168,13 @@ class ConfigManager {
|
|||||||
llmRetryMaxAttempts: toNumber('LLM_RETRY_MAX_ATTEMPTS', 3),
|
llmRetryMaxAttempts: toNumber('LLM_RETRY_MAX_ATTEMPTS', 3),
|
||||||
llmRetryBaseDelayMs: toNumber('LLM_RETRY_BASE_DELAY_MS', 1000),
|
llmRetryBaseDelayMs: toNumber('LLM_RETRY_BASE_DELAY_MS', 1000),
|
||||||
enableTriage: toBoolean('ENABLE_TRIAGE', true),
|
enableTriage: toBoolean('ENABLE_TRIAGE', true),
|
||||||
|
// Codex engine
|
||||||
|
codexApiUrl: values.CODEX_API_URL ?? 'https://api.openai.com/v1',
|
||||||
|
codexApiKey: values.CODEX_API_KEY,
|
||||||
|
codexModel: values.CODEX_MODEL ?? 'o3',
|
||||||
|
codexTimeoutMs: toNumber('CODEX_TIMEOUT_MS', 300000),
|
||||||
|
codexReviewPrompt: values.CODEX_REVIEW_PROMPT,
|
||||||
|
// Memory
|
||||||
qdrantUrl: values.QDRANT_URL,
|
qdrantUrl: values.QDRANT_URL,
|
||||||
enableMemory: toBoolean('ENABLE_MEMORY', false),
|
enableMemory: toBoolean('ENABLE_MEMORY', false),
|
||||||
fewShotExamplesCount: toNumber('FEW_SHOT_EXAMPLES_COUNT', 10),
|
fewShotExamplesCount: toNumber('FEW_SHOT_EXAMPLES_COUNT', 10),
|
||||||
|
|||||||
@@ -178,10 +178,10 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [
|
|||||||
envKey: 'REVIEW_ENGINE',
|
envKey: 'REVIEW_ENGINE',
|
||||||
group: 'review',
|
group: 'review',
|
||||||
label: '审查引擎',
|
label: '审查引擎',
|
||||||
description: '代码审查模式:legacy(传统)或 agent(多代理编排)',
|
description: '代码审查模式:legacy(传统)、agent(多代理编排)或 codex(Codex CLI)',
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
enumValues: ['legacy', 'agent'],
|
enumValues: ['legacy', 'agent', 'codex'],
|
||||||
defaultValue: 'legacy',
|
defaultValue: 'legacy',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -309,6 +309,53 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Codex 审查引擎 ──────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
envKey: 'CODEX_API_URL',
|
||||||
|
group: 'review',
|
||||||
|
label: 'Codex API 地址',
|
||||||
|
description: 'Codex CLI 使用的 LLM API 端点地址',
|
||||||
|
type: 'url',
|
||||||
|
sensitive: false,
|
||||||
|
defaultValue: 'https://api.openai.com/v1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
envKey: 'CODEX_API_KEY',
|
||||||
|
group: 'review',
|
||||||
|
label: 'Codex API 密钥',
|
||||||
|
description: 'Codex CLI 调用 LLM 所需的 API 密钥',
|
||||||
|
type: 'string',
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
envKey: 'CODEX_MODEL',
|
||||||
|
group: 'review',
|
||||||
|
label: 'Codex 模型',
|
||||||
|
description: 'Codex CLI 使用的模型名称',
|
||||||
|
type: 'string',
|
||||||
|
sensitive: false,
|
||||||
|
defaultValue: 'o3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
envKey: 'CODEX_TIMEOUT_MS',
|
||||||
|
group: 'review',
|
||||||
|
label: 'Codex 超时(ms)',
|
||||||
|
description: 'Codex CLI 单次审查的执行超时时间(毫秒)',
|
||||||
|
type: 'number',
|
||||||
|
sensitive: false,
|
||||||
|
min: 30000,
|
||||||
|
max: 600000,
|
||||||
|
defaultValue: 300000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
envKey: 'CODEX_REVIEW_PROMPT',
|
||||||
|
group: 'review',
|
||||||
|
label: 'Codex 审查提示词',
|
||||||
|
description: '覆盖 Codex 引擎默认的代码审查提示词(留空使用内置提示词)',
|
||||||
|
type: 'text',
|
||||||
|
sensitive: false,
|
||||||
|
},
|
||||||
|
|
||||||
// ── 记忆与学习 ──────────────────────────────────────────────────────────
|
// ── 记忆与学习 ──────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
envKey: 'QDRANT_URL',
|
envKey: 'QDRANT_URL',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as crypto from 'node:crypto';
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { map } from 'lodash-es';
|
import { map } from 'lodash-es';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { codexEngine } from '../review/codex/codex-engine';
|
||||||
import { reviewEngine } from '../review/engine';
|
import { reviewEngine } from '../review/engine';
|
||||||
import { aiReviewService } from '../services/ai-review';
|
import { aiReviewService } from '../services/ai-review';
|
||||||
import { feishuService } from '../services/feishu';
|
import { feishuService } from '../services/feishu';
|
||||||
@@ -150,13 +151,13 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.review.engine === 'agent') {
|
if (config.review.engine === 'agent' || config.review.engine === 'codex') {
|
||||||
// Fork PR策略:始终clone base repo(保证有baseSha),headCloneUrl作为额外remote(保证有headSha)
|
// Fork PR策略:始终clone base repo(保证有baseSha),headCloneUrl作为额外remote(保证有headSha)
|
||||||
const baseCloneUrl = resolveCloneUrl(repo);
|
const baseCloneUrl = resolveCloneUrl(repo);
|
||||||
const headSha = pullRequest.head?.sha;
|
const headSha = pullRequest.head?.sha;
|
||||||
const baseSha = pullRequest.base?.sha;
|
const baseSha = pullRequest.base?.sha;
|
||||||
if (!baseCloneUrl || !headSha || !baseSha) {
|
if (!baseCloneUrl || !headSha || !baseSha) {
|
||||||
return c.json({ error: '缺少Agent审查所需字段(clone_url/base sha/head sha)' }, 400);
|
return c.json({ error: '缺少审查所需字段(clone_url/base sha/head sha)' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测fork PR:head.repo存在且与base repo不同
|
// 检测fork PR:head.repo存在且与base repo不同
|
||||||
@@ -167,7 +168,8 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
|
|
||||||
// 包含baseSha以支持retarget场景:相同headSha但baseSha变化时需要重新审查
|
// 包含baseSha以支持retarget场景:相同headSha但baseSha变化时需要重新审查
|
||||||
const idempotencyKey = `${owner}/${repoName}#${prNumber}:${baseSha}...${headSha}`;
|
const idempotencyKey = `${owner}/${repoName}#${prNumber}:${baseSha}...${headSha}`;
|
||||||
const { run, reused } = await reviewEngine.enqueuePullRequest({
|
const engineInstance = config.review.engine === 'codex' ? codexEngine : reviewEngine;
|
||||||
|
const { run, reused } = await engineInstance.enqueuePullRequest({
|
||||||
eventType: 'pull_request',
|
eventType: 'pull_request',
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
owner,
|
owner,
|
||||||
@@ -179,10 +181,11 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
headSha,
|
headSha,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const engineLabel = config.review.engine === 'codex' ? 'Codex' : 'Agent';
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
status: reused ? 'deduplicated' : 'accepted',
|
status: reused ? 'deduplicated' : 'accepted',
|
||||||
message: reused ? '审查任务已存在,已去重' : 'Agent代码审查任务已入队',
|
message: reused ? '审查任务已存在,已去重' : `${engineLabel}代码审查任务已入队`,
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
},
|
},
|
||||||
202
|
202
|
||||||
@@ -261,15 +264,16 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise<Response>
|
|||||||
removed: commitInfo.removed.length,
|
removed: commitInfo.removed.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Agent模式优先处理:从本地仓库派生diff,不依赖webhook文件列表
|
// Agent/Codex模式优先处理:从本地仓库派生diff,不依赖webhook文件列表
|
||||||
if (config.review.engine === 'agent') {
|
if (config.review.engine === 'agent' || config.review.engine === 'codex') {
|
||||||
const cloneUrl = resolveCloneUrl(body.repository);
|
const cloneUrl = resolveCloneUrl(body.repository);
|
||||||
if (!cloneUrl) {
|
if (!cloneUrl) {
|
||||||
return c.json({ error: '缺少Agent审查所需字段(clone_url)' }, 400);
|
return c.json({ error: '缺少审查所需字段(clone_url)' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const idempotencyKey = `${owner}/${repoName}@${commitSha}`;
|
const idempotencyKey = `${owner}/${repoName}@${commitSha}`;
|
||||||
const { run, reused } = await reviewEngine.enqueueCommit({
|
const engineInstance = config.review.engine === 'codex' ? codexEngine : reviewEngine;
|
||||||
|
const { run, reused } = await engineInstance.enqueueCommit({
|
||||||
eventType: 'commit_status',
|
eventType: 'commit_status',
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
owner,
|
owner,
|
||||||
@@ -280,10 +284,11 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise<Response>
|
|||||||
relatedPrNumber: relatedPR?.number,
|
relatedPrNumber: relatedPR?.number,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const engineLabel = config.review.engine === 'codex' ? 'Codex' : 'Agent';
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
status: reused ? 'deduplicated' : 'accepted',
|
status: reused ? 'deduplicated' : 'accepted',
|
||||||
message: reused ? '审查任务已存在,已去重' : 'Agent提交审查任务已入队',
|
message: reused ? '审查任务已存在,已去重' : `${engineLabel}提交审查任务已入队`,
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
},
|
},
|
||||||
202
|
202
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { llmConfigRouter } from './controllers/llm-config';
|
|||||||
import { handleGiteaWebhook } from './controllers/review';
|
import { handleGiteaWebhook } from './controllers/review';
|
||||||
import { initMasterKey } from './crypto/secrets';
|
import { initMasterKey } from './crypto/secrets';
|
||||||
import { initDatabase } from './db/database';
|
import { initDatabase } from './db/database';
|
||||||
|
import { codexEngine } from './review/codex/codex-engine';
|
||||||
|
import { mcpRouter } from './review/codex/mcp-handler';
|
||||||
import { reviewEngine } from './review/engine';
|
import { reviewEngine } from './review/engine';
|
||||||
|
|
||||||
initMasterKey();
|
initMasterKey();
|
||||||
@@ -40,6 +42,9 @@ app.get('/', (c) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// MCP 端点(Codex 审查引擎使用,无需认证)
|
||||||
|
app.route('/mcp/gitea-review', mcpRouter);
|
||||||
|
|
||||||
// 统一的Gitea webhook路由 - 处理所有事件类型
|
// 统一的Gitea webhook路由 - 处理所有事件类型
|
||||||
app.post('/webhook/gitea', handleGiteaWebhook);
|
app.post('/webhook/gitea', handleGiteaWebhook);
|
||||||
|
|
||||||
@@ -71,9 +76,13 @@ app.get('*', serveStatic({ path: './public/index.html' }));
|
|||||||
const port = config.app.port;
|
const port = config.app.port;
|
||||||
console.log(`⚡️ 服务启动在 http://localhost:${port}`);
|
console.log(`⚡️ 服务启动在 http://localhost:${port}`);
|
||||||
|
|
||||||
|
// 启动审查引擎(根据配置选择)
|
||||||
reviewEngine.start().catch((error) => {
|
reviewEngine.start().catch((error) => {
|
||||||
console.error('❌ 启动Agent Review Engine失败', error);
|
console.error('❌ 启动Agent Review Engine失败', error);
|
||||||
});
|
});
|
||||||
|
codexEngine.start().catch((error) => {
|
||||||
|
console.error('❌ 启动Codex Review Engine失败', error);
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化反馈系统(总是初始化,记忆系统可选)
|
// 初始化反馈系统(总是初始化,记忆系统可选)
|
||||||
const reviewStore = reviewEngine.getStore();
|
const reviewStore = reviewEngine.getStore();
|
||||||
|
|||||||
Reference in New Issue
Block a user