import { Hono } from 'hono'; import { eq } from 'drizzle-orm'; import { settings } from '../db/schema'; import { backupDatabaseToRclone } from '../backup'; import { hashPassword } from '../password'; import type { Env, Variables } from '../index'; export const settingsApi = new Hono<{ Bindings: Env; Variables: Variables }>(); const DEFAULT_ID = 'default'; async function ensureRow(db: any) { const row = (await db.select().from(settings).where(eq(settings.id, DEFAULT_ID))).at(0); if (row) return row; await db.insert(settings).values({ id: DEFAULT_ID, updatedAt: Date.now() }); return (await db.select().from(settings).where(eq(settings.id, DEFAULT_ID))).at(0); } // 读取(API key 不返回明文,只返回是否已配置) settingsApi.get('/', async (c) => { const row = await ensureRow(c.var.db); let customSizes: string[] = []; try { customSizes = JSON.parse(row.customSizes || '[]'); } catch {} return c.json({ openaiBaseUrl: row.openaiBaseUrl || '', openaiApiKeySet: !!row.openaiApiKey, imageModel: row.imageModel || '', translateModel: row.translateModel || '', customSizes, defaultSize: row.defaultSize || '1024x1024', accessPasswordSet: !!row.accessPassword, jobPollInterval: row.jobPollInterval || 3000, }); }); // 保存(只更新提交的字段;API key 留空表示不变) settingsApi.put('/', async (c) => { const db = c.var.db; await ensureRow(db); const body = await c.req.json>(); const update: Record = { updatedAt: Date.now() }; if (body.openaiBaseUrl !== undefined) update.openaiBaseUrl = String(body.openaiBaseUrl).trim() || 'https://api.openai.com/v1'; if (body.imageModel !== undefined) update.imageModel = String(body.imageModel).trim() || 'gpt-image-2'; if (body.translateModel !== undefined) update.translateModel = String(body.translateModel).trim() || 'gpt-4o-mini'; if (body.defaultSize !== undefined) { const v = String(body.defaultSize).trim().toLowerCase().replace(/×/g, 'x'); if (/^\d{2,5}x\d{2,5}$/.test(v)) update.defaultSize = v; } if (body.customSizes !== undefined) { let arr: string[] = []; if (Array.isArray(body.customSizes)) arr = body.customSizes; else if (typeof body.customSizes === 'string') { arr = body.customSizes.split(/[\s,;\n]+/); } arr = arr .map((s) => String(s).trim().toLowerCase().replace(/×/g, 'x')) .filter((s) => /^\d{2,5}x\d{2,5}$/.test(s)); // 去重,过滤掉与预设重复的 const presets = new Set(['1024x1024', '1792x1024', '1024x1792']); arr = Array.from(new Set(arr)).filter((s) => !presets.has(s)); update.customSizes = JSON.stringify(arr); } if (body.jobPollInterval !== undefined) { const n = Number(body.jobPollInterval); if (Number.isFinite(n)) { update.jobPollInterval = Math.max(1000, Math.min(30000, Math.round(n))); } } // API key:仅当传入非空字符串时才更新,空串/未传不动 if (body.openaiApiKey && String(body.openaiApiKey).trim()) update.openaiApiKey = String(body.openaiApiKey).trim(); // 访问密码:null 表示清除;空字符串/未传 = 保持不变;其他 = 更新 if (body.accessPassword === null) { update.accessPassword = ''; } else if (typeof body.accessPassword === 'string' && body.accessPassword.trim()) { update.accessPassword = hashPassword(body.accessPassword.trim()); } await db.update(settings).set(update).where(eq(settings.id, DEFAULT_ID)); const backup = await backupDatabaseToRclone(); return c.json({ ok: true, backup }); }); // 内部使用:返回完整设置(含明文 key) export async function getFullSettings(db: any) { return await ensureRow(db); }