wechat_ai/src/pages/ClonePage.jsx

235 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}