diff --git a/src/index.ts b/src/index.ts index d69d0fb..a82776f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,7 +49,10 @@ app.route('/admin/api', adminController.publicRoutes); // 受保护的路由 const adminProtected = new Hono(); -adminProtected.use('/*', jwt({ secret: config.admin.jwtSecret, alg: 'HS256' })); +adminProtected.use('/*', (c, next) => { + const jwtMiddleware = jwt({ secret: config.admin.jwtSecret, alg: 'HS256' }); + return jwtMiddleware(c, next); +}); adminProtected.route('/', adminController.protectedRoutes); adminProtected.route('/feedback', feedbackRouter); adminProtected.route('/config', configRouter); diff --git a/src/review/engine.ts b/src/review/engine.ts index a16a54b..b2f2090 100644 --- a/src/review/engine.ts +++ b/src/review/engine.ts @@ -8,32 +8,56 @@ import { FileReviewStore } from './store/file-review-store'; import { CommitReviewPayload, PullRequestReviewPayload, ReviewRun } from './types'; class ReviewEngine { - private readonly store = new FileReviewStore(config.review.workdir); - private readonly sandboxExec = new SandboxExec(config.review.allowedCommands); - private readonly localRepoManager = new LocalRepoManager( - config.review.workdir, - this.sandboxExec, - config.review.commandTimeoutMs, - config.gitea.accessToken - ); - private readonly diffExtractor = new DiffExtractor( - this.sandboxExec, - this.localRepoManager, - config.review.commandTimeoutMs, - config.review.maxFilesPerRun, - config.review.maxFileContentChars - ); - private readonly orchestrator = new ReviewOrchestrator( - this.store, - this.localRepoManager, - this.diffExtractor - ); - + // Sub-objects are created lazily per config snapshot. + // store holds state (runs, steps) so we keep ONE instance but update workdir. + private _store: FileReviewStore | null = null; private started = false; private activeRunsCount = 0; private timer: ReturnType | null = null; private tickInProgress = false; + /** Lazily-created store — stable singleton (holds review state). */ + private get store(): FileReviewStore { + if (!this._store) { + this._store = new FileReviewStore(config.review.workdir); + } + return this._store; + } + + /** Fresh SandboxExec that always reflects current allowed-commands config. */ + private createSandboxExec(): SandboxExec { + return new SandboxExec(config.review.allowedCommands); + } + + /** Fresh LocalRepoManager that reads current config values. */ + private createLocalRepoManager(sandboxExec: SandboxExec): LocalRepoManager { + return new LocalRepoManager( + config.review.workdir, + sandboxExec, + config.review.commandTimeoutMs, + config.gitea.accessToken + ); + } + + /** Fresh DiffExtractor that reads current config values. */ + private createDiffExtractor(sandboxExec: SandboxExec, localRepoManager: LocalRepoManager): DiffExtractor { + return new DiffExtractor( + sandboxExec, + localRepoManager, + config.review.commandTimeoutMs, + config.review.maxFilesPerRun, + config.review.maxFileContentChars + ); + } + + /** Create a fresh orchestrator with current config for each run. */ + private createOrchestrator(): ReviewOrchestrator { + const sandboxExec = this.createSandboxExec(); + const localRepoManager = this.createLocalRepoManager(sandboxExec); + const diffExtractor = this.createDiffExtractor(sandboxExec, localRepoManager); + return new ReviewOrchestrator(this.store, localRepoManager, diffExtractor); + } + async start(): Promise { if (this.started || config.review.engine !== 'agent') { return; @@ -92,27 +116,23 @@ class ReviewEngine { } private async tick(): Promise { - // 防止重入:如果上一次tick还在执行,跳过本次调度 if (this.tickInProgress) { return; } this.tickInProgress = true; try { - // 检查是否达到并行限制 const maxParallel = config.review.maxParallelRuns; if (this.activeRunsCount >= maxParallel) { return; } - // 尝试获取并启动新任务,直到达到并行上限 while (this.activeRunsCount < maxParallel) { const run = await this.store.acquireNextQueuedRun(); if (!run) { - break; // 队列为空 + break; } - // 启动异步任务,不等待完成 this.activeRunsCount++; this.processRun(run).finally(() => { this.activeRunsCount--; @@ -132,10 +152,12 @@ class ReviewEngine { activeRuns: this.activeRunsCount, }); - try { - await this.orchestrator.execute(run); + // Create a fresh orchestrator per run so it picks up latest config values + const orchestrator = this.createOrchestrator(); + + try { + await orchestrator.execute(run); - // 检查run状态,防止将ignored状态覆盖为succeeded const runDetails = await this.store.getRunDetails(run.id); if (runDetails && runDetails.run.status !== 'ignored') { await this.store.markRunSucceeded(run.id); diff --git a/src/services/feishu.ts b/src/services/feishu.ts index 246aac1..91eb202 100644 --- a/src/services/feishu.ts +++ b/src/services/feishu.ts @@ -3,27 +3,11 @@ import config from '../config'; import { logger } from '../utils/logger'; export class FeishuService { - private webhookUrl?: string; - private webhookSecret?: string; - - constructor() { - this.webhookUrl = config.feishu.webhookUrl; - this.webhookSecret = config.feishu.webhookSecret; - - if (!this.webhookUrl) { - logger.info('飞书webhook URL未配置,飞书通知已禁用'); - } - - if (this.webhookUrl && !this.webhookSecret) { - logger.warn('飞书webhook密钥未配置,签名验证将被禁用'); - } - } - /** * 判断飞书通知是否已启用 */ isEnabled(): boolean { - return !!this.webhookUrl; + return !!config.feishu.webhookUrl; } /** @@ -43,7 +27,10 @@ export class FeishuService { * @param usernames 需要@的用户名列表 */ async sendMessage(content: string, usernames: string[] = []): Promise { - if (!this.webhookUrl) { + const webhookUrl = config.feishu.webhookUrl; + const webhookSecret = config.feishu.webhookSecret; + + if (!webhookUrl) { logger.debug('飞书通知已跳过: webhook URL未配置'); return; } @@ -66,12 +53,12 @@ export class FeishuService { } // 如果配置了密钥,添加签名 - if (this.webhookSecret) { + if (webhookSecret) { message.timestamp = timestamp; - message.sign = this.generateSign(timestamp, this.webhookSecret); + message.sign = this.generateSign(timestamp, webhookSecret); } - const response = await fetch(this.webhookUrl, { + const response = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/services/gitea.ts b/src/services/gitea.ts index 28dea13..3fbbd7b 100644 --- a/src/services/gitea.ts +++ b/src/services/gitea.ts @@ -8,24 +8,28 @@ export interface LineComment { comment: string; } -// 创建API客户端 +// API客户端 — 使用 interceptor 确保每次请求都读取最新的 config const giteaClient = axios.create({ - baseURL: config.gitea.apiUrl, - headers: { - Authorization: `token ${config.gitea.accessToken}`, - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, +}); +giteaClient.interceptors.request.use((req) => { + req.baseURL = config.gitea.apiUrl; + req.headers.Authorization = `token ${config.gitea.accessToken}`; + return req; }); -// 创建用于管理操作的API客户端 +// 管理操作的API客户端 — 同样动态读取 config const giteaAdminClient = axios.create({ - baseURL: config.gitea.apiUrl, headers: { - Authorization: `token ${config.admin.giteaAdminToken || config.gitea.accessToken}`, 'Content-Type': 'application/json', - 'User-Agent': 'curl/7.81.0', // 伪装成 curl + 'User-Agent': 'curl/7.81.0', }, - proxy: false, // 禁用所有代理 + proxy: false, +}); +giteaAdminClient.interceptors.request.use((req) => { + req.baseURL = config.gitea.apiUrl; + req.headers.Authorization = `token ${config.admin.giteaAdminToken || config.gitea.accessToken}`; + return req; }); // Gitea服务接口定义