Echocq commited on
Commit
4e90688
·
verified ·
1 Parent(s): db14b73

feat: AI assistant bubble + multi-provider temp-mail + settings view

Browse files
src/server/runtime-config.ts CHANGED
@@ -215,10 +215,12 @@ export function getAiConfigStatus() {
215
  const baseUrl = getAiBaseUrl();
216
  const hasKey = Boolean(getAiApiKey());
217
  const fromSettings = Boolean(getSetting("ai.baseUrl") || getSetting("ai.apiKey"));
 
 
218
  return {
219
  configured: Boolean(baseUrl && hasKey),
220
  baseUrl: baseUrl ?? "",
221
- model: getAiModel(),
222
  hasKey,
223
  source: fromSettings ? "ui" : baseUrl || hasKey ? "env" : "none"
224
  };
 
215
  const baseUrl = getAiBaseUrl();
216
  const hasKey = Boolean(getAiApiKey());
217
  const fromSettings = Boolean(getSetting("ai.baseUrl") || getSetting("ai.apiKey"));
218
+ // 只回显用户/env 真正设过的模型;没设就返回空,避免 UI 凭空冒出 gpt-4o-mini 默认值。
219
+ // (实际调用 getAiModel() 仍有 DEFAULT_AI_MODEL 兜底,不影响气泡可用。)
220
  return {
221
  configured: Boolean(baseUrl && hasKey),
222
  baseUrl: baseUrl ?? "",
223
+ model: getSetting("ai.model") ?? config.AI_MODEL ?? "",
224
  hasKey,
225
  source: fromSettings ? "ui" : baseUrl || hasKey ? "env" : "none"
226
  };
src/web/src/components/AiBubble.tsx CHANGED
@@ -142,7 +142,7 @@ export function AiBubble() {
142
  </label>
143
  <label className="ai-field">
144
  <span>{T("模型", "Model")}</span>
145
- <input value={model} onChange={(e) => setModel(e.target.value)} placeholder="gpt-4o-mini" spellCheck={false} />
146
  </label>
147
  <label className="ai-field">
148
  <span>API Key {config?.hasKey ? T("(已存,留空不改)", "(saved, blank = keep)") : ""}</span>
 
142
  </label>
143
  <label className="ai-field">
144
  <span>{T("模型", "Model")}</span>
145
+ <input value={model} onChange={(e) => setModel(e.target.value)} placeholder={T("模型名(设置页可一键获取)", "model name")} spellCheck={false} />
146
  </label>
147
  <label className="ai-field">
148
  <span>API Key {config?.hasKey ? T("(已存,留空不改)", "(saved, blank = keep)") : ""}</span>
src/web/src/components/SettingsView.tsx CHANGED
@@ -44,6 +44,7 @@ export function SettingsView({
44
  const [endpoint, setEndpoint] = useState("");
45
  const [domain, setDomain] = useState("");
46
  const [password, setPassword] = useState("");
 
47
  const [busy, setBusy] = useState(false);
48
  const [test, setTest] = useState<Record<string, { ok: boolean; msg: string } | "loading">>({});
49
 
@@ -51,12 +52,26 @@ export function SettingsView({
51
  const editingProvider = editingExisting ? tempProviders.find((p) => p.id === editingId) : undefined;
52
 
53
  function openAdd() {
54
- setEditingId(NEW); setName(""); setType("php"); setEndpoint(""); setDomain(""); setPassword("");
55
  }
56
  function openEdit(p: TempProviderPublic) {
57
- setEditingId(p.id); setName(p.name); setType(p.type); setEndpoint(p.endpoint); setDomain(p.domain); setPassword("");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
- function cancel() { setEditingId(null); }
60
 
61
  async function save() {
62
  if (!name.trim() || !endpoint.trim()) return;
@@ -326,9 +341,14 @@ export function SettingsView({
326
  {type === "cf"
327
  ? L("管理员 auth(x-admin-auth 口令)", "Admin auth (x-admin-auth)")
328
  : L("管理员密码(X-Admin-Password)", "Admin password (X-Admin-Password)")}
 
 
 
 
 
329
  </span>
330
- <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password"
331
- placeholder={editingExisting ? (editingProvider?.hasPassword ? L("已设 · 留空不改(出于安全不回显", "set · blank = keep (never echoed)") : L("未设", "not set")) : L("必填", "required")} />
332
  </label>
333
  <div className="settings-form-foot">
334
  {type === "cf" && (
 
44
  const [endpoint, setEndpoint] = useState("");
45
  const [domain, setDomain] = useState("");
46
  const [password, setPassword] = useState("");
47
+ const [showPwd, setShowPwd] = useState(false);
48
  const [busy, setBusy] = useState(false);
49
  const [test, setTest] = useState<Record<string, { ok: boolean; msg: string } | "loading">>({});
50
 
 
52
  const editingProvider = editingExisting ? tempProviders.find((p) => p.id === editingId) : undefined;
53
 
54
  function openAdd() {
55
+ setEditingId(NEW); setName(""); setType("php"); setEndpoint(""); setDomain(""); setPassword(""); setShowPwd(false);
56
  }
57
  function openEdit(p: TempProviderPublic) {
58
+ setEditingId(p.id); setName(p.name); setType(p.type); setEndpoint(p.endpoint); setDomain(p.domain); setPassword(""); setShowPwd(false);
59
+ }
60
+ function cancel() { setEditingId(null); setShowPwd(false); }
61
+
62
+ // 编辑时回显当前密码:拉真实值填进框 + 切成明文(门后安全)。再点收回遮罩。
63
+ async function revealCurrentPwd() {
64
+ if (showPwd) { setShowPwd(false); return; }
65
+ if (!editingId || editingId === NEW) return;
66
+ try {
67
+ const s = await fetchSecrets();
68
+ const t = s.temp.find((x) => x.id === editingId);
69
+ if (t?.password) { setPassword(t.password); setShowPwd(true); }
70
+ else onError(L("该源未设密码", "no password set"));
71
+ } catch (e) {
72
+ onError(e instanceof Error ? e.message : String(e));
73
+ }
74
  }
 
75
 
76
  async function save() {
77
  if (!name.trim() || !endpoint.trim()) return;
 
341
  {type === "cf"
342
  ? L("管理员 auth(x-admin-auth 口令)", "Admin auth (x-admin-auth)")
343
  : L("管理员密码(X-Admin-Password)", "Admin password (X-Admin-Password)")}
344
+ {editingExisting && editingProvider?.hasPassword && (
345
+ <button type="button" className="mini-link" onClick={revealCurrentPwd}>
346
+ {showPwd ? L("🙈 遮罩", "🙈 hide") : L("👁 显示当前", "👁 show current")}
347
+ </button>
348
+ )}
349
  </span>
350
+ <input type={showPwd ? "text" : "password"} value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password"
351
+ placeholder={editingExisting ? (editingProvider?.hasPassword ? L("已设 · 留空不改,或点「显示当前」回显", "set · blank = keep, or click show current") : L("未设", "not set")) : L("必填", "required")} />
352
  </label>
353
  <div className="settings-form-foot">
354
  {type === "cf" && (