235 lines
11 KiB
JavaScript
235 lines
11 KiB
JavaScript
import { useEffect, useState } from "react";
|
||
import { invoke } from "@tauri-apps/api/core";
|
||
import { listen } from "@tauri-apps/api/event";
|
||
import { Crosshair, Power } from "lucide-react";
|
||
import NodeSphere from "../components/NodeSphere";
|
||
import Terminal from "../components/Terminal";
|
||
import { openWindow } from "../utils/navigation";
|
||
|
||
const engineStatusContent = {
|
||
idle: { title: "微信引擎未运行", description: "启动后等待并处理本地微信消息。" },
|
||
running: { title: "微信引擎运行中", description: "已处理 26 条消息 · 当前队列 0" },
|
||
error: { title: "微信引擎运行异常", description: "启动失败,请查看运行日志并检查本地配置。" },
|
||
};
|
||
|
||
const requiredAnnotationTypes = ["contact_list", "chat_content", "input_box", "unread_message"];
|
||
const initialChecks = [
|
||
{ id: "model", label: "大模型连接情况", detail: "配置完整,连接延迟 324ms", status: "waiting", passed: true },
|
||
{ id: "annotation", label: "微信区域标注情况", detail: "联系人、聊天内容、输入框和发送区域完整", status: "waiting", passed: true },
|
||
{ id: "window", label: "微信窗口连接情况", detail: "已找到 WeChat 主窗口", status: "waiting", passed: true },
|
||
];
|
||
|
||
function hasCompleteAnnotation(annotation) {
|
||
const regionTypes = new Set(annotation?.regions?.map((region) => region.type) || []);
|
||
return requiredAnnotationTypes.every((regionType) => regionTypes.has(regionType));
|
||
}
|
||
|
||
const wait = (duration) => new Promise((resolve) => setTimeout(resolve, duration));
|
||
|
||
export default function ClonePage() {
|
||
const [engineStatus, setEngineStatus] = useState("idle");
|
||
const [engineBusy, setEngineBusy] = useState(false);
|
||
const [annotationBusy, setAnnotationBusy] = useState(false);
|
||
const [annotationComplete, setAnnotationComplete] = useState(false);
|
||
const [agentLogs, setAgentLogs] = useState([{ level: "info", message: "[12:30:01] 等待启动" }]);
|
||
const [checkOpen, setCheckOpen] = useState(false);
|
||
const [checks, setChecks] = useState(initialChecks);
|
||
const [checkPhase, setCheckPhase] = useState("idle");
|
||
const [autoSendPaused, setAutoSendPaused] = useState(false);
|
||
const engineRunning = engineStatus === "running";
|
||
const statusContent = engineStatusContent[engineStatus];
|
||
|
||
useEffect(() => {
|
||
if (!window.__TAURI_INTERNALS__) return undefined;
|
||
let unlisten;
|
||
let cancelled = false;
|
||
listen("engine-state-changed", (event) => {
|
||
const running = event.payload?.enabled === true;
|
||
setEngineStatus(running ? "running" : "idle");
|
||
if (!running) setAutoSendPaused(false);
|
||
setEngineBusy(false);
|
||
}).then((cleanup) => { if (cancelled) cleanup(); else unlisten = cleanup; });
|
||
return () => { cancelled = true; if (unlisten) unlisten(); };
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!window.__TAURI_INTERNALS__) return undefined;
|
||
let unlisten;
|
||
let cancelled = false;
|
||
async function observeAnnotationCompletion() {
|
||
const cleanup = await listen("annotation-completion-changed", (event) => setAnnotationComplete(event.payload === true));
|
||
if (cancelled) { cleanup(); return; }
|
||
unlisten = cleanup;
|
||
const annotation = await invoke("load_regions").catch(() => null);
|
||
if (!cancelled) setAnnotationComplete(hasCompleteAnnotation(annotation));
|
||
}
|
||
void observeAnnotationCompletion();
|
||
return () => { cancelled = true; if (unlisten) unlisten(); };
|
||
}, []);
|
||
|
||
async function toggleAgent() {
|
||
if (engineBusy) return false;
|
||
if (!window.__TAURI_INTERNALS__) {
|
||
setEngineStatus(engineRunning ? "idle" : "running");
|
||
setAutoSendPaused(false);
|
||
setAgentLogs((current) => [...current, { level: "info", message: engineRunning ? "[12:38:30] 引擎已停止" : "[12:38:21] 引擎启动成功" }]);
|
||
return true;
|
||
}
|
||
setEngineBusy(true);
|
||
try {
|
||
if (engineRunning) {
|
||
await invoke("stop_agent");
|
||
await invoke("stop_vision_stream");
|
||
setEngineStatus("idle");
|
||
setAutoSendPaused(false);
|
||
setAgentLogs((current) => [...current.slice(-300), { level: "warning", message: "引擎已停用,暂未读取 Rust 日志" }]);
|
||
} else {
|
||
await invoke("start_vision_stream");
|
||
try { await invoke("start_agent"); } catch (error) { await invoke("stop_vision_stream").catch(() => {}); throw new Error(`agent 启动失败:${String(error)}`); }
|
||
setEngineStatus("running");
|
||
setAutoSendPaused(false);
|
||
setAgentLogs((current) => [...current.slice(-300), { level: "info", message: "引擎已启动,暂未读取 Rust 日志" }]);
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
setEngineStatus("error");
|
||
setAgentLogs((current) => [...current, { level: "error", message: String(error) }]);
|
||
return false;
|
||
} finally {
|
||
setEngineBusy(false);
|
||
}
|
||
}
|
||
|
||
function toggleAutoSend() {
|
||
setAutoSendPaused((paused) => {
|
||
const nextPaused = !paused;
|
||
setAgentLogs((current) => [
|
||
...current.slice(-300),
|
||
{
|
||
level: nextPaused ? "warning" : "info",
|
||
message: nextPaused ? "已暂停自动发送" : "已恢复自动发送",
|
||
},
|
||
]);
|
||
return nextPaused;
|
||
});
|
||
}
|
||
|
||
function requestToggle() {
|
||
if (engineRunning) { void toggleAgent(); return; }
|
||
setChecks(initialChecks);
|
||
setCheckPhase("idle");
|
||
setCheckOpen(true);
|
||
setTimeout(runStartupChecks, 220);
|
||
}
|
||
|
||
async function runStartupChecks() {
|
||
setCheckPhase("checking");
|
||
setChecks(initialChecks);
|
||
for (let index = 0; index < initialChecks.length; index += 1) {
|
||
setChecks((items) => items.map((item, itemIndex) => ({ ...item, status: itemIndex === index ? "checking" : itemIndex < index ? "passed" : "waiting" })));
|
||
await wait(950);
|
||
const check = initialChecks[index];
|
||
if (!check.passed) {
|
||
setChecks((items) => items.map((item, itemIndex) => ({ ...item, status: itemIndex === index ? "failed" : item.status })));
|
||
setCheckPhase("failed");
|
||
await wait(1600);
|
||
setCheckOpen(false);
|
||
return;
|
||
}
|
||
setChecks((items) => items.map((item, itemIndex) => ({ ...item, status: itemIndex === index ? "passed" : item.status })));
|
||
}
|
||
setCheckPhase("starting");
|
||
const started = await toggleAgent();
|
||
if (started) { setCheckPhase("passed"); await wait(900); setCheckOpen(false); } else { setCheckPhase("failed-start"); await wait(1600); setCheckOpen(false); }
|
||
}
|
||
|
||
async function startAnnotation() {
|
||
if (annotationBusy) return;
|
||
if (!window.__TAURI_INTERNALS__) { setAgentLogs((current) => [...current.slice(-300), { level: "warning", message: "窗口选择标注请在 Tauri Windows 应用中使用" }]); return; }
|
||
setAnnotationBusy(true);
|
||
try { await invoke("enter_window_select_mode"); setAgentLogs((current) => [...current.slice(-300), { level: "info", message: "已进入窗口选择模式,请点击需要标注的窗口" }]); }
|
||
catch (error) { setAgentLogs((current) => [...current.slice(-300), { level: "error", message: String(error) }]); }
|
||
finally { setAnnotationBusy(false); }
|
||
}
|
||
|
||
return (
|
||
<section className="flex min-h-full flex-1 flex-col gap-3">
|
||
<div className="engine-visual flex flex-col items-center gap-5 px-2 py-2">
|
||
<NodeSphere status={engineStatus} />
|
||
<div className="text-center" aria-live="polite"><h2 className="text-[22px] font-extrabold">{statusContent.title}</h2><p className="mt-2 text-[13px] text-muted">{statusContent.description}</p></div>
|
||
{engineRunning ? (
|
||
<div className="grid w-full grid-cols-2 gap-2">
|
||
<button className="btn-secondary h-10 w-full" onClick={toggleAutoSend} disabled={engineBusy}>
|
||
{autoSendPaused ? "恢复自动发送" : "暂停自动发送"}
|
||
</button>
|
||
<button
|
||
className="btn-danger h-10 w-full"
|
||
onClick={() => void toggleAgent()}
|
||
disabled={engineBusy}
|
||
>
|
||
停止引擎
|
||
</button>
|
||
</div>
|
||
) : annotationComplete ? (
|
||
<button className="btn-primary w-full gap-2" onClick={requestToggle} disabled={engineBusy}>
|
||
<Power size={16} />
|
||
{engineBusy ? "处理中" : engineStatus === "error" ? "重新启动" : "启动引擎"}
|
||
</button>
|
||
) : (
|
||
<button className="btn-primary w-full gap-2" onClick={startAnnotation} disabled={annotationBusy}>
|
||
<Crosshair size={16} />
|
||
{annotationBusy ? "正在打开" : "标注微信"}
|
||
</button>
|
||
)}
|
||
</div>
|
||
<Terminal title={engineRunning ? "最近日志" : "运行日志"} lines={agentLogs} onViewAll={() => openWindow("/window/engine-logs")} onClear={() => setAgentLogs([])} />
|
||
{checkOpen ? <StartupCheckModal checks={checks} phase={checkPhase} /> : null}
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function StartupCheckModal({ checks, phase }) {
|
||
const visibleChecks = checks.filter((item) => item.status !== "waiting");
|
||
const progress = phase === "starting" || phase === "passed"
|
||
? 100
|
||
: Math.round((visibleChecks.length / checks.length) * 100);
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 grid place-items-center bg-black/55 p-3 backdrop-blur-[2px]">
|
||
<div className="startup-check-terminal w-full max-w-[520px] overflow-hidden shadow-2xl" aria-live="polite">
|
||
<div
|
||
className="startup-check-progress"
|
||
role="progressbar"
|
||
aria-label="启动检查进度"
|
||
aria-valuemin="0"
|
||
aria-valuemax="100"
|
||
aria-valuenow={progress}
|
||
>
|
||
<div className="startup-check-progress-bar" style={{ width: `${progress}%` }} />
|
||
</div>
|
||
|
||
<div className="startup-check-body">
|
||
<p className="startup-check-intro">
|
||
正在检测引擎启动环境<span className="startup-log-cursor" aria-hidden="true">_</span>
|
||
</p>
|
||
|
||
<div className="space-y-2">
|
||
{visibleChecks.map((item) => (
|
||
<div key={item.id} className={`startup-check-line startup-log-enter startup-check-${item.status}`}>
|
||
<span className="shrink-0">{item.label}</span>
|
||
<span className="startup-check-leader" aria-hidden="true" />
|
||
<span className="shrink-0 font-bold">
|
||
{item.status === "checking" ? "检测中..." : item.status === "passed" ? "成功" : "失败"}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{phase === "starting" ? <p className="startup-check-summary startup-log-enter">全部检查成功,正在启动引擎...</p> : null}
|
||
{phase === "failed" || phase === "failed-start" ? <p className="startup-check-error startup-log-enter">检查失败,启动流程已终止。</p> : null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|