From 129094a39e77b0bb66b052ed19bb8969d1757503 Mon Sep 17 00:00:00 2001 From: jeffusion Date: Sat, 7 Mar 2026 00:15:45 +0800 Subject: [PATCH] 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) --- src/config/config-manager.ts | 14 ++++++++++ src/config/config-schema.ts | 51 ++++++++++++++++++++++++++++++++++-- src/controllers/review.ts | 23 +++++++++------- src/index.ts | 9 +++++++ 4 files changed, 86 insertions(+), 11 deletions(-) diff --git a/src/config/config-manager.ts b/src/config/config-manager.ts index 82ee16e..c93053d 100644 --- a/src/config/config-manager.ts +++ b/src/config/config-manager.ts @@ -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), diff --git a/src/config/config-schema.ts b/src/config/config-schema.ts index 735ee1a..68bfec3 100644 --- a/src/config/config-schema.ts +++ b/src/config/config-schema.ts @@ -178,10 +178,10 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [ envKey: 'REVIEW_ENGINE', group: 'review', label: '审查引擎', - description: '代码审查模式:legacy(传统)或 agent(多代理编排)', + description: '代码审查模式:legacy(传统)、agent(多代理编排)或 codex(Codex 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', diff --git a/src/controllers/review.ts b/src/controllers/review.ts index b2f2867..ae1cf28 100644 --- a/src/controllers/review.ts +++ b/src/controllers/review.ts @@ -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 } } - if (config.review.engine === 'agent') { + if (config.review.engine === 'agent' || config.review.engine === 'codex') { // Fork PR策略:始终clone base repo(保证有baseSha),headCloneUrl作为额外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 PR:head.repo存在且与base repo不同 @@ -167,7 +168,8 @@ async function handlePullRequestEvent(c: Context, body: any): Promise // 包含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 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 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 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 diff --git a/src/index.ts b/src/index.ts index a82776f..8c66d56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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();