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:
jeffusion
2026-03-07 00:15:45 +08:00
committed by 路遥知码力
parent 9308c60aa0
commit 129094a39e
4 changed files with 86 additions and 11 deletions

View File

@@ -41,6 +41,13 @@ export interface AppConfig {
llmRetryMaxAttempts: number;
llmRetryBaseDelayMs: number;
enableTriage: boolean;
// Codex engine
codexApiUrl: string;
codexApiKey: string | undefined;
codexModel: string;
codexTimeoutMs: number;
codexReviewPrompt: string | undefined;
// Memory (shared)
qdrantUrl: string | undefined;
enableMemory: boolean;
fewShotExamplesCount: number;
@@ -161,6 +168,13 @@ class ConfigManager {
llmRetryMaxAttempts: toNumber('LLM_RETRY_MAX_ATTEMPTS', 3),
llmRetryBaseDelayMs: toNumber('LLM_RETRY_BASE_DELAY_MS', 1000),
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,
enableMemory: toBoolean('ENABLE_MEMORY', false),
fewShotExamplesCount: toNumber('FEW_SHOT_EXAMPLES_COUNT', 10),

View File

@@ -178,10 +178,10 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [
envKey: 'REVIEW_ENGINE',
group: 'review',
label: '审查引擎',
description: '代码审查模式legacy传统agent多代理编排',
description: '代码审查模式legacy传统agent多代理编排或 codexCodex CLI',
type: 'enum',
sensitive: false,
enumValues: ['legacy', 'agent'],
enumValues: ['legacy', 'agent', 'codex'],
defaultValue: 'legacy',
},
{
@@ -309,6 +309,53 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [
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',

View File

@@ -2,6 +2,7 @@ import * as crypto from 'node:crypto';
import { Context } from 'hono';
import { map } from 'lodash-es';
import config from '../config';
import { codexEngine } from '../review/codex/codex-engine';
import { reviewEngine } from '../review/engine';
import { aiReviewService } from '../services/ai-review';
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保证有baseShaheadCloneUrl作为额外remote保证有headSha
const baseCloneUrl = resolveCloneUrl(repo);
const headSha = pullRequest.head?.sha;
const baseSha = pullRequest.base?.sha;
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 PRhead.repo存在且与base repo不同
@@ -167,7 +168,8 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
// 包含baseSha以支持retarget场景相同headSha但baseSha变化时需要重新审查
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',
idempotencyKey,
owner,
@@ -179,10 +181,11 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
headSha,
});
const engineLabel = config.review.engine === 'codex' ? 'Codex' : 'Agent';
return c.json(
{
status: reused ? 'deduplicated' : 'accepted',
message: reused ? '审查任务已存在,已去重' : 'Agent代码审查任务已入队',
message: reused ? '审查任务已存在,已去重' : `${engineLabel}代码审查任务已入队`,
runId: run.id,
},
202
@@ -261,15 +264,16 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise<Response>
removed: commitInfo.removed.length,
});
// Agent模式优先处理从本地仓库派生diff不依赖webhook文件列表
if (config.review.engine === 'agent') {
// Agent/Codex模式优先处理从本地仓库派生diff不依赖webhook文件列表
if (config.review.engine === 'agent' || config.review.engine === 'codex') {
const cloneUrl = resolveCloneUrl(body.repository);
if (!cloneUrl) {
return c.json({ error: '缺少Agent审查所需字段(clone_url)' }, 400);
return c.json({ error: '缺少审查所需字段(clone_url)' }, 400);
}
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',
idempotencyKey,
owner,
@@ -280,10 +284,11 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise<Response>
relatedPrNumber: relatedPR?.number,
});
const engineLabel = config.review.engine === 'codex' ? 'Codex' : 'Agent';
return c.json(
{
status: reused ? 'deduplicated' : 'accepted',
message: reused ? '审查任务已存在,已去重' : 'Agent提交审查任务已入队',
message: reused ? '审查任务已存在,已去重' : `${engineLabel}提交审查任务已入队`,
runId: run.id,
},
202

View File

@@ -9,6 +9,8 @@ import { llmConfigRouter } from './controllers/llm-config';
import { handleGiteaWebhook } from './controllers/review';
import { initMasterKey } from './crypto/secrets';
import { initDatabase } from './db/database';
import { codexEngine } from './review/codex/codex-engine';
import { mcpRouter } from './review/codex/mcp-handler';
import { reviewEngine } from './review/engine';
initMasterKey();
@@ -40,6 +42,9 @@ app.get('/', (c) => {
});
});
// MCP 端点Codex 审查引擎使用,无需认证)
app.route('/mcp/gitea-review', mcpRouter);
// 统一的Gitea webhook路由 - 处理所有事件类型
app.post('/webhook/gitea', handleGiteaWebhook);
@@ -71,9 +76,13 @@ app.get('*', serveStatic({ path: './public/index.html' }));
const port = config.app.port;
console.log(`⚡️ 服务启动在 http://localhost:${port}`);
// 启动审查引擎(根据配置选择)
reviewEngine.start().catch((error) => {
console.error('❌ 启动Agent Review Engine失败', error);
});
codexEngine.start().catch((error) => {
console.error('❌ 启动Codex Review Engine失败', error);
});
// 初始化反馈系统(总是初始化,记忆系统可选)
const reviewStore = reviewEngine.getStore();