feat(hololake): complete public runtime loop
This commit is contained in:
parent
71cece74b0
commit
92cce27973
26 changed files with 2626 additions and 79 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue