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:
|
| 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="
|
| 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("已设 · 留空不改
|
| 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" && (
|