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,53 +1,293 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { Channel, ExternalAiBridge, KnowledgeDocument, MutationReceipt, SystemSnapshot, TimelineEvent } from "./types";
|
||||
import type {
|
||||
AgentProposal,
|
||||
AgentReceipt,
|
||||
Channel,
|
||||
ExternalAiBridge,
|
||||
KnowledgeDocument,
|
||||
MutationReceipt,
|
||||
PublicPersona,
|
||||
RealtimeInvitation,
|
||||
RuntimeOverview,
|
||||
SystemSnapshot,
|
||||
TimelineEvent,
|
||||
} from "./types";
|
||||
|
||||
const tauri = () => "__TAURI_INTERNALS__" in window;
|
||||
const now = () => new Date().toISOString();
|
||||
const key = "hololake.clean.browser-state.v1";
|
||||
const runtimeKey = "hololake.clean.browser-runtime.v1";
|
||||
|
||||
type BrowserState = { channel: Channel | null; timeline: TimelineEvent[]; documents: KnowledgeDocument[]; bridges: ExternalAiBridge[] };
|
||||
const empty: BrowserState = { channel: null, timeline: [], documents: [], bridges: [] };
|
||||
const load = (): BrowserState => JSON.parse(localStorage.getItem(key) ?? JSON.stringify(empty));
|
||||
const save = (state: BrowserState) => localStorage.setItem(key, JSON.stringify(state));
|
||||
const event = (sourceKind: TimelineEvent["sourceKind"], title: string, content: string, state: TimelineEvent["state"] = "SUCCEEDED"): TimelineEvent => ({ eventId: crypto.randomUUID(), sourceKind, title, content, occurredAt: now(), state });
|
||||
type BrowserState = {
|
||||
channel: Channel | null;
|
||||
timeline: TimelineEvent[];
|
||||
documents: KnowledgeDocument[];
|
||||
bridges: ExternalAiBridge[];
|
||||
};
|
||||
const empty: BrowserState = {
|
||||
channel: null,
|
||||
timeline: [],
|
||||
documents: [],
|
||||
bridges: [],
|
||||
};
|
||||
const load = (): BrowserState =>
|
||||
JSON.parse(localStorage.getItem(key) ?? JSON.stringify(empty));
|
||||
const save = (state: BrowserState) =>
|
||||
localStorage.setItem(key, JSON.stringify(state));
|
||||
const loadRuntime = (): { personas: PublicPersona[] } =>
|
||||
JSON.parse(localStorage.getItem(runtimeKey) ?? '{"personas":[]}');
|
||||
const saveRuntime = (state: { personas: PublicPersona[] }) =>
|
||||
localStorage.setItem(runtimeKey, JSON.stringify(state));
|
||||
const event = (
|
||||
sourceKind: TimelineEvent["sourceKind"],
|
||||
title: string,
|
||||
content: string,
|
||||
state: TimelineEvent["state"] = "SUCCEEDED",
|
||||
): TimelineEvent => ({
|
||||
eventId: crypto.randomUUID(),
|
||||
sourceKind,
|
||||
title,
|
||||
content,
|
||||
occurredAt: now(),
|
||||
state,
|
||||
});
|
||||
|
||||
export async function snapshot(): Promise<SystemSnapshot> {
|
||||
if (tauri()) return invoke("system_snapshot");
|
||||
const s = load();
|
||||
return { appVersion: "1.0.0-browser", channel: s.channel, timeline: s.timeline, documents: s.documents.map(({ path, title, updatedAt, sha256 }) => ({ path, title, updatedAt, sha256 })), moduleRegistryVersion: "1", publicUpdateEndpoint: "https://guanghulab.com/hololake/releases/latest.json", publicDistributionState: "SIGNED_UPDATE_FEED_READY_NO_RELEASE", externalAiBridges: s.bridges };
|
||||
return {
|
||||
appVersion: "1.1.0-browser",
|
||||
channel: s.channel,
|
||||
timeline: s.timeline,
|
||||
documents: s.documents.map(({ path, title, updatedAt, sha256 }) => ({
|
||||
path,
|
||||
title,
|
||||
updatedAt,
|
||||
sha256,
|
||||
})),
|
||||
moduleRegistryVersion: "1",
|
||||
publicUpdateEndpoint:
|
||||
"https://guanghulab.com/hololake/releases/latest.json",
|
||||
publicDistributionState: "SIGNED_UPDATE_FEED_READY_NO_RELEASE",
|
||||
externalAiBridges: s.bridges,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createChannel(name: string): Promise<Channel> {
|
||||
if (tauri()) return invoke("create_channel", { name });
|
||||
const s = load(); const channel: Channel = { channelId: `HL-CH-${crypto.randomUUID().slice(0,8).toUpperCase()}`, name, createdAt: now(), privateGitPath: "浏览器预览不写入磁盘", publicDistributionState: "SIGNED_UPDATE_FEED_READY_NO_RELEASE" };
|
||||
s.channel = channel; s.timeline.unshift(event("SYSTEM_RECEIPT", "频道已建立", `${channel.channelId} 已登记。`)); save(s); return channel;
|
||||
const s = load();
|
||||
const channel: Channel = {
|
||||
channelId: `HL-CH-${crypto.randomUUID().slice(0, 8).toUpperCase()}`,
|
||||
name,
|
||||
createdAt: now(),
|
||||
privateGitPath: "浏览器预览不写入磁盘",
|
||||
publicDistributionState: "SIGNED_UPDATE_FEED_READY_NO_RELEASE",
|
||||
};
|
||||
s.channel = channel;
|
||||
s.timeline.unshift(
|
||||
event("SYSTEM_RECEIPT", "频道已建立", `${channel.channelId} 已登记。`),
|
||||
);
|
||||
save(s);
|
||||
return channel;
|
||||
}
|
||||
|
||||
export async function submitMessage(content: string): Promise<MutationReceipt> {
|
||||
if (tauri()) return invoke("submit_user_message", { content });
|
||||
const s = load(); const e = event("USER_MESSAGE", "用户语言", content, "RECORDED"); s.timeline.unshift(e, event("SYSTEM_RECEIPT", "系统回执", "用户语言已分流保存;等待人格体接入,不伪造人格回应。", "WAITING")); save(s);
|
||||
return { receiptId: crypto.randomUUID(), state: "WAITING_PERSONA", message: "用户语言已记录", gitCommit: null, event: e };
|
||||
const s = load();
|
||||
const e = event("USER_MESSAGE", "用户语言", content, "RECORDED");
|
||||
s.timeline.unshift(
|
||||
e,
|
||||
event(
|
||||
"SYSTEM_RECEIPT",
|
||||
"系统回执",
|
||||
"用户语言已分流保存;等待人格体接入,不伪造人格回应。",
|
||||
"WAITING",
|
||||
),
|
||||
);
|
||||
save(s);
|
||||
return {
|
||||
receiptId: crypto.randomUUID(),
|
||||
state: "WAITING_PERSONA",
|
||||
message: "用户语言已记录",
|
||||
gitCommit: null,
|
||||
event: e,
|
||||
};
|
||||
}
|
||||
|
||||
export async function readDocument(path: string): Promise<KnowledgeDocument> {
|
||||
if (tauri()) return invoke("read_knowledge_document", { path });
|
||||
const doc = load().documents.find(d => d.path === path); if (!doc) throw new Error("文档不存在"); return doc;
|
||||
const doc = load().documents.find((d) => d.path === path);
|
||||
if (!doc) throw new Error("文档不存在");
|
||||
return doc;
|
||||
}
|
||||
export async function createDocument(title: string): Promise<MutationReceipt> {
|
||||
if (tauri()) return invoke("create_knowledge_document", { title });
|
||||
const s = load(); const path = `${title.trim().replace(/[\\/:*?\"<>|]/g,"-")}.md`; const d = { path, title: title.trim(), body: `# ${title.trim()}\n\n`, updatedAt: now(), sha256: "browser-preview" }; s.documents.unshift(d); const e=event("AGENT_ACTION","Agent 执行",`已创建 ${path}`); s.timeline.unshift(e); save(s); return {receiptId:crypto.randomUUID(),state:"SUCCEEDED",message:"文档已创建",gitCommit:"browser-preview",event:e};
|
||||
const s = load();
|
||||
const path = `${title.trim().replace(/[\\/:*?\"<>|]/g, "-")}.md`;
|
||||
const d = {
|
||||
path,
|
||||
title: title.trim(),
|
||||
body: `# ${title.trim()}\n\n`,
|
||||
updatedAt: now(),
|
||||
sha256: "browser-preview",
|
||||
};
|
||||
s.documents.unshift(d);
|
||||
const e = event("AGENT_ACTION", "Agent 执行", `已创建 ${path}`);
|
||||
s.timeline.unshift(e);
|
||||
save(s);
|
||||
return {
|
||||
receiptId: crypto.randomUUID(),
|
||||
state: "SUCCEEDED",
|
||||
message: "文档已创建",
|
||||
gitCommit: "browser-preview",
|
||||
event: e,
|
||||
};
|
||||
}
|
||||
export async function saveDocument(path: string, body: string): Promise<MutationReceipt> {
|
||||
export async function saveDocument(
|
||||
path: string,
|
||||
body: string,
|
||||
): Promise<MutationReceipt> {
|
||||
if (tauri()) return invoke("save_knowledge_document", { path, body });
|
||||
const s=load(); const d=s.documents.find(x=>x.path===path); if(!d) throw new Error("文档不存在"); d.body=body; d.updatedAt=now(); const e=event("SYSTEM_RECEIPT","系统回执",`${path} 已保存到私人 Git 预览层。`); s.timeline.unshift(e); save(s); return {receiptId:crypto.randomUUID(),state:"SUCCEEDED",message:"保存完成",gitCommit:"browser-preview",event:e};
|
||||
const s = load();
|
||||
const d = s.documents.find((x) => x.path === path);
|
||||
if (!d) throw new Error("文档不存在");
|
||||
d.body = body;
|
||||
d.updatedAt = now();
|
||||
const e = event(
|
||||
"SYSTEM_RECEIPT",
|
||||
"系统回执",
|
||||
`${path} 已保存到私人 Git 预览层。`,
|
||||
);
|
||||
s.timeline.unshift(e);
|
||||
save(s);
|
||||
return {
|
||||
receiptId: crypto.randomUUID(),
|
||||
state: "SUCCEEDED",
|
||||
message: "保存完成",
|
||||
gitCommit: "browser-preview",
|
||||
event: e,
|
||||
};
|
||||
}
|
||||
export async function deleteDocument(path: string): Promise<MutationReceipt> {
|
||||
if (tauri()) return invoke("delete_knowledge_document", { path });
|
||||
const s=load(); s.documents=s.documents.filter(x=>x.path!==path); const e=event("SYSTEM_RECEIPT","系统回执",`${path} 已移入回收站。`); s.timeline.unshift(e); save(s); return {receiptId:crypto.randomUUID(),state:"SUCCEEDED",message:"已移入回收站",gitCommit:"browser-preview",event:e};
|
||||
const s = load();
|
||||
s.documents = s.documents.filter((x) => x.path !== path);
|
||||
const e = event("SYSTEM_RECEIPT", "系统回执", `${path} 已移入回收站。`);
|
||||
s.timeline.unshift(e);
|
||||
save(s);
|
||||
return {
|
||||
receiptId: crypto.randomUUID(),
|
||||
state: "SUCCEEDED",
|
||||
message: "已移入回收站",
|
||||
gitCommit: "browser-preview",
|
||||
event: e,
|
||||
};
|
||||
}
|
||||
export async function importFolder(): Promise<MutationReceipt | null> { if (!tauri()) throw new Error("文件夹导入请在已安装的 HoloLake 中使用"); return invoke("import_knowledge_folder"); }
|
||||
export async function exportDocument(path: string): Promise<string | null> { if (!tauri()) throw new Error("下载请在已安装的 HoloLake 中使用"); return invoke("export_knowledge_document", { path }); }
|
||||
export async function registerBridge(displayName: string): Promise<ExternalAiBridge> {
|
||||
export async function importFolder(): Promise<MutationReceipt | null> {
|
||||
if (!tauri()) throw new Error("文件夹导入请在已安装的 HoloLake 中使用");
|
||||
return invoke("import_knowledge_folder");
|
||||
}
|
||||
export async function exportDocument(path: string): Promise<string | null> {
|
||||
if (!tauri()) throw new Error("下载请在已安装的 HoloLake 中使用");
|
||||
return invoke("export_knowledge_document", { path });
|
||||
}
|
||||
export async function registerBridge(
|
||||
displayName: string,
|
||||
): Promise<ExternalAiBridge> {
|
||||
if (tauri()) return invoke("register_external_ai_bridge", { displayName });
|
||||
const s=load(); const b={bridgeId:`HL-AI-${crypto.randomUUID().slice(0,8).toUpperCase()}`,displayName,inboxPath:"浏览器预览",outboxPath:"浏览器预览",state:"LOCAL_EXPRESSION_ONLY"}; s.bridges.push(b); save(s); return b;
|
||||
const s = load();
|
||||
const b = {
|
||||
bridgeId: `HL-AI-${crypto.randomUUID().slice(0, 8).toUpperCase()}`,
|
||||
displayName,
|
||||
inboxPath: "浏览器预览",
|
||||
outboxPath: "浏览器预览",
|
||||
state: "LOCAL_EXPRESSION_ONLY",
|
||||
};
|
||||
s.bridges.push(b);
|
||||
save(s);
|
||||
return b;
|
||||
}
|
||||
export async function runtimeOverview(): Promise<RuntimeOverview> {
|
||||
if (tauri()) return invoke("get_runtime_overview");
|
||||
const preview = loadRuntime();
|
||||
return {
|
||||
realtime: {
|
||||
schema: "preview",
|
||||
state: "LISTENING",
|
||||
protocol: "GLP_LOCAL_REALTIME/1",
|
||||
endpoint: "tcp://127.0.0.1:39281",
|
||||
connectedClients: 0,
|
||||
loopbackOnly: true,
|
||||
transportGrantsAuthority: false,
|
||||
},
|
||||
persona: {
|
||||
schema: "preview",
|
||||
state: preview.personas.length
|
||||
? "TRIAL_PERSONA_READY"
|
||||
: "READY_NO_PERSONA",
|
||||
activePersonaId: preview.personas[0]?.personaId ?? null,
|
||||
personas: preview.personas,
|
||||
verifiedExistingPersonaCount: 0,
|
||||
},
|
||||
proposals: [],
|
||||
};
|
||||
}
|
||||
export async function realtimeInvitation(): Promise<RealtimeInvitation> {
|
||||
if (tauri()) return invoke("get_realtime_invitation");
|
||||
return {
|
||||
schema: "preview",
|
||||
protocol: "GLP_LOCAL_REALTIME/1",
|
||||
endpoint: "tcp://127.0.0.1:39281",
|
||||
token: "browser-preview",
|
||||
descriptorPath: "browser-preview",
|
||||
connectorCommand: "仅已安装 App 可生成接入命令",
|
||||
warning: "令牌不证明人格身份。",
|
||||
};
|
||||
}
|
||||
export async function registerPersona(
|
||||
displayName: string,
|
||||
): Promise<PublicPersona> {
|
||||
if (tauri()) return invoke("register_public_persona", { displayName });
|
||||
const persona = {
|
||||
personaId: `HL-PERSONA-${crypto.randomUUID().slice(0, 10).toUpperCase()}`,
|
||||
displayName,
|
||||
state: "LOCAL_TRIAL_UNVERIFIED_EXTERNAL_HOST_REQUIRED",
|
||||
createdAt: now(),
|
||||
reversibleUntilUnixMs: Date.now() + 30 * 86400000,
|
||||
};
|
||||
const preview = loadRuntime();
|
||||
preview.personas.push(persona);
|
||||
saveRuntime(preview);
|
||||
return persona;
|
||||
}
|
||||
export async function deleteTrialPersona(
|
||||
personaId: string,
|
||||
): Promise<RuntimeOverview["persona"]> {
|
||||
if (tauri())
|
||||
return invoke("delete_trial_persona", {
|
||||
personaId,
|
||||
exactConfirmation: `删除试用人格 ${personaId}`,
|
||||
});
|
||||
const preview = loadRuntime();
|
||||
preview.personas = preview.personas.filter(
|
||||
(persona) => persona.personaId !== personaId,
|
||||
);
|
||||
saveRuntime(preview);
|
||||
return {
|
||||
schema: "preview",
|
||||
state: preview.personas.length ? "TRIAL_PERSONA_READY" : "READY_NO_PERSONA",
|
||||
activePersonaId: preview.personas[0]?.personaId ?? null,
|
||||
personas: preview.personas,
|
||||
verifiedExistingPersonaCount: 0,
|
||||
};
|
||||
}
|
||||
export async function approveProposal(
|
||||
proposalId: string,
|
||||
): Promise<AgentReceipt> {
|
||||
return invoke("approve_agent_proposal", { proposalId });
|
||||
}
|
||||
export async function rejectProposal(
|
||||
proposalId: string,
|
||||
): Promise<AgentProposal> {
|
||||
return invoke("reject_agent_proposal", { proposalId });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue