import { useEffect, useMemo, useRef, useState } from "react"; import { api, AppInfo, ModelInfo } from "../api"; import { useStore } from "../store"; export default function SettingsPage() { const [info, setInfo] = useState(null); const [settings, setSettings] = useState>({}); const [recommendDirDraft, setRecommendDirDraft] = useState(""); const [uploadMsg, setUploadMsg] = useState(null); const [startupMsg, setStartupMsg] = useState(null); const [autostart, setAutostart] = useState(null); const [autoLoadDraft, setAutoLoadDraft] = useState([]); const [pickId, setPickId] = useState(""); const uploadInputRef = useRef(null); const models = useStore((s) => s.models); const refreshModels = useStore((s) => s.refreshModels); useEffect(() => { api.appInfo().then(setInfo); api.settingsGet().then(setSettings); api.autostartStatus().then(setAutostart).catch(() => setAutostart(false)); refreshModels(); }, []); useEffect(() => { setRecommendDirDraft(settings.recommend_dir ?? ""); }, [settings.recommend_dir]); useEffect(() => { try { const parsed = JSON.parse(settings.auto_load_models ?? "[]"); setAutoLoadDraft(Array.isArray(parsed) ? parsed : []); } catch { setAutoLoadDraft([]); } }, [settings.auto_load_models]); async function handleSave(key: string, value: string) { await api.settingsSet(key, value); setSettings((prev) => ({ ...prev, [key]: value })); } async function handleAutostartToggle(checked: boolean) { try { setAutostart(await api.autostartSet(checked)); setStartupMsg(checked ? "已开启开机启动" : "已关闭开机启动"); } catch (e) { setStartupMsg(`开机启动设置失败:${String(e)}`); } } const localModels = useMemo( () => models.filter((m) => m.kind === "local"), [models], ); const draftRows = useMemo( () => autoLoadDraft .map((id) => localModels.find((m) => m.id === id)) .filter((m): m is ModelInfo => Boolean(m)), [autoLoadDraft, localModels], ); const available = useMemo( () => localModels.filter((m) => !autoLoadDraft.includes(m.id)), [localModels, autoLoadDraft], ); function moveRow(index: number, dir: -1 | 1) { setAutoLoadDraft((prev) => { const next = [...prev]; const target = index + dir; if (target < 0 || target >= next.length) return prev; [next[index], next[target]] = [next[target], next[index]]; return next; }); } function removeRow(index: number) { setAutoLoadDraft((prev) => prev.filter((_, i) => i !== index)); } async function handleSaveAutoLoad() { await handleSave("auto_load_models", JSON.stringify(draftRows.map((m) => m.id))); } function modelDisplayName(m: ModelInfo) { return m.file_name.replace(/\.gguf$/i, ""); } async function handleUpload(file: File) { setUploadMsg(null); try { const text = await file.text(); await api.importRecommendations(file.name, text); setUploadMsg(`已导入 ${file.name}`); } catch (e) { setUploadMsg(String(e)); } finally { if (uploadInputRef.current) uploadInputRef.current.value = ""; } } return (

设置

运行路径与引擎配置

关于
版本:{info?.version}({info?.platform})
数据目录:{info?.db_path}
日志目录:{info?.logs_dir} {info?.logs_dir ? ( ) : null}
引擎可用: {info?.engine_exists ? ( ) : ( 否(请配置 llama-server 路径) )}
引擎与路径
handleSave("model_dir", v)} /> handleSave("engine_bin", v)} /> handleSave("hf_endpoint", v)} /> handleSave("backend", v)} /> handleSave("upload_max_mb", v)} />
启动
{startupMsg ? (
{startupMsg}
) : null}
开机启动
登录 Windows 后自动启动仙人工作室
自动加载模型
应用启动后按列表顺序逐个加载本地模型;由于同一时间只能运行一个模型,全部加载完成后保留列表最后一个在运行
{draftRows.length === 0 ? (
还没有添加模型,从下方下拉框选择要自动加载的本地模型
) : ( draftRows.map((m, i) => (
{i + 1} {modelDisplayName(m)}
)) )}
列表顺序即加载顺序,可拖动上下按钮调整
对话
自动生成对话标题
首次对话生成回复后,自动为会话生成标题并覆盖
handleSave("auto_title", checked ? "true" : "false") } />
自动预测用户接下来说的话
大模型回答完成后自动预测几条用户可能要说的话,点击即可填入输入框
handleSave("suggest_enabled", checked ? "true" : "false") } />
{ const n = Math.max(1, Math.min(5, Number(v) || 3)); handleSave("suggest_count", String(n)); }} />
推荐模型列表
JSON 文件目录(软件自带默认列表,上传的 JSON 也会保存到这里)
setRecommendDirDraft(e.target.value)} />
{ if (e.target.files && e.target.files.length > 0) { handleUpload(e.target.files[0]); } }} /> 可上传多个推荐列表 JSON,同名文件将覆盖
{uploadMsg ? (
{uploadMsg}
) : null}
JSON 格式示例:
              {`{
  "models": [
    {
      "id": "Qwen/Qwen2.5-7B-Instruct-GGUF",
      "source": "huggingface",
      "params": "7B",
      "architecture": "dense",
      "vision": false,
      "files": [
        {
          "path": "xx-q4_k_m.gguf",
          "quant": "Q4_K_M",
          "size_gb": 4.68,
          "links": [
            "https://hf-mirror.com/Qwen/Qwen2.5-7B-Instruct-GGUF/resolve/main/xx-q4_k_m.gguf",
            "https://modelscope.cn/models/Qwen/Qwen2.5-7B-Instruct-GGUF/resolve/master/xx-q4_k_m.gguf"
          ]
        }
      ]
    }
  ]
}`}
            
获取 llama-server:运行 scripts/fetch-llama.ps1{" "} 自动下载预编译引擎,或手动指定已下载的 exe 路径。
); } function Toggle({ checked, onChange, disabled = false, }: { checked: boolean; onChange: (checked: boolean) => void; disabled?: boolean; }) { return ( ); } function SettingInput({ label, value, onSave, }: { label: string; value: string; onSave: (v: string) => void; }) { const [draft, setDraft] = useState(value); useEffect(() => setDraft(value), [value]); return ( ); }