feat(config): migrate all runtime settings from env vars to SQLite DB

Replace env-var based config with DB-first approach (Portainer model).
Only PORT, DATABASE_PATH, and MASTER_KEY_PATH remain as env vars.
All other settings (Gitea, Feishu, security, review engine, memory) are
managed through the Admin Dashboard Web UI backed by system_settings table.

- ConfigManager rewrites getRawValue() to read from settingsRepo with
  fallback to compiled-in defaults (no more process.env reads)
- seedDefaults() auto-generates JWT_SECRET and WEBHOOK_SECRET on first boot
- getSource() returns 'db' | 'default' (removed 'env' source type)
- Merged 'app'+'admin' config groups into 'security' group
- Removed PORT from CONFIG_FIELDS (env-var only)
- Removed readonly/readonlyWarning from all field definitions
This commit is contained in:
jeffusion
2026-03-05 11:35:19 +08:00
committed by 路遥知码力
parent 9d986f4b5a
commit 4c32a460d3
4 changed files with 139 additions and 366 deletions

View File

@@ -1,101 +1,6 @@
/**
* Three-layer configuration manager.
* Priority: Zod defaults → process.env → JSON overrides
*
* Override file format:
* { version: 1, updatedAt: string, overrides: Record<string, string> }
*
* Bun-friendly IO: reads via readFile, writes atomically via temp+rename.
*/
import { randomUUID } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { config as dotenvConfig } from 'dotenv';
import { z } from 'zod';
// Load .env before any process.env access (must precede singleton construction)
dotenvConfig();
// ---------------------------------------------------------------------------
// Override file types
// ---------------------------------------------------------------------------
interface OverridesFile {
version: 1;
updatedAt: string;
overrides: Record<string, string>;
}
// ---------------------------------------------------------------------------
// Zod schema (identical to src/config/index.ts)
// ---------------------------------------------------------------------------
const defaultAllowedReviewCommands = ['git', 'rg', 'cat', 'sed', 'wc'];
const envSchema = z.object({
// Gitea
GITEA_API_URL: z.string().url().default('http://localhost:5174/api/v1'),
GITEA_ACCESS_TOKEN: z.string().default('test_token'),
GITEA_ADMIN_TOKEN: z.string().optional(),
CUSTOM_SUMMARY_PROMPT: z.string().optional(),
CUSTOM_LINE_COMMENT_PROMPT: z.string().optional(),
GLOBAL_PROMPT: z.string().optional(),
// Feishu
FEISHU_WEBHOOK_URL: z.preprocess(
(val) => (typeof val === 'string' && val.trim() === '' ? undefined : val),
z.string().url().optional()
),
FEISHU_WEBHOOK_SECRET: z.string().optional(),
// App
PORT: z.string().transform(Number).default('5174'),
WEBHOOK_SECRET: z.string().default('test_webhook_secret'),
// Admin
ADMIN_PASSWORD: z.string().default('password'),
JWT_SECRET: z.string().default('a-secure-secret-for-jwt'),
// Review engine
REVIEW_ENGINE: z.enum(['legacy', 'agent']).default('legacy'),
REVIEW_WORKDIR: z.string().default('/tmp/gitea-assistant'),
REVIEW_MAX_PARALLEL_RUNS: z.coerce.number().int().min(1).max(8).default(2),
REVIEW_MAX_FILES_PER_RUN: z.coerce.number().int().min(1).max(1000).default(200),
REVIEW_MAX_FILE_CONTENT_CHARS: z.coerce.number().int().min(1000).max(1_000_000).default(40_000),
REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE: z.coerce.number().min(0).max(1).default(0.8),
REVIEW_ENABLE_HUMAN_GATE: z
.enum(['true', 'false'])
.default('true')
.transform((value) => value === 'true'),
REVIEW_ALLOWED_COMMANDS: z.string().default(defaultAllowedReviewCommands.join(',')),
REVIEW_COMMAND_TIMEOUT_MS: z.coerce.number().int().min(1000).max(300000).default(10000),
// Memory & learning
QDRANT_URL: z.preprocess(
(val) => (typeof val === 'string' && val.trim() === '' ? undefined : val),
z.string().url().optional()
),
ENABLE_MEMORY: z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true'),
FEW_SHOT_EXAMPLES_COUNT: z.coerce.number().int().min(0).max(20).default(10),
// Reflection & debate
ENABLE_REFLECTION: z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true'),
MAX_REFLECTION_ROUNDS: z.coerce.number().int().min(1).max(5).default(2),
ENABLE_DEBATE: z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true'),
DEBATE_THRESHOLD: z.enum(['high', 'medium']).default('high'),
});
import { randomBytes } from 'node:crypto';
import { settingsRepo } from '../db/repositories/settings-repo';
import { CONFIG_FIELDS, type ConfigFieldMeta } from './config-schema';
// ---------------------------------------------------------------------------
// Config shape (matches default export of src/config/index.ts)
@@ -142,196 +47,172 @@ export interface AppConfig {
};
}
// ---------------------------------------------------------------------------
// Dev fallback (matches src/config/index.ts behavior when validation fails)
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ConfigManager
// ---------------------------------------------------------------------------
class ConfigManager {
private readonly overridesPath: string;
private overrides: Record<string, string> = {};
private readonly fieldsMap = new Map<string, ConfigFieldMeta>(
CONFIG_FIELDS.map((field) => [field.envKey, field])
);
constructor() {
this.overridesPath = resolve(process.env.CONFIG_OVERRIDES_PATH || './config-overrides.json');
this.loadOverridesSync();
private defaultToString(value: string | number | boolean): string {
if (typeof value === 'string') return value;
return String(value);
}
/** Synchronously load overrides at construction time (file is tiny). */
private loadOverridesSync(): void {
private getRawValue(key: string): string | undefined {
try {
const text = readFileSync(this.overridesPath, 'utf-8');
const data: OverridesFile = JSON.parse(text);
if (data && typeof data.overrides === 'object' && data.overrides !== null) {
this.overrides = { ...data.overrides };
const fromDb = settingsRepo.get(key);
if (fromDb !== null) {
return fromDb;
}
} catch {
// File missing or invalid JSON — start with empty overrides
// DB not initialized yet (e.g. during tests that don't init DB)
// Fall through to return the default value below
}
const field = this.fieldsMap.get(key);
if (!field || field.defaultValue === undefined) {
return undefined;
}
return this.defaultToString(field.defaultValue);
}
// ── Override file I/O ────────────────────────────────────────────────────
/** Load overrides from disk. If file is missing or malformed, treat as empty. */
async loadOverrides(): Promise<void> {
try {
const text = await readFile(this.overridesPath, 'utf-8');
const data: OverridesFile = JSON.parse(text);
if (data && typeof data.overrides === 'object' && data.overrides !== null) {
this.overrides = { ...data.overrides };
} else {
this.overrides = {};
}
} catch {
// File missing or invalid JSON — start with empty overrides
this.overrides = {};
getAllRawValues(): Record<string, string | undefined> {
const values: Record<string, string | undefined> = {};
for (const field of CONFIG_FIELDS) {
values[field.envKey] = this.getRawValue(field.envKey);
}
return values;
}
/** Persist current overrides to disk. Tries atomic rename; falls back to direct write. */
private async persistOverrides(): Promise<void> {
const dir = dirname(this.overridesPath);
await mkdir(dir, { recursive: true });
getCurrent(): AppConfig {
const values = this.getAllRawValues();
const portValue = process.env.PORT;
const parsedPort = portValue !== undefined && portValue !== '' ? Number(portValue) : 5174;
const port = Number.isFinite(parsedPort) ? parsedPort : 5174;
const payload: OverridesFile = {
version: 1,
updatedAt: new Date().toISOString(),
overrides: { ...this.overrides },
const toNumber = (key: string, fallback: number): number => {
const raw = values[key];
if (raw === undefined) return fallback;
const num = Number(raw);
return Number.isFinite(num) ? num : fallback;
};
const json = JSON.stringify(payload, null, 2);
const toBoolean = (key: string, fallback: boolean): boolean => {
const raw = values[key];
if (raw === undefined) return fallback;
return raw === 'true';
};
// Atomic rename may fail on K8s volumes (EBUSY/EXDEV); fall back to direct write.
const tmpPath = `${this.overridesPath}.${randomUUID()}.tmp`;
try {
await writeFile(tmpPath, json, 'utf-8');
await rename(tmpPath, this.overridesPath);
} catch {
await writeFile(this.overridesPath, json, 'utf-8');
// Clean up orphaned tmp file (best effort)
try { await unlink(tmpPath); } catch { /* ignore */ }
}
}
// ── Core API ─────────────────────────────────────────────────────────────
/**
* Returns the fully resolved config object with the same shape as the
* default export of `src/config/index.ts`.
*
* Layering: Zod defaults → process.env → overrides JSON
*/
getCurrent(): AppConfig {
// Build a merged env-like record: process.env overlaid with overrides
const merged: Record<string, string | undefined> = {};
for (const key of Object.keys(envSchema.shape)) {
const envVal = process.env[key];
if (envVal !== undefined && envVal !== '') {
merged[key] = envVal;
}
// Override wins if present and non-empty
const ov = this.overrides[key];
if (ov !== undefined && ov !== '') {
merged[key] = ov;
}
}
const parseResult = envSchema.safeParse(merged);
if (!parseResult.success) {
throw new Error('Configuration validation error');
}
const env = parseResult.data;
const toStringArray = (key: string, fallback: string[]): string[] => {
const raw = values[key];
if (raw === undefined) return fallback;
return raw
.split(',')
.map((item) => item.trim())
.filter(Boolean);
};
return {
gitea: {
apiUrl: env.GITEA_API_URL,
accessToken: env.GITEA_ACCESS_TOKEN,
apiUrl: values.GITEA_API_URL ?? 'http://localhost:5174/api/v1',
accessToken: values.GITEA_ACCESS_TOKEN ?? 'test_token',
},
feishu: {
webhookUrl: env.FEISHU_WEBHOOK_URL,
webhookSecret: env.FEISHU_WEBHOOK_SECRET,
webhookUrl: values.FEISHU_WEBHOOK_URL,
webhookSecret: values.FEISHU_WEBHOOK_SECRET,
},
app: {
port: env.PORT,
webhookSecret: env.WEBHOOK_SECRET,
port,
webhookSecret: values.WEBHOOK_SECRET ?? 'test_webhook_secret',
},
admin: {
password: env.ADMIN_PASSWORD,
jwtSecret: env.JWT_SECRET,
giteaAdminToken: env.GITEA_ADMIN_TOKEN,
password: values.ADMIN_PASSWORD ?? 'password',
jwtSecret: values.JWT_SECRET ?? 'a-secure-secret-for-jwt',
giteaAdminToken: values.GITEA_ADMIN_TOKEN,
},
review: {
engine: env.REVIEW_ENGINE,
workdir: env.REVIEW_WORKDIR,
customSummaryPrompt: env.CUSTOM_SUMMARY_PROMPT,
customLineCommentPrompt: env.CUSTOM_LINE_COMMENT_PROMPT,
globalPrompt: env.GLOBAL_PROMPT,
maxParallelRuns: env.REVIEW_MAX_PARALLEL_RUNS,
maxFilesPerRun: env.REVIEW_MAX_FILES_PER_RUN,
maxFileContentChars: env.REVIEW_MAX_FILE_CONTENT_CHARS,
autoPublishMinConfidence: env.REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE,
enableHumanGate: env.REVIEW_ENABLE_HUMAN_GATE,
allowedCommands: env.REVIEW_ALLOWED_COMMANDS.split(',')
.map((item) => item.trim())
.filter(Boolean),
commandTimeoutMs: env.REVIEW_COMMAND_TIMEOUT_MS,
qdrantUrl: env.QDRANT_URL,
enableMemory: env.ENABLE_MEMORY,
fewShotExamplesCount: env.FEW_SHOT_EXAMPLES_COUNT,
enableReflection: env.ENABLE_REFLECTION,
maxReflectionRounds: env.MAX_REFLECTION_ROUNDS,
enableDebate: env.ENABLE_DEBATE,
debateThreshold: env.DEBATE_THRESHOLD,
engine: values.REVIEW_ENGINE ?? 'legacy',
workdir: values.REVIEW_WORKDIR ?? '/tmp/gitea-assistant',
customSummaryPrompt: values.CUSTOM_SUMMARY_PROMPT,
customLineCommentPrompt: values.CUSTOM_LINE_COMMENT_PROMPT,
globalPrompt: values.GLOBAL_PROMPT,
maxParallelRuns: toNumber('REVIEW_MAX_PARALLEL_RUNS', 2),
maxFilesPerRun: toNumber('REVIEW_MAX_FILES_PER_RUN', 200),
maxFileContentChars: toNumber('REVIEW_MAX_FILE_CONTENT_CHARS', 40000),
autoPublishMinConfidence: toNumber('REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE', 0.8),
enableHumanGate: toBoolean('REVIEW_ENABLE_HUMAN_GATE', true),
allowedCommands: toStringArray('REVIEW_ALLOWED_COMMANDS', [
'git',
'rg',
'cat',
'sed',
'wc',
]),
commandTimeoutMs: toNumber('REVIEW_COMMAND_TIMEOUT_MS', 10000),
qdrantUrl: values.QDRANT_URL,
enableMemory: toBoolean('ENABLE_MEMORY', false),
fewShotExamplesCount: toNumber('FEW_SHOT_EXAMPLES_COUNT', 10),
enableReflection: toBoolean('ENABLE_REFLECTION', false),
maxReflectionRounds: toNumber('MAX_REFLECTION_ROUNDS', 2),
enableDebate: toBoolean('ENABLE_DEBATE', false),
debateThreshold: values.DEBATE_THRESHOLD ?? 'high',
},
};
}
/** Return raw overrides record. */
getOverrides(): Record<string, string> {
return { ...this.overrides };
}
/**
* Merge updates into overrides and persist.
* If a value is empty string `''`, that key is deleted (reset to lower layer).
*/
async setOverrides(updates: Record<string, string>): Promise<void> {
for (const [key, value] of Object.entries(updates)) {
const field = this.fieldsMap.get(key);
if (!field) {
continue;
}
if (value === '') {
delete this.overrides[key];
settingsRepo.delete(key);
} else {
this.overrides[key] = value;
settingsRepo.set(key, value, field.sensitive);
}
}
await this.persistOverrides();
}
/** Remove specified keys from overrides and persist. */
async resetKeys(keys: string[]): Promise<void> {
for (const key of keys) {
delete this.overrides[key];
settingsRepo.delete(key);
}
await this.persistOverrides();
}
/**
* Determine where the effective value for a given env key comes from.
*/
getSource(envKey: string): 'default' | 'env' | 'override' {
const ov = this.overrides[envKey];
if (ov !== undefined && ov !== '') {
return 'override';
getSource(envKey: string): 'default' | 'db' {
try {
return settingsRepo.get(envKey) !== null ? 'db' : 'default';
} catch {
return 'default';
}
const envVal = process.env[envKey];
if (envVal !== undefined && envVal !== '') {
return 'env';
}
seedDefaults(): void {
if (settingsRepo.listAll().length > 0) {
return;
}
for (const field of CONFIG_FIELDS) {
let value: string | undefined;
if (field.envKey === 'JWT_SECRET' || field.envKey === 'WEBHOOK_SECRET') {
value = randomBytes(32).toString('hex');
} else if (field.envKey === 'ADMIN_PASSWORD') {
value = 'password';
} else if (field.defaultValue !== undefined) {
value = this.defaultToString(field.defaultValue);
}
if (value !== undefined) {
settingsRepo.set(field.envKey, value, field.sensitive);
}
}
return 'default';
}
}

View File

@@ -7,7 +7,7 @@
// Types
// ---------------------------------------------------------------------------
export type ConfigGroup = 'gitea' | 'feishu' | 'app' | 'admin' | 'review' | 'memory';
export type ConfigGroup = 'gitea' | 'feishu' | 'security' | 'review' | 'memory';
export type ConfigFieldType = 'string' | 'number' | 'boolean' | 'url' | 'text' | 'enum';
@@ -18,8 +18,6 @@ export interface ConfigFieldMeta {
description: string;
type: ConfigFieldType;
sensitive: boolean;
readonly?: boolean;
readonlyWarning?: string;
enumValues?: string[];
min?: number;
max?: number;
@@ -51,15 +49,9 @@ export const CONFIG_GROUPS: ConfigGroupMeta[] = [
icon: 'bell',
},
{
key: 'app',
label: '应用',
description: '服务端口与 Webhook 安全',
icon: 'settings',
},
{
key: 'admin',
label: '管理后台',
description: '后台登录密码与 JWT 密钥',
key: 'security',
label: '安全设置',
description: 'Webhook、后台密码与 JWT 密钥',
icon: 'shield',
},
{
@@ -152,48 +144,32 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [
sensitive: true,
},
// ── 应用 ────────────────────────────────────────────────────────────────
{
envKey: 'PORT',
group: 'app',
label: '监听端口',
description: '服务监听的 HTTP 端口号,修改需通过 .env 配置并重启服务',
type: 'number',
sensitive: false,
readonly: true,
defaultValue: 5174,
},
{
envKey: 'WEBHOOK_SECRET',
group: 'app',
group: 'security',
label: 'Webhook 密钥',
description:
'用于验证 Gitea Webhook 请求来源的 HMAC 密钥,修改需通过 .env 配置并同步更新 Gitea',
description: '用于验证 Gitea Webhook 请求来源的 HMAC 密钥',
type: 'string',
sensitive: true,
readonly: true,
defaultValue: 'test_webhook_secret',
},
// ── 管理后台 ────────────────────────────────────────────────────────────
{
envKey: 'ADMIN_PASSWORD',
group: 'admin',
group: 'security',
label: '管理员密码',
description: '后台管理界面的登录密码',
type: 'string',
sensitive: true,
readonlyWarning: '修改后当前登录会话可能失效',
defaultValue: 'password',
},
{
envKey: 'JWT_SECRET',
group: 'admin',
group: 'security',
label: 'JWT 密钥',
description: '用于签发后台登录 Token 的密钥,修改需通过 .env 配置',
description: '用于签发后台登录 Token 的密钥',
type: 'string',
sensitive: true,
readonly: true,
defaultValue: 'a-secure-secret-for-jwt',
},

View File

@@ -1,5 +1,5 @@
import { Hono } from 'hono';
import { type AppConfig, configManager } from '../config/config-manager';
import { configManager } from '../config/config-manager';
import { CONFIG_FIELDS, CONFIG_GROUPS, type ConfigFieldMeta } from '../config/config-schema';
import { logger } from '../utils/logger';
@@ -9,7 +9,6 @@ const MASKED_VALUE = '••••••••';
/** Number fields that must be integers (decimal not allowed). */
const INTEGER_FIELDS = new Set([
'PORT',
'REVIEW_MAX_PARALLEL_RUNS',
'REVIEW_MAX_FILES_PER_RUN',
'REVIEW_MAX_FILE_CONTENT_CHARS',
@@ -23,83 +22,6 @@ const FIELDS_MAP = new Map<string, ConfigFieldMeta>(CONFIG_FIELDS.map((f) => [f.
// ── Helpers ──────────────────────────────────────────────────────────────────
/**
* Map an envKey to its effective value from the resolved AppConfig.
* Explicit switch — no dynamic property access.
*/
function getEffectiveValue(
envKey: string,
current: AppConfig
): string | number | boolean | undefined {
switch (envKey) {
// Gitea
case 'GITEA_API_URL':
return current.gitea.apiUrl;
case 'GITEA_ACCESS_TOKEN':
return current.gitea.accessToken;
case 'GITEA_ADMIN_TOKEN':
return current.admin.giteaAdminToken;
// Review prompts (moved from OpenAI group)
case 'CUSTOM_SUMMARY_PROMPT':
return current.review.customSummaryPrompt;
case 'CUSTOM_LINE_COMMENT_PROMPT':
return current.review.customLineCommentPrompt;
case 'GLOBAL_PROMPT':
return current.review.globalPrompt;
// Feishu
case 'FEISHU_WEBHOOK_URL':
return current.feishu.webhookUrl;
case 'FEISHU_WEBHOOK_SECRET':
return current.feishu.webhookSecret;
// App
case 'PORT':
return current.app.port;
case 'WEBHOOK_SECRET':
return current.app.webhookSecret;
// Admin
case 'ADMIN_PASSWORD':
return current.admin.password;
case 'JWT_SECRET':
return current.admin.jwtSecret;
// Review
case 'REVIEW_ENGINE':
return current.review.engine;
case 'REVIEW_WORKDIR':
return current.review.workdir;
case 'REVIEW_MAX_PARALLEL_RUNS':
return current.review.maxParallelRuns;
case 'REVIEW_MAX_FILES_PER_RUN':
return current.review.maxFilesPerRun;
case 'REVIEW_MAX_FILE_CONTENT_CHARS':
return current.review.maxFileContentChars;
case 'REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE':
return current.review.autoPublishMinConfidence;
case 'REVIEW_ENABLE_HUMAN_GATE':
return current.review.enableHumanGate;
case 'REVIEW_ALLOWED_COMMANDS':
return current.review.allowedCommands.join(',');
case 'REVIEW_COMMAND_TIMEOUT_MS':
return current.review.commandTimeoutMs;
// Memory
case 'QDRANT_URL':
return current.review.qdrantUrl;
case 'ENABLE_MEMORY':
return current.review.enableMemory;
case 'FEW_SHOT_EXAMPLES_COUNT':
return current.review.fewShotExamplesCount;
case 'ENABLE_REFLECTION':
return current.review.enableReflection;
case 'MAX_REFLECTION_ROUNDS':
return current.review.maxReflectionRounds;
case 'ENABLE_DEBATE':
return current.review.enableDebate;
case 'DEBATE_THRESHOLD':
return current.review.debateThreshold;
default:
return undefined;
}
}
/**
* Validate a single field value against its metadata.
* Returns an error message string, or `null` if valid.
@@ -157,13 +79,13 @@ export const configRouter = new Hono();
* effective values, and source. Sensitive fields are masked.
*/
configRouter.get('/', (c) => {
const current = configManager.getCurrent();
const rawValues = configManager.getAllRawValues();
const groups = CONFIG_GROUPS.map((group) => {
const groupFields = CONFIG_FIELDS.filter((f) => f.group === group.key);
const fields = groupFields.map((field) => {
const rawValue = getEffectiveValue(field.envKey, current);
const rawValue = rawValues[field.envKey];
const hasValue = rawValue !== undefined && rawValue !== '';
const source = configManager.getSource(field.envKey);
const value = field.sensitive && hasValue ? MASKED_VALUE : rawValue;
@@ -174,8 +96,6 @@ configRouter.get('/', (c) => {
description: field.description,
type: field.type,
sensitive: field.sensitive,
...(field.readonly && { readonly: true }),
...(field.readonlyWarning !== undefined && { readonlyWarning: field.readonlyWarning }),
...(field.enumValues !== undefined && { enumValues: field.enumValues }),
...(field.min !== undefined && { min: field.min }),
...(field.max !== undefined && { max: field.max }),
@@ -199,9 +119,9 @@ configRouter.get('/', (c) => {
});
/**
* PUT / — Validate and persist override updates.
* PUT / — Validate and persist configuration updates.
* Masked sentinel ('••••••••') for sensitive fields is silently skipped.
* Empty string '' causes the key to be reset (deleted from overrides).
* Empty string '' causes the key to be reset (deleted from DB).
*/
configRouter.put('/', async (c) => {
try {
@@ -221,11 +141,6 @@ configRouter.put('/', async (c) => {
errors.push(`未知配置项: ${key}`);
continue;
}
// Silently skip readonly fields — frontend sends entire form state
if (field.readonly) {
continue;
}
const value = String(rawValue ?? '');
// Skip masked sentinel for sensitive fields — do not overwrite with mask
@@ -262,7 +177,7 @@ configRouter.put('/', async (c) => {
});
/**
* POST /reset — Remove specified keys from overrides (revert to env / default).
* POST /reset — Remove specified keys from DB (revert to default).
*/
configRouter.post('/reset', async (c) => {
try {

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { serveStatic } from 'hono/bun';
import { jwt } from 'hono/jwt';
import config from './config';
import config, { configManager } from './config';
import { adminController } from './controllers/admin';
import { configRouter } from './controllers/config';
import { feedbackRouter, initializeFeedbackSystem } from './controllers/feedback';
@@ -11,6 +11,10 @@ import { initMasterKey } from './crypto/secrets';
import { initDatabase } from './db/database';
import { reviewEngine } from './review/engine';
initMasterKey();
initDatabase();
configManager.seedDefaults();
// 创建Hono应用实例
const app = new Hono();
@@ -31,7 +35,7 @@ app.get('/', (c) => {
},
signature: webhookSecretConfigured
? '签名验证已启用 (使用X-Gitea-Signature头)'
: '警告: 签名验证未配置,建议设置WEBHOOK_SECRET环境变量',
: '警告: 签名验证未配置,建议在管理后台设置 Webhook 密钥',
},
});
});
@@ -64,9 +68,6 @@ app.get('*', serveStatic({ path: './public/index.html' }));
const port = config.app.port;
console.log(`⚡️ 服务启动在 http://localhost:${port}`);
initMasterKey();
initDatabase();
reviewEngine.start().catch((error) => {
console.error('❌ 启动Agent Review Engine失败', error);
});