feat(hololake): complete public runtime loop

This commit is contained in:
冰朔 2026-09-03 19:44:19 +08:00
commit 92cce27973
26 changed files with 2626 additions and 79 deletions

View file

@ -1,12 +1,14 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { listen } from "@tauri-apps/api/event";
import { Icon } from "./icons";
import * as api from "./runtime";
import type {
KnowledgeDocument,
KnowledgeSummary,
SourceKind,
RuntimeOverview,
SystemSnapshot,
} from "./types";
import {
@ -34,6 +36,7 @@ const nav: [View, string, string][] = [
const labels: Record<SourceKind, string> = {
USER_MESSAGE: "用户语言",
PERSONA_RESPONSE: "人格回应",
EXTERNAL_AI_MESSAGE: "外部 AI",
SYSTEM_CONTEXT: "系统环境",
PROTOCOL_EVENT: "协议运行",
AGENT_ACTION: "Agent 执行",
@ -43,6 +46,7 @@ const labels: Record<SourceKind, string> = {
export default function App() {
const [data, setData] = useState<SystemSnapshot | null>(null),
[runtime, setRuntime] = useState<RuntimeOverview | null>(null),
[view, setView] = useState<View>("channel"),
[error, setError] = useState(""),
[busy, setBusy] = useState(false);
@ -51,8 +55,23 @@ export default function App() {
.snapshot()
.then(setData)
.catch((e) => setError(String(e)));
useEffect(() => {
const refreshRuntime = () =>
api
.runtimeOverview()
.then(setRuntime)
.catch((e) => setError(String(e)));
const refreshAll = () => {
refresh();
refreshRuntime();
};
useEffect(() => {
refreshAll();
if (!("__TAURI_INTERNALS__" in window)) return;
let unlisten: (() => void) | undefined;
listen("hololake-runtime-event", () => refreshAll()).then((dispose) => {
unlisten = dispose;
});
return () => unlisten?.();
}, []);
if (!data)
return (
@ -61,7 +80,7 @@ export default function App() {
HoloLake
</div>
);
if (!data.channel) return <Onboarding onDone={refresh} error={error} />;
if (!data.channel) return <Onboarding onDone={refreshAll} error={error} />;
return (
<div className="app-shell">
<div className="titlebar">
@ -104,7 +123,8 @@ export default function App() {
{view === "channel" && (
<Channel
data={data}
refresh={refresh}
runtime={runtime}
refresh={refreshAll}
openKnowledge={() => setView("knowledge")}
/>
)}{" "}
@ -115,7 +135,8 @@ export default function App() {
{view === "settings" && (
<Settings
data={data}
refresh={refresh}
runtime={runtime}
refresh={refreshAll}
busy={busy}
setBusy={setBusy}
setError={setError}
@ -171,10 +192,12 @@ function Onboarding({ onDone, error }: { onDone: () => void; error: string }) {
function Channel({
data,
runtime,
refresh,
openKnowledge,
}: {
data: SystemSnapshot;
runtime: RuntimeOverview | null;
refresh: () => void;
openKnowledge: () => void;
}) {
@ -210,14 +233,19 @@ function Channel({
.filter(
(e) =>
e.sourceKind === "USER_MESSAGE" ||
e.sourceKind === "PERSONA_RESPONSE",
e.sourceKind === "PERSONA_RESPONSE" ||
e.sourceKind === "EXTERNAL_AI_MESSAGE",
)
.slice(0, 8)
.reverse()
.map((e) => (
<article
className={
e.sourceKind === "USER_MESSAGE" ? "human" : "persona"
e.sourceKind === "USER_MESSAGE"
? "human"
: e.sourceKind === "PERSONA_RESPONSE"
? "persona"
: "external-ai"
}
key={e.eventId}
>
@ -226,6 +254,52 @@ function Channel({
</article>
))}
</section>
{runtime &&
runtime.proposals.some(
(proposal) => proposal.state === "PENDING_HUMAN_APPROVAL",
) && (
<section className="approval-queue">
<h2> Agent </h2>
{runtime.proposals
.filter(
(proposal) => proposal.state === "PENDING_HUMAN_APPROVAL",
)
.map((proposal) => (
<article key={proposal.proposalId}>
<div>
<strong>{proposal.gir.programId}</strong>
<p>
{proposal.gir.actions
.map((action) => action.operation)
.join(" → ")}
</p>
<small>
{proposal.clientId} · {proposal.proposalId}
</small>
</div>
<div>
<button
className="soft"
onClick={async () => {
await api.rejectProposal(proposal.proposalId);
refresh();
}}
>
</button>
<button
onClick={async () => {
await api.approveProposal(proposal.proposalId);
refresh();
}}
>
</button>
</div>
</article>
))}
</section>
)}
<ReceiptStream data={data} />
<div className="composer">
<textarea
@ -256,6 +330,23 @@ function Channel({
<strong></strong>
<p className="path">{data.channel?.privateGitPath}</p>
</article>
<article>
<h2></h2>
<strong>
{runtime?.persona.personas[0]?.displayName ?? "尚未建立"}
</strong>
<p>
{runtime?.persona.personas[0]?.state ?? "不会自动生成默认人格"}
</p>
</article>
<article>
<h2>GLP </h2>
<strong>
{runtime?.realtime.state ?? "正在启动"} ·{" "}
{runtime?.realtime.connectedClients ?? 0}
</strong>
<p>{runtime?.realtime.endpoint ?? "仅监听本机回环地址"}</p>
</article>
<article>
<h2></h2>
<strong>
@ -276,7 +367,9 @@ function ReceiptStream({ data }: { data: SystemSnapshot }) {
const rows = data.timeline
.filter(
(e) =>
e.sourceKind !== "USER_MESSAGE" && e.sourceKind !== "PERSONA_RESPONSE",
e.sourceKind !== "USER_MESSAGE" &&
e.sourceKind !== "PERSONA_RESPONSE" &&
e.sourceKind !== "EXTERNAL_AI_MESSAGE",
)
.slice(0, 5);
return (
@ -839,18 +932,22 @@ function Portal() {
}
function Settings({
data,
runtime,
refresh,
busy,
setBusy,
setError,
}: {
data: SystemSnapshot;
runtime: RuntimeOverview | null;
refresh: () => void;
busy: boolean;
setBusy: (v: boolean) => void;
setError: (v: string) => void;
}) {
const [bridgeName, setBridgeName] = useState("");
const [bridgeName, setBridgeName] = useState(""),
[personaName, setPersonaName] = useState(""),
[runtimeMessage, setRuntimeMessage] = useState("");
const update = async () => {
setBusy(true);
try {
@ -894,7 +991,11 @@ function Settings({
<article className="bridge">
<div>
<h2> AI</h2>
<p></p>
<p>
{runtime?.realtime.endpoint ?? "本机实时桥正在启动"} ·{" "}
{runtime?.realtime.connectedClients ?? 0}{" "}
</p>
</div>
<div>
<input
@ -915,17 +1016,95 @@ function Settings({
</button>
</div>
</article>
<article className="bridge">
<div>
<h2></h2>
<p>
{runtime?.persona.personas.length
? `${runtime.persona.personas.length} 个本频道试用人格;既有人格验证数 ${runtime.persona.verifiedExistingPersonaCount}`
: "建立一个本频道可逆试用人格,再由外部 AI 宿主实时承载。"}
</p>
</div>
<div>
<input
value={personaName}
onChange={(e) => setPersonaName(e.target.value)}
placeholder="给试用人格起名"
/>
<button
onClick={async () => {
if (personaName.trim()) {
const persona = await api.registerPersona(personaName.trim());
setPersonaName("");
setRuntimeMessage(
`${persona.displayName} 已建立;尚未验证为既有历史人格。`,
);
refresh();
}
}}
>
</button>
</div>
</article>
<article>
<div>
<h2></h2>
<p>
{runtimeMessage ||
"先建立一个外部 AI 入口,然后复制本机 GLP 接入命令给外部编程 AI。"}
</p>
</div>
<button
className="soft"
disabled={!data.externalAiBridges.length}
onClick={async () => {
const invitation = await api.realtimeInvitation();
const bridge = data.externalAiBridges[0];
const persona = runtime?.persona.personas[0];
const command = `${invitation.connectorCommand} --bridge-id ${bridge.bridgeId}${persona ? ` --persona-id ${persona.personaId}` : ""}`;
await navigator.clipboard.writeText(command);
setRuntimeMessage(
"实时接入命令已复制。令牌只在本机 descriptor 文件中。",
);
}}
>
</button>
</article>
{data.externalAiBridges.map((b) => (
<article key={b.bridgeId}>
<div>
<h2>{b.displayName}</h2>
<p>
{b.bridgeId} · {b.inboxPath}
{b.bridgeId} ·{" "}
{runtime?.realtime.protocol ?? "GLP_LOCAL_REALTIME/1"}
</p>
</div>
<span className="status">{b.state}</span>
</article>
))}
{runtime?.persona.personas.map((persona) => (
<article key={persona.personaId}>
<div>
<h2>{persona.displayName}</h2>
<p>
{persona.personaId} · {persona.state}
</p>
</div>
<button
className="soft danger"
onClick={async () => {
if (confirm(`删除试用人格 ${persona.personaId}`)) {
await api.deleteTrialPersona(persona.personaId);
refresh();
}
}}
>
</button>
</article>
))}
</section>
</div>
);