feat(voice): 提升桌面端语音交互与本地任务执行支持
This commit is contained in:
@@ -2,7 +2,8 @@ import { useEffect, useState } from "react";
|
||||
import { Dialog } from "../ui/Dialog";
|
||||
import { Button } from "../ui/Button";
|
||||
import { useToast } from "../ui/Toast";
|
||||
import { getMyJarvis, saveMyJarvis, type JarvisConfig } from "../lib/api";
|
||||
import { getMyJarvis, saveMyJarvis, type JarvisConfig, GATEWAY, getToken } from "../lib/api";
|
||||
import { localRunnerAvailable, localRunnerStatus, startLocalRunner, stopLocalRunner } from "../lib/desktop";
|
||||
|
||||
// 每用户 JARVIS 设置:名字 / 人设 / (高级)自带豆包配置。
|
||||
// 名字与人设归用户自己;豆包配置齐全则语音走用户的账号,否则走系统兜底。
|
||||
@@ -105,12 +106,85 @@ export function JarvisSettings({ open, onClose }: { open: boolean; onClose: () =
|
||||
<Field label="音色 voice_type" value={cfg.tts_voice_type} onChange={(v) => set("tts_voice_type", v)} placeholder="zh_male_m191_uranus_bigtts" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LocalAccessSection />
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// LocalAccessSection 本地文件访问(JARVIS「本地的手」,只读起步):用户显式选目录 + 开启,
|
||||
// JARVIS 才能看/读该目录内的文件;随时可关。仅桌面壳可用(浏览器隐藏整个区块)。
|
||||
function LocalAccessSection() {
|
||||
const toast = useToast();
|
||||
const [dir, setDir] = useState("");
|
||||
const [status, setStatus] = useState("offline");
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localRunnerAvailable()) return;
|
||||
let alive = true;
|
||||
const poll = () => localRunnerStatus().then((s) => alive && setStatus(s)).catch(() => {});
|
||||
poll();
|
||||
const iv = window.setInterval(poll, 2000);
|
||||
return () => {
|
||||
alive = false;
|
||||
window.clearInterval(iv);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!localRunnerAvailable()) return null;
|
||||
|
||||
const online = status.startsWith("online");
|
||||
const onToggle = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
if (online || status === "connecting") {
|
||||
stopLocalRunner();
|
||||
setStatus("offline");
|
||||
} else {
|
||||
if (!dir.trim()) throw new Error("先填一个允许 JARVIS 访问的本地目录(绝对路径)");
|
||||
await startLocalRunner(GATEWAY, getToken(), dir.trim());
|
||||
setStatus("connecting");
|
||||
toast.push("success", "本地文件访问已开启(只读,锁定在该目录内)");
|
||||
}
|
||||
} catch (e) {
|
||||
toast.push("error", (e as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-md border border-line bg-ink-900/50 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400">本地文件访问(试验 · 只读)</span>
|
||||
<span className={online ? "text-[11px] text-emerald-400" : "text-[11px] text-slate-500"}>
|
||||
{online ? "已开启" : status === "connecting" ? "连接中…" : "未开启"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] leading-relaxed text-slate-500">
|
||||
开启后 JARVIS 能<b>查看/读取</b>下面这个目录里的文件(仅此目录,不能写、不能执行命令)。
|
||||
对话里可以说“看看我工作目录里有什么”。
|
||||
</p>
|
||||
{online ? (
|
||||
<p className="break-all font-mono text-[11px] text-slate-400">{status.slice("online:".length)}</p>
|
||||
) : (
|
||||
<input
|
||||
className="w-full rounded-md border border-line bg-ink-850 px-3 py-1.5 font-mono text-sm text-slate-100 focus:border-brand focus:outline-none"
|
||||
value={dir}
|
||||
onChange={(e) => setDir(e.target.value)}
|
||||
placeholder="/Users/你/Documents/某个目录"
|
||||
/>
|
||||
)}
|
||||
<Button variant={online ? "ghost" : "secondary"} size="sm" onClick={onToggle} disabled={busy}>
|
||||
{online || status === "connecting" ? "关闭本地访问" : "开启本地访问"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label,
|
||||
value,
|
||||
|
||||
Reference in New Issue
Block a user