File size: 3,756 Bytes
cfea436
 
 
 
3c49634
cfea436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c49634
cfea436
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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<Record<string, any>>();
  const update: Record<string, any> = { 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);
}