mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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服务接口定义
|
||||
|
||||
Reference in New Issue
Block a user