fix: make all config consumers read dynamically instead of caching at module load

After migrating config to DB, values changed via Web UI were not picked
up by consumers that cached config at module load time.

- gitea.ts: replace static axios.create() with request interceptors that
  read config.gitea.apiUrl and accessToken on every request
- feishu.ts: remove constructor caching of webhookUrl/webhookSecret,
  read from config.feishu.* on each sendMessage() call
- engine.ts: create SandboxExec/LocalRepoManager/DiffExtractor/Orchestrator
  per review run instead of once at class init, so workdir/token/limits
  always reflect current config. FileReviewStore stays singleton (has state).
- index.ts: wrap JWT middleware in per-request handler so config.admin.jwtSecret
  is read dynamically instead of captured once at startup
This commit is contained in:
jeffusion
2026-03-05 16:08:50 +08:00
committed by 路遥知码力
parent e3b8365ea2
commit 9a356a228f
4 changed files with 78 additions and 62 deletions

View File

@@ -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);

View File

@@ -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<typeof setInterval> | 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<void> {
if (this.started || config.review.engine !== 'agent') {
return;
@@ -92,27 +116,23 @@ class ReviewEngine {
}
private async tick(): Promise<void> {
// 防止重入如果上一次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);

View File

@@ -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<void> {
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',

View File

@@ -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服务接口定义