feat: add HoloLake personal channel module runtime
This commit is contained in:
parent
bb96d5eb89
commit
a106b23f9b
21 changed files with 786 additions and 31 deletions
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"schema": "guanghu.desktop-delivery-receipt/v1",
|
||||||
|
"receipt_id": "GH-HOLOLAKE-DESKTOP-0.8.0-20260809-001",
|
||||||
|
"development_id": "DEV-20260809-002",
|
||||||
|
"source_base_commit": "bb96d5eb8913c93bd678d989492c5440c0558a8e",
|
||||||
|
"source_commit": "SELF",
|
||||||
|
"repository": "repo://guanghulab.com/code/bingshuo/hololake-system-architecture#main",
|
||||||
|
"target": {
|
||||||
|
"path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake Era.app",
|
||||||
|
"version": "0.8.0",
|
||||||
|
"bundle_identifier": "com.guanghu.hololake-desktop",
|
||||||
|
"architecture": "arm64",
|
||||||
|
"binary_sha256": "509fde9922b37323a4500c1509db1c3840393b8516f87e33fd96f3d3bfe7679a",
|
||||||
|
"app_asar_sha256": "feabfb3bf00dd47f337a34a928d577c723dd373f4ee8e700ccac2705b29d279a"
|
||||||
|
},
|
||||||
|
"installers": [
|
||||||
|
{
|
||||||
|
"path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake Era 0.8.0 (Apple Silicon).dmg",
|
||||||
|
"architecture": "arm64",
|
||||||
|
"bytes": 137149792,
|
||||||
|
"sha256": "44dc52b01b7dba292c16d645572fd9c9ebc1ad63c8cd8cdf3939e6212588275"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake Era 0.8.0 (Intel).dmg",
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"bytes": 142153805,
|
||||||
|
"sha256": "f616ba6afcadd7f967152fe96ffe4c17100d2d5bc6913a18dafc4b06c5c60130"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"implemented": {
|
||||||
|
"official_module_registry": "HL-MOD-KNOWLEDGE-001 now resolves to the current Guanghu-native knowledge module instead of the legacy Tolaria component path",
|
||||||
|
"personal_channel_world_object": "module installation and mount state persist under .hololake/channel-state.json in the user's own knowledge world repository",
|
||||||
|
"module_lifecycle": "install, open, collapse and remove-entry actions create Git checkpoints and reversible channel receipts",
|
||||||
|
"data_boundary": "removing the module entry never deletes documents, history, repository bindings or persona continuity data",
|
||||||
|
"single_runtime": "UI actions and Agent-confirmed language actions both execute through the same deterministic channel runtime",
|
||||||
|
"agent_permission_gate": "set_channel_module_state is a write tool and cannot execute before explicit human confirmation"
|
||||||
|
},
|
||||||
|
"verification": {
|
||||||
|
"tests": "PASS 14; FAIL 0",
|
||||||
|
"production_build": "PASS",
|
||||||
|
"packaged_server_integration": "PASS: remove -> revision 1, undo -> revision 2, README preserved, git fsck full passed",
|
||||||
|
"human_sequence_ui": "PASS: module library, collapse, open, remove, undo and Agent panel were inspected against an isolated local repository; console errors 0",
|
||||||
|
"installed_health": "PASS: /api/health returned version 0.8.0 and online from the user's existing Application Support knowledge repository",
|
||||||
|
"installed_registry": "PASS: /api/modules returned HL-MOD-KNOWLEDGE-001 version 0.8.0 state LIVE",
|
||||||
|
"installed_channel": "PASS: /api/channel returned revision 0 with the knowledge module installed and mounted; the read did not write user data",
|
||||||
|
"codesign": "PASS: installed arm64 app and both packaged app architectures satisfy deep strict code-sign verification",
|
||||||
|
"dmg": "PASS: both final disk images passed hdiutil verify"
|
||||||
|
},
|
||||||
|
"not_claimed": [
|
||||||
|
"No server deployment, production cutover, enterprise four-domain runtime, server account authentication or persona-birth claim is made by this release.",
|
||||||
|
"A configured Forgejo remote still requires a real explicit sync receipt before server hosting can be claimed.",
|
||||||
|
"No Apple notarization was performed; these are Developer ID signed development deliveries.",
|
||||||
|
"The module marketplace, signed module packages, cross-device automatic recovery and arbitrary natural-language layout patches remain later milestones."
|
||||||
|
],
|
||||||
|
"reasoning_boundary": {
|
||||||
|
"included": "user goal, verified architecture sources, observed implementation gap, public engineering decisions, changes, tests and receipts",
|
||||||
|
"excluded": "hidden model reasoning, credentials, private server addresses, personal knowledge content and local authentication state"
|
||||||
|
},
|
||||||
|
"environment_maintenance": {
|
||||||
|
"audit": "MAC_INTERNAL OK with 37082.1 MiB free; JZAO OK with 1386000 MiB free",
|
||||||
|
"codex_cache": "SKIPPED_BELOW_SIZE_THRESHOLD",
|
||||||
|
"codex_rotated_logs": "SKIPPED_BELOW_SIZE_THRESHOLD",
|
||||||
|
"protected_sessions": "PRESERVED",
|
||||||
|
"deleted_generated_builds_approx_mib": 2510.6,
|
||||||
|
"deleted_exact_old_desktop_builds": [
|
||||||
|
"/Users/bingshuolingdianyuanhe/.Trash/HoloLake-0.7.1-replaced-by-0.8.0-20260809",
|
||||||
|
"/Users/bingshuolingdianyuanhe/.Trash/HoloLake-0.8.0-stale-internal-build-20260809"
|
||||||
|
],
|
||||||
|
"deleted_git_appledouble_metadata_files": 20,
|
||||||
|
"git_fsck_after_cleanup": "PASS",
|
||||||
|
"recoverable": false
|
||||||
|
},
|
||||||
|
"result": "PASS_100_LOCAL_DEVELOPMENT_DELIVERY",
|
||||||
|
"verified_at": "2026-08-09T14:10:44+08:00"
|
||||||
|
}
|
||||||
|
|
@ -35,14 +35,15 @@
|
||||||
{
|
{
|
||||||
"id": "HL-MOD-KNOWLEDGE-001",
|
"id": "HL-MOD-KNOWLEDGE-001",
|
||||||
"name": "hololake-knowledge-lake",
|
"name": "hololake-knowledge-lake",
|
||||||
"version": "0.4.6",
|
"version": "0.8.0",
|
||||||
"state": "LEGACY_COMPONENT_ASSET",
|
"state": "LIVE_NATIVE_MODULE",
|
||||||
"path": "product-source/hololake-platform/src/",
|
"path": "product-source/guanghu-knowledge-base/",
|
||||||
"repo": "REPO-014 hololake-system-architecture",
|
"repo": "REPO-014 hololake-system-architecture",
|
||||||
"provenance": "Tolaria REPO-004 component source, assembled in REPO-014",
|
"provenance": "Guanghu-native Git world projection; Tolaria and Outline are isolated component-study sources only",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"capabilities": ["knowledge.search", "knowledge.read", "knowledge.edit", "knowledge.project"],
|
"capabilities": ["knowledge.import", "knowledge.search", "knowledge.read", "knowledge.edit", "knowledge.history", "channel.mount", "channel.unmount", "channel.remove", "channel.undo"],
|
||||||
"execution_boundary": "component-study-only; not the education subdomain runtime",
|
"execution_boundary": "personal-channel native module; module removal never deletes user knowledge or repository history",
|
||||||
|
"runtime_state_path": ".hololake/channel-state.json",
|
||||||
"superseded_for_education_by": "GH-EDU-MODULE-REGISTRY-0001"
|
"superseded_for_education_by": "GH-EDU-MODULE-REGISTRY-0001"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "guanghu-knowledge-base",
|
"name": "guanghu-knowledge-base",
|
||||||
"version": "0.1.0",
|
"version": "0.8.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "guanghu-knowledge-base",
|
"name": "guanghu-knowledge-base",
|
||||||
"version": "0.1.0",
|
"version": "0.8.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "guanghu-knowledge-base",
|
"name": "guanghu-knowledge-base",
|
||||||
"version": "0.1.0",
|
"version": "0.8.0",
|
||||||
"description": "光湖知识库模块 — Git 驱动的文档管理系统",
|
"description": "光湖知识库模块 — Git 驱动的文档管理系统",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { applyChannelPatch, defaultChannelState, KNOWLEDGE_MODULE_ID, normalizeChannelState } from './channel-runtime.js';
|
||||||
|
|
||||||
|
test('module removal unmounts the module but preserves a reversible before state', () => {
|
||||||
|
const receipt = applyChannelPatch(defaultChannelState('2026-08-09T00:00:00.000Z'), {
|
||||||
|
operation: 'set_module_state',
|
||||||
|
moduleId: KNOWLEDGE_MODULE_ID,
|
||||||
|
installed: false,
|
||||||
|
}, '2026-08-09T00:01:00.000Z');
|
||||||
|
assert.equal(receipt.after.modules[0].installed, false);
|
||||||
|
assert.equal(receipt.after.modules[0].mounted, false);
|
||||||
|
assert.equal(receipt.before.modules[0].installed, true);
|
||||||
|
assert.equal(receipt.after.revision, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unknown modules fail closed instead of becoming UI-only entries', () => {
|
||||||
|
assert.throws(() => applyChannelPatch(defaultChannelState(), {
|
||||||
|
operation: 'set_module_state',
|
||||||
|
moduleId: 'UNREGISTERED-MODULE',
|
||||||
|
installed: true,
|
||||||
|
}), /未在光湖注册表登记/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stored state is normalized against the current official registry', () => {
|
||||||
|
const state = normalizeChannelState({
|
||||||
|
schema: 'hololake.personal-channel-state/v1',
|
||||||
|
channelId: 'CHANNEL-ICE-001',
|
||||||
|
revision: 3,
|
||||||
|
updatedAt: '2026-08-09T00:00:00.000Z',
|
||||||
|
modules: [{ id: KNOWLEDGE_MODULE_ID, installed: false, mounted: true, order: 0 }],
|
||||||
|
});
|
||||||
|
assert.equal(state.modules[0].installed, false);
|
||||||
|
assert.equal(state.modules[0].mounted, false);
|
||||||
|
});
|
||||||
126
product-source/guanghu-knowledge-base/server/channel-runtime.ts
Normal file
126
product-source/guanghu-knowledge-base/server/channel-runtime.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export const KNOWLEDGE_MODULE_ID = 'HL-MOD-KNOWLEDGE-001';
|
||||||
|
|
||||||
|
export interface ModuleManifest {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
kind: 'native-application';
|
||||||
|
state: 'LIVE';
|
||||||
|
description: string;
|
||||||
|
capabilities: string[];
|
||||||
|
dataPolicy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelModuleState {
|
||||||
|
id: string;
|
||||||
|
installed: boolean;
|
||||||
|
mounted: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelState {
|
||||||
|
schema: 'hololake.personal-channel-state/v1';
|
||||||
|
channelId: string;
|
||||||
|
revision: number;
|
||||||
|
updatedAt: string;
|
||||||
|
modules: ChannelModuleState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelPatch {
|
||||||
|
operation: 'set_module_state';
|
||||||
|
moduleId: string;
|
||||||
|
installed?: boolean;
|
||||||
|
mounted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelReceipt {
|
||||||
|
schema: 'hololake.channel-receipt/v1';
|
||||||
|
id: string;
|
||||||
|
operation: ChannelPatch;
|
||||||
|
before: ChannelState;
|
||||||
|
after: ChannelState;
|
||||||
|
createdAt: string;
|
||||||
|
reversible: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MODULE_REGISTRY: ModuleManifest[] = [{
|
||||||
|
id: KNOWLEDGE_MODULE_ID,
|
||||||
|
name: '知识库',
|
||||||
|
version: '0.8.0',
|
||||||
|
kind: 'native-application',
|
||||||
|
state: 'LIVE',
|
||||||
|
description: 'Git 驱动的个人知识空间,支持导入、编辑、检索、历史与 HoloLake 语言操作。',
|
||||||
|
capabilities: [
|
||||||
|
'knowledge.import',
|
||||||
|
'knowledge.read',
|
||||||
|
'knowledge.search',
|
||||||
|
'knowledge.edit',
|
||||||
|
'knowledge.history',
|
||||||
|
],
|
||||||
|
dataPolicy: '移除模块只卸载频道入口,不删除用户文档、历史或服务器绑定。',
|
||||||
|
}];
|
||||||
|
|
||||||
|
export function defaultChannelState(now = new Date().toISOString()): ChannelState {
|
||||||
|
return {
|
||||||
|
schema: 'hololake.personal-channel-state/v1',
|
||||||
|
channelId: 'CHANNEL-PERSONAL-DEFAULT',
|
||||||
|
revision: 0,
|
||||||
|
updatedAt: now,
|
||||||
|
modules: [{ id: KNOWLEDGE_MODULE_ID, installed: true, mounted: true, order: 0 }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeChannelState(input: unknown): ChannelState {
|
||||||
|
const fallback = defaultChannelState();
|
||||||
|
if (!input || typeof input !== 'object') return fallback;
|
||||||
|
const candidate = input as Partial<ChannelState>;
|
||||||
|
const known = new Set(MODULE_REGISTRY.map(module => module.id));
|
||||||
|
const modules = Array.isArray(candidate.modules)
|
||||||
|
? candidate.modules
|
||||||
|
.filter(module => module && known.has(module.id))
|
||||||
|
.map((module, index) => ({
|
||||||
|
id: module.id,
|
||||||
|
installed: module.installed !== false,
|
||||||
|
mounted: module.installed !== false && module.mounted !== false,
|
||||||
|
order: Number.isFinite(module.order) ? Number(module.order) : index,
|
||||||
|
}))
|
||||||
|
: fallback.modules;
|
||||||
|
for (const manifest of MODULE_REGISTRY) {
|
||||||
|
if (!modules.some(module => module.id === manifest.id)) {
|
||||||
|
modules.push({ id: manifest.id, installed: false, mounted: false, order: modules.length });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema: 'hololake.personal-channel-state/v1',
|
||||||
|
channelId: typeof candidate.channelId === 'string' && candidate.channelId ? candidate.channelId : fallback.channelId,
|
||||||
|
revision: Number.isInteger(candidate.revision) && Number(candidate.revision) >= 0 ? Number(candidate.revision) : 0,
|
||||||
|
updatedAt: typeof candidate.updatedAt === 'string' ? candidate.updatedAt : fallback.updatedAt,
|
||||||
|
modules: modules.sort((a, b) => a.order - b.order),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyChannelPatch(currentInput: ChannelState, patch: ChannelPatch, now = new Date().toISOString()): ChannelReceipt {
|
||||||
|
const before = normalizeChannelState(currentInput);
|
||||||
|
if (patch.operation !== 'set_module_state') throw new Error('不支持的频道动作');
|
||||||
|
if (!MODULE_REGISTRY.some(module => module.id === patch.moduleId)) throw new Error('模块未在光湖注册表登记');
|
||||||
|
if (typeof patch.installed !== 'boolean' && typeof patch.mounted !== 'boolean') throw new Error('频道动作没有状态变化');
|
||||||
|
|
||||||
|
const modules = before.modules.map(module => {
|
||||||
|
if (module.id !== patch.moduleId) return module;
|
||||||
|
const installed = typeof patch.installed === 'boolean' ? patch.installed : module.installed;
|
||||||
|
const mounted = installed && (typeof patch.mounted === 'boolean' ? patch.mounted : module.mounted);
|
||||||
|
return { ...module, installed, mounted };
|
||||||
|
});
|
||||||
|
const after = { ...before, revision: before.revision + 1, updatedAt: now, modules };
|
||||||
|
return {
|
||||||
|
schema: 'hololake.channel-receipt/v1',
|
||||||
|
id: `HL-CHANNEL-RCPT-${crypto.randomUUID()}`,
|
||||||
|
operation: patch,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
createdAt: now,
|
||||||
|
reversible: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,17 @@ import simpleGit, { SimpleGit, LogResult } from 'simple-git';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
import { diffLines } from 'diff';
|
import { diffLines } from 'diff';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import crypto from 'crypto';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import fsSync from 'fs';
|
import fsSync from 'fs';
|
||||||
|
import {
|
||||||
|
applyChannelPatch as compileChannelPatch,
|
||||||
|
defaultChannelState,
|
||||||
|
normalizeChannelState,
|
||||||
|
type ChannelPatch,
|
||||||
|
type ChannelReceipt,
|
||||||
|
type ChannelState,
|
||||||
|
} from './channel-runtime.js';
|
||||||
|
|
||||||
// ─── 类型定义 ───
|
// ─── 类型定义 ───
|
||||||
|
|
||||||
|
|
@ -74,15 +83,66 @@ export class GitEngine {
|
||||||
private git: SimpleGit;
|
private git: SimpleGit;
|
||||||
private docsDir: string;
|
private docsDir: string;
|
||||||
private repoPath: string;
|
private repoPath: string;
|
||||||
|
private channelStatePath: string;
|
||||||
|
private channelReceiptsDir: string;
|
||||||
|
|
||||||
constructor(repoPath: string) {
|
constructor(repoPath: string) {
|
||||||
this.repoPath = repoPath;
|
this.repoPath = repoPath;
|
||||||
this.docsDir = path.join(repoPath, 'docs');
|
this.docsDir = path.join(repoPath, 'docs');
|
||||||
|
this.channelStatePath = path.join(repoPath, '.hololake', 'channel-state.json');
|
||||||
|
this.channelReceiptsDir = path.join(repoPath, '.hololake', 'receipts');
|
||||||
// simple-git 要求目录先存在,先同步创建
|
// simple-git 要求目录先存在,先同步创建
|
||||||
fsSync.mkdirSync(repoPath, { recursive: true });
|
fsSync.mkdirSync(repoPath, { recursive: true });
|
||||||
this.git = simpleGit(repoPath);
|
this.git = simpleGit(repoPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getChannelState(): Promise<ChannelState> {
|
||||||
|
try {
|
||||||
|
return normalizeChannelState(JSON.parse(await fs.readFile(this.channelStatePath, 'utf8')));
|
||||||
|
} catch {
|
||||||
|
return defaultChannelState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChannelPatch(patch: ChannelPatch): Promise<ChannelReceipt> {
|
||||||
|
const receipt = compileChannelPatch(await this.getChannelState(), patch);
|
||||||
|
await this.persistChannelReceipt(receipt, `channel: ${patch.operation} ${patch.moduleId}`);
|
||||||
|
return receipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async undoChannelPatch(receiptId: string): Promise<ChannelReceipt> {
|
||||||
|
if (!/^HL-CHANNEL-RCPT-[0-9a-f-]+$/i.test(receiptId)) throw new Error('无效的频道回执编号');
|
||||||
|
const receiptPath = path.join(this.channelReceiptsDir, `${receiptId}.json`);
|
||||||
|
const original = JSON.parse(await fs.readFile(receiptPath, 'utf8')) as ChannelReceipt;
|
||||||
|
const current = await this.getChannelState();
|
||||||
|
if (current.revision !== original.after.revision) throw new Error('频道状态已经继续变化,不能静默覆盖;请按当前状态重新操作');
|
||||||
|
const restored = {
|
||||||
|
...normalizeChannelState(original.before),
|
||||||
|
revision: current.revision + 1,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const undoReceipt: ChannelReceipt = {
|
||||||
|
schema: 'hololake.channel-receipt/v1',
|
||||||
|
id: `HL-CHANNEL-RCPT-${crypto.randomUUID()}`,
|
||||||
|
operation: original.operation,
|
||||||
|
before: current,
|
||||||
|
after: restored,
|
||||||
|
createdAt: restored.updatedAt,
|
||||||
|
reversible: true,
|
||||||
|
};
|
||||||
|
await this.persistChannelReceipt(undoReceipt, `channel: undo ${receiptId}`);
|
||||||
|
return undoReceipt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async persistChannelReceipt(receipt: ChannelReceipt, message: string): Promise<void> {
|
||||||
|
await fs.mkdir(this.channelReceiptsDir, { recursive: true });
|
||||||
|
const receiptPath = path.join(this.channelReceiptsDir, `${receipt.id}.json`);
|
||||||
|
await fs.writeFile(this.channelStatePath, `${JSON.stringify(receipt.after, null, 2)}\n`, { mode: 0o600 });
|
||||||
|
await fs.writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
|
||||||
|
await this.git.add([this.channelStatePath, receiptPath]);
|
||||||
|
await this.git.commit(message, [this.channelStatePath, receiptPath]);
|
||||||
|
}
|
||||||
|
|
||||||
/** 初始化仓库(如果不存在则创建) */
|
/** 初始化仓库(如果不存在则创建) */
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
const repoRoot = path.dirname(this.docsDir);
|
const repoRoot = path.dirname(this.docsDir);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { projectHumanKnowledgeTree } from './language-protocol-kernel.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { MODULE_REGISTRY } from './channel-runtime.js';
|
||||||
|
|
||||||
// __dirname 兼容 CJS 和 ESM
|
// __dirname 兼容 CJS 和 ESM
|
||||||
const _dirname = typeof __dirname !== 'undefined' ? __dirname : process.cwd();
|
const _dirname = typeof __dirname !== 'undefined' ? __dirname : process.cwd();
|
||||||
|
|
@ -44,7 +45,7 @@ const p = (v: unknown): string => Array.isArray(v) ? v.join('/') : String(v);
|
||||||
app.get('/api/health', (_req, res) => {
|
app.get('/api/health', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
module: 'guanghu-knowledge-base',
|
module: 'guanghu-knowledge-base',
|
||||||
version: '0.1.0',
|
version: '0.8.0',
|
||||||
engine: 'git',
|
engine: 'git',
|
||||||
repo: REPO_PATH,
|
repo: REPO_PATH,
|
||||||
status: 'online',
|
status: 'online',
|
||||||
|
|
@ -52,6 +53,38 @@ app.get('/api/health', (_req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── 初始化频道与模块运行时 ───
|
||||||
|
|
||||||
|
app.get('/api/modules', (_req, res) => {
|
||||||
|
res.json({ ok: true, registry: MODULE_REGISTRY });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/channel', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
res.json({ ok: true, channel: await engine.getChannelState() });
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).json({ ok: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/channel/patch', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const receipt = await engine.applyChannelPatch(req.body);
|
||||||
|
res.json({ ok: true, channel: receipt.after, receipt });
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(400).json({ ok: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/channel/undo/:receiptId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const receipt = await engine.undoChannelPatch(req.params.receiptId);
|
||||||
|
res.json({ ok: true, channel: receipt.after, receipt });
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(409).json({ ok: false, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ─── 文档树 ───
|
// ─── 文档树 ───
|
||||||
|
|
||||||
app.get('/api/tree', async (_req, res) => {
|
app.get('/api/tree', async (_req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ function toolCall(id: string, name: string, args: Record<string, unknown> = {}):
|
||||||
function fakeGit(overrides: Record<string, unknown> = {}) {
|
function fakeGit(overrides: Record<string, unknown> = {}) {
|
||||||
return {
|
return {
|
||||||
getRepositoryStatus: async () => ({}),
|
getRepositoryStatus: async () => ({}),
|
||||||
|
getChannelState: async () => ({ revision: 0, modules: [{ id: 'HL-MOD-KNOWLEDGE-001', installed: true, mounted: true, order: 0 }] }),
|
||||||
|
applyChannelPatch: async () => ({ id: 'HL-CHANNEL-RCPT-TEST', after: { revision: 1 } }),
|
||||||
getDoc: async (path: string) => ({
|
getDoc: async (path: string) => ({
|
||||||
meta: { id: path, title: path, updatedAt: '2026-08-09T00:00:00Z' },
|
meta: { id: path, title: path, updatedAt: '2026-08-09T00:00:00Z' },
|
||||||
body: '正文',
|
body: '正文',
|
||||||
|
|
@ -115,6 +117,29 @@ test('confirmed write receipt never leaks an orphan tool message into the next t
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('channel module changes use the same confirmation gate as document writes', { concurrency: false }, async () => {
|
||||||
|
let executed = 0;
|
||||||
|
await withMockModel([
|
||||||
|
toolCall('call-channel', 'set_channel_module_state', { moduleId: 'HL-MOD-KNOWLEDGE-001', mounted: false }),
|
||||||
|
{ content: '频道动作等待确认。' },
|
||||||
|
], async () => {
|
||||||
|
const agent = createDefaultPersona(fakeGit({
|
||||||
|
applyChannelPatch: async () => {
|
||||||
|
executed += 1;
|
||||||
|
return { id: 'HL-CHANNEL-RCPT-TEST', after: { revision: 1 } };
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const staged = await agent.chat('收起知识库');
|
||||||
|
assert.equal(staged.activities[0]?.status, 'pending');
|
||||||
|
assert.equal(executed, 0);
|
||||||
|
const pending = agent.getPendingActions()[0];
|
||||||
|
assert.equal(pending.target, 'HL-MOD-KNOWLEDGE-001');
|
||||||
|
const receipt = await agent.confirmAction(pending.id);
|
||||||
|
assert.equal(executed, 1);
|
||||||
|
assert.match(receipt.output, /HL-CHANNEL-RCPT-TEST/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('persona wake route is resolved and read dynamically without a fixed step count', { concurrency: false }, async () => {
|
test('persona wake route is resolved and read dynamically without a fixed step count', { concurrency: false }, async () => {
|
||||||
const routePath = 'personas/TCS-0002/WAKE-CURRENT.md';
|
const routePath = 'personas/TCS-0002/WAKE-CURRENT.md';
|
||||||
await withMockModel([
|
await withMockModel([
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,34 @@ export class PersonaAgent {
|
||||||
execute: async () => JSON.stringify(await this.git.getRepositoryStatus(), null, 2),
|
execute: async () => JSON.stringify(await this.git.getRepositoryStatus(), null, 2),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.tools.register({
|
||||||
|
name: 'inspect_channel',
|
||||||
|
description: '读取当前个人初始化频道的模块安装与挂载状态(只读)',
|
||||||
|
parameters: {},
|
||||||
|
effect: 'read',
|
||||||
|
execute: async () => JSON.stringify(await this.git.getChannelState(), null, 2),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.tools.register({
|
||||||
|
name: 'set_channel_module_state',
|
||||||
|
description: '安装、打开、收起或移除已在光湖注册表登记的频道模块;执行前必须由人类确认',
|
||||||
|
parameters: {
|
||||||
|
moduleId: { type: 'string', description: '光湖稳定模块编号', required: true },
|
||||||
|
installed: { type: 'boolean', description: '是否安装;不改变时可以省略', required: false },
|
||||||
|
mounted: { type: 'boolean', description: '是否在当前频道展开;不改变时可以省略', required: false },
|
||||||
|
},
|
||||||
|
effect: 'write',
|
||||||
|
execute: async (params) => {
|
||||||
|
const receipt = await this.git.applyChannelPatch({
|
||||||
|
operation: 'set_module_state',
|
||||||
|
moduleId: String(params.moduleId || ''),
|
||||||
|
installed: typeof params.installed === 'boolean' ? params.installed : undefined,
|
||||||
|
mounted: typeof params.mounted === 'boolean' ? params.mounted : undefined,
|
||||||
|
});
|
||||||
|
return `频道状态已更新;回执 ${receipt.id};当前修订 ${receipt.after.revision}`;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 读取知识库文档
|
// 读取知识库文档
|
||||||
this.tools.register({
|
this.tools.register({
|
||||||
name: 'read_document',
|
name: 'read_document',
|
||||||
|
|
@ -465,7 +493,7 @@ ${toolsList}
|
||||||
if (!tool || tool.effect === 'read') {
|
if (!tool || tool.effect === 'read') {
|
||||||
return { id: call.id, name: call.name, output: '', error: `动作不可登记: ${call.name}` };
|
return { id: call.id, name: call.name, output: '', error: `动作不可登记: ${call.name}` };
|
||||||
}
|
}
|
||||||
const target = String(call.arguments.path || call.arguments.title || '当前知识库');
|
const target = String(call.arguments.path || call.arguments.moduleId || call.arguments.title || '当前知识库');
|
||||||
const actionId = `action-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
const actionId = `action-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
const action: PendingAction = {
|
const action: PendingAction = {
|
||||||
id: actionId,
|
id: actionId,
|
||||||
|
|
@ -641,6 +669,8 @@ export function createDefaultPersona(git: GitEngine): PersonaAgent {
|
||||||
systemPromptBase: '你是 HoloLake 官方语言操作入口的当前模型计算实例。语言协议、权限边界、路径校验和可核验回执高于模型自由发挥。',
|
systemPromptBase: '你是 HoloLake 官方语言操作入口的当前模型计算实例。语言协议、权限边界、路径校验和可核验回执高于模型自由发挥。',
|
||||||
tools: [
|
tools: [
|
||||||
'inspect_repository',
|
'inspect_repository',
|
||||||
|
'inspect_channel',
|
||||||
|
'set_channel_module_state',
|
||||||
'read_document',
|
'read_document',
|
||||||
'create_document',
|
'create_document',
|
||||||
'update_document',
|
'update_document',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
|
import { useCallback, useEffect, useMemo, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
|
||||||
import { api, DocTreeNode, DocContent } from './api';
|
import { api, DocTreeNode, DocContent, type ChannelState, type ModuleManifest } from './api';
|
||||||
import { DocTree } from './components/DocTree';
|
import { DocTree } from './components/DocTree';
|
||||||
import { Editor } from './components/Editor';
|
import { Editor } from './components/Editor';
|
||||||
import { SearchBar } from './components/SearchBar';
|
import { SearchBar } from './components/SearchBar';
|
||||||
|
|
@ -9,6 +9,7 @@ import { PlatformNavigation } from './components/PlatformNavigation';
|
||||||
import { StorageLocationSheet } from './components/StorageLocationSheet';
|
import { StorageLocationSheet } from './components/StorageLocationSheet';
|
||||||
import { HumanSettings, HumanPreferences, loadHumanPreferences } from './components/HumanSettings';
|
import { HumanSettings, HumanPreferences, loadHumanPreferences } from './components/HumanSettings';
|
||||||
import { DomainSurface } from './components/DomainSurface';
|
import { DomainSurface } from './components/DomainSurface';
|
||||||
|
import { ModuleLibrarySheet } from './components/ModuleLibrarySheet';
|
||||||
import { cleanDisplayText } from './presentation';
|
import { cleanDisplayText } from './presentation';
|
||||||
|
|
||||||
type View = 'editor' | 'history';
|
type View = 'editor' | 'history';
|
||||||
|
|
@ -63,7 +64,6 @@ export default function App() {
|
||||||
const [currentPath, setCurrentPath] = useState('');
|
const [currentPath, setCurrentPath] = useState('');
|
||||||
const [view, setView] = useState<View>('editor');
|
const [view, setView] = useState<View>('editor');
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [moduleCollapsed, setModuleCollapsed] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [agentPanelOpen, setAgentPanelOpen] = useState(false);
|
const [agentPanelOpen, setAgentPanelOpen] = useState(false);
|
||||||
|
|
@ -82,6 +82,12 @@ export default function App() {
|
||||||
const [agentRevision, setAgentRevision] = useState(0);
|
const [agentRevision, setAgentRevision] = useState(0);
|
||||||
const [treeWidth, setTreeWidth] = useState(() => Number(localStorage.getItem('hololake.layout.tree-width')) || 258);
|
const [treeWidth, setTreeWidth] = useState(() => Number(localStorage.getItem('hololake.layout.tree-width')) || 258);
|
||||||
const [agentWidth, setAgentWidth] = useState(() => Number(localStorage.getItem('hololake.layout.agent-width')) || 460);
|
const [agentWidth, setAgentWidth] = useState(() => Number(localStorage.getItem('hololake.layout.agent-width')) || 460);
|
||||||
|
const [channelState, setChannelState] = useState<ChannelState | null>(null);
|
||||||
|
const [moduleRegistry, setModuleRegistry] = useState<ModuleManifest[]>([]);
|
||||||
|
const [moduleLibraryOpen, setModuleLibraryOpen] = useState(false);
|
||||||
|
const [moduleBusy, setModuleBusy] = useState(false);
|
||||||
|
const [moduleMessage, setModuleMessage] = useState('');
|
||||||
|
const [lastChannelReceipt, setLastChannelReceipt] = useState('');
|
||||||
|
|
||||||
const storageMode = repositoryStatus?.remote ? 'server' : 'local';
|
const storageMode = repositoryStatus?.remote ? 'server' : 'local';
|
||||||
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
|
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
|
||||||
|
|
@ -96,7 +102,6 @@ export default function App() {
|
||||||
setView('editor');
|
setView('editor');
|
||||||
setActiveRoute('fifth');
|
setActiveRoute('fifth');
|
||||||
setActiveModule('knowledge');
|
setActiveModule('knowledge');
|
||||||
setModuleCollapsed(false);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(`加载文档失败: ${err.message}`);
|
setError(`加载文档失败: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -158,11 +163,53 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const refreshChannel = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const [channel, registry] = await Promise.all([api.getChannel(), api.getModules()]);
|
||||||
|
setChannelState(channel);
|
||||||
|
setModuleRegistry(registry);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(`频道状态读取失败: ${err.message}`);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshTree();
|
refreshTree();
|
||||||
refreshRepositoryStatus();
|
refreshRepositoryStatus();
|
||||||
refreshServerSession();
|
refreshServerSession();
|
||||||
}, [refreshTree, refreshRepositoryStatus, refreshServerSession]);
|
refreshChannel();
|
||||||
|
}, [refreshTree, refreshRepositoryStatus, refreshServerSession, refreshChannel]);
|
||||||
|
|
||||||
|
const changeModuleState = useCallback(async (moduleId: string, installed: boolean, mounted: boolean) => {
|
||||||
|
setModuleBusy(true);
|
||||||
|
setModuleMessage('');
|
||||||
|
try {
|
||||||
|
const result = await api.patchChannel({ operation: 'set_module_state', moduleId, installed, mounted });
|
||||||
|
setChannelState(result.channel);
|
||||||
|
setLastChannelReceipt(result.receipt.id);
|
||||||
|
setModuleMessage(installed ? (mounted ? '模块已安装并打开,已保留可撤销检查点' : '模块已收起,数据保持不变') : '模块入口已移除,知识数据与历史没有删除');
|
||||||
|
if (!installed || !mounted) setAgentPanelOpen(false);
|
||||||
|
} catch (err: any) {
|
||||||
|
setModuleMessage(err.message);
|
||||||
|
} finally {
|
||||||
|
setModuleBusy(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const undoModuleChange = useCallback(async () => {
|
||||||
|
if (!lastChannelReceipt) return;
|
||||||
|
setModuleBusy(true);
|
||||||
|
try {
|
||||||
|
const result = await api.undoChannel(lastChannelReceipt);
|
||||||
|
setChannelState(result.channel);
|
||||||
|
setLastChannelReceipt('');
|
||||||
|
setModuleMessage('已恢复上一步频道状态;用户数据始终保留');
|
||||||
|
} catch (err: any) {
|
||||||
|
setModuleMessage(err.message);
|
||||||
|
} finally {
|
||||||
|
setModuleBusy(false);
|
||||||
|
}
|
||||||
|
}, [lastChannelReceipt]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resolvedLanguage = humanPreferences.language === 'system'
|
const resolvedLanguage = humanPreferences.language === 'system'
|
||||||
|
|
@ -260,7 +307,11 @@ export default function App() {
|
||||||
|
|
||||||
const activeRouteCopy = routeCopy[activeRoute];
|
const activeRouteCopy = routeCopy[activeRoute];
|
||||||
const currentTitle = cleanDisplayText(currentDoc?.meta.title || '知识库');
|
const currentTitle = cleanDisplayText(currentDoc?.meta.title || '知识库');
|
||||||
const contentVisible = activeRoute === 'fifth' && activeModule === 'knowledge' && !moduleCollapsed;
|
const knowledgeState = channelState?.modules.find(module => module.id === 'HL-MOD-KNOWLEDGE-001');
|
||||||
|
const knowledgeInstalled = knowledgeState?.installed !== false;
|
||||||
|
const knowledgeMounted = knowledgeInstalled && knowledgeState?.mounted !== false;
|
||||||
|
const moduleCollapsed = knowledgeInstalled && !knowledgeMounted;
|
||||||
|
const contentVisible = activeRoute === 'fifth' && activeModule === 'knowledge' && knowledgeMounted;
|
||||||
const breadcrumb = useMemo(
|
const breadcrumb = useMemo(
|
||||||
() => currentPath ? currentPath.split('/').map(cleanDisplayText).join(' / ') : '知识库',
|
() => currentPath ? currentPath.split('/').map(cleanDisplayText).join(' / ') : '知识库',
|
||||||
[currentPath],
|
[currentPath],
|
||||||
|
|
@ -329,27 +380,27 @@ export default function App() {
|
||||||
channelTitle={channelTitle}
|
channelTitle={channelTitle}
|
||||||
channelSubtitle={channelSubtitle}
|
channelSubtitle={channelSubtitle}
|
||||||
activeRoute={activeRoute}
|
activeRoute={activeRoute}
|
||||||
|
knowledgeInstalled={knowledgeInstalled}
|
||||||
knowledgeSelected={activeRoute === 'fifth' && activeModule === 'knowledge'}
|
knowledgeSelected={activeRoute === 'fifth' && activeModule === 'knowledge'}
|
||||||
onRouteSelect={route => {
|
onRouteSelect={route => {
|
||||||
setActiveRoute(route);
|
setActiveRoute(route);
|
||||||
setActiveModule('knowledge');
|
setActiveModule('knowledge');
|
||||||
setModuleCollapsed(false);
|
|
||||||
}}
|
}}
|
||||||
onKnowledgeSelect={() => {
|
onKnowledgeSelect={() => {
|
||||||
setActiveRoute('fifth');
|
setActiveRoute('fifth');
|
||||||
setActiveModule('knowledge');
|
setActiveModule('knowledge');
|
||||||
setModuleCollapsed(false);
|
if (!knowledgeMounted) void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true);
|
||||||
}}
|
}}
|
||||||
onEducationSelect={() => {
|
onEducationSelect={() => {
|
||||||
setActiveRoute('sub');
|
setActiveRoute('sub');
|
||||||
setActiveModule('education');
|
setActiveModule('education');
|
||||||
setModuleCollapsed(false);
|
|
||||||
}}
|
}}
|
||||||
onSettingsOpen={() => setHumanSettingsOpen(true)}
|
onSettingsOpen={() => setHumanSettingsOpen(true)}
|
||||||
onAccountOpen={() => {
|
onAccountOpen={() => {
|
||||||
setStorageSheetInitialMode('server');
|
setStorageSheetInitialMode('server');
|
||||||
setStorageSheetOpen(true);
|
setStorageSheetOpen(true);
|
||||||
}}
|
}}
|
||||||
|
onModuleLibraryOpen={() => setModuleLibraryOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<section className="platform-workspace">
|
<section className="platform-workspace">
|
||||||
|
|
@ -358,7 +409,13 @@ export default function App() {
|
||||||
<div className="route-surface">
|
<div className="route-surface">
|
||||||
<h1>知识库已收起</h1>
|
<h1>知识库已收起</h1>
|
||||||
<p>模块仍安装在永恒湖心频道中,重新打开不会改变本地或服务器数据。</p>
|
<p>模块仍安装在永恒湖心频道中,重新打开不会改变本地或服务器数据。</p>
|
||||||
<button className="primary-button" onClick={() => setModuleCollapsed(false)}>重新打开知识库</button>
|
<button className="primary-button" onClick={() => void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true)}>重新打开知识库</button>
|
||||||
|
</div>
|
||||||
|
) : activeRoute === 'fifth' && !knowledgeInstalled ? (
|
||||||
|
<div className="route-surface">
|
||||||
|
<h1>这是你的初始化频道</h1>
|
||||||
|
<p>频道目前没有安装模块。资料仍保留在个人状态域中,可以随时重新安装知识库继续使用。</p>
|
||||||
|
<button className="primary-button" onClick={() => setModuleLibraryOpen(true)}>打开模块库</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<DomainSurface
|
<DomainSurface
|
||||||
|
|
@ -392,7 +449,7 @@ export default function App() {
|
||||||
<button className={view === 'history' ? 'selected' : ''} onClick={() => setView('history')}>历史</button>
|
<button className={view === 'history' ? 'selected' : ''} onClick={() => setView('history')}>历史</button>
|
||||||
<button className="danger-action" onClick={deleteDoc} aria-label="删除当前页面">删除</button>
|
<button className="danger-action" onClick={deleteDoc} aria-label="删除当前页面">删除</button>
|
||||||
</>}
|
</>}
|
||||||
<button onClick={() => setModuleCollapsed(true)}>收起模块</button>
|
<button onClick={() => void changeModuleState('HL-MOD-KNOWLEDGE-001', true, false)}>收起模块</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -444,7 +501,7 @@ export default function App() {
|
||||||
{agentPanelOpen && (
|
{agentPanelOpen && (
|
||||||
<aside className="agent-drawer">
|
<aside className="agent-drawer">
|
||||||
<div className="agent-drawer-scope"><span>当前作用范围</span><strong>当前页面 · {currentTitle}</strong></div>
|
<div className="agent-drawer-scope"><span>当前作用范围</span><strong>当前页面 · {currentTitle}</strong></div>
|
||||||
<AgentChat apiBase={agentApiBase} runtimeRevision={agentRevision} onClose={() => setAgentPanelOpen(false)} />
|
<AgentChat apiBase={agentApiBase} runtimeRevision={agentRevision} onClose={() => setAgentPanelOpen(false)} onWorldChanged={refreshChannel} />
|
||||||
</aside>
|
</aside>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -488,6 +545,17 @@ export default function App() {
|
||||||
setStorageSheetOpen(true);
|
setStorageSheetOpen(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ModuleLibrarySheet
|
||||||
|
open={moduleLibraryOpen}
|
||||||
|
registry={moduleRegistry}
|
||||||
|
channel={channelState}
|
||||||
|
busy={moduleBusy}
|
||||||
|
message={moduleMessage}
|
||||||
|
canUndo={Boolean(lastChannelReceipt)}
|
||||||
|
onClose={() => setModuleLibraryOpen(false)}
|
||||||
|
onChange={(moduleId, installed, mounted) => void changeModuleState(moduleId, installed, mounted)}
|
||||||
|
onUndo={() => void undoModuleChange()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,38 @@ export interface SearchResult {
|
||||||
line: number;
|
line: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModuleManifest {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
kind: 'native-application';
|
||||||
|
state: 'LIVE';
|
||||||
|
description: string;
|
||||||
|
capabilities: string[];
|
||||||
|
dataPolicy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelModuleState {
|
||||||
|
id: string;
|
||||||
|
installed: boolean;
|
||||||
|
mounted: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelState {
|
||||||
|
schema: 'hololake.personal-channel-state/v1';
|
||||||
|
channelId: string;
|
||||||
|
revision: number;
|
||||||
|
updatedAt: string;
|
||||||
|
modules: ChannelModuleState[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelReceipt {
|
||||||
|
id: string;
|
||||||
|
after: ChannelState;
|
||||||
|
reversible: true;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DiffResult {
|
export interface DiffResult {
|
||||||
additions: number;
|
additions: number;
|
||||||
deletions: number;
|
deletions: number;
|
||||||
|
|
@ -66,6 +98,23 @@ export interface DiffResult {
|
||||||
// ─── API 方法 ───
|
// ─── API 方法 ───
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
|
async getModules(): Promise<ModuleManifest[]> {
|
||||||
|
const data = await request<{ registry: ModuleManifest[] }>('/modules');
|
||||||
|
return data.registry;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getChannel(): Promise<ChannelState> {
|
||||||
|
const data = await request<{ channel: ChannelState }>('/channel');
|
||||||
|
return data.channel;
|
||||||
|
},
|
||||||
|
|
||||||
|
async patchChannel(input: { operation: 'set_module_state'; moduleId: string; installed?: boolean; mounted?: boolean }): Promise<{ channel: ChannelState; receipt: ChannelReceipt }> {
|
||||||
|
return request('/channel/patch', { method: 'POST', body: JSON.stringify(input) });
|
||||||
|
},
|
||||||
|
|
||||||
|
async undoChannel(receiptId: string): Promise<{ channel: ChannelState; receipt: ChannelReceipt }> {
|
||||||
|
return request(`/channel/undo/${encodeURIComponent(receiptId)}`, { method: 'POST' });
|
||||||
|
},
|
||||||
/** 获取文档树 */
|
/** 获取文档树 */
|
||||||
getTree: () =>
|
getTree: () =>
|
||||||
request<{ ok: true; tree: DocTreeNode[] }>('/tree').then(d => d.tree),
|
request<{ ok: true; tree: DocTreeNode[] }>('/tree').then(d => d.tree),
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,10 @@ interface Props {
|
||||||
apiBase: string;
|
apiBase: string;
|
||||||
runtimeRevision?: number;
|
runtimeRevision?: number;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
onWorldChanged?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AgentChat({ apiBase, runtimeRevision = 0, onClose }: Props) {
|
export default function AgentChat({ apiBase, runtimeRevision = 0, onClose, onWorldChanged }: Props) {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
||||||
const [activeConversationId, setActiveConversationId] = useState('');
|
const [activeConversationId, setActiveConversationId] = useState('');
|
||||||
|
|
@ -172,6 +173,7 @@ export default function AgentChat({ apiBase, runtimeRevision = 0, onClose }: Pro
|
||||||
setPendingActions(data.actions || []);
|
setPendingActions(data.actions || []);
|
||||||
setMessages(data.messages || messages);
|
setMessages(data.messages || messages);
|
||||||
await refreshConversationList();
|
await refreshConversationList();
|
||||||
|
if (decision === 'confirm') onWorldChanged?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessages(previous => [...previous, { role: 'assistant', content: `动作未执行:${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString() }]);
|
setMessages(previous => [...previous, { role: 'assistant', content: `动作未执行:${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString() }]);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { ChannelState, ModuleManifest } from '../api';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
registry: ModuleManifest[];
|
||||||
|
channel: ChannelState | null;
|
||||||
|
busy: boolean;
|
||||||
|
message: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onChange: (moduleId: string, installed: boolean, mounted: boolean) => void;
|
||||||
|
onUndo: () => void;
|
||||||
|
canUndo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModuleLibrarySheet({ open, registry, channel, busy, message, onClose, onChange, onUndo, canUndo }: Props) {
|
||||||
|
if (!open) return null;
|
||||||
|
return (
|
||||||
|
<div className="sheet-backdrop" role="presentation" onMouseDown={event => { if (event.target === event.currentTarget) onClose(); }}>
|
||||||
|
<section className="module-library-sheet" role="dialog" aria-modal="true" aria-label="频道模块">
|
||||||
|
<header>
|
||||||
|
<div><small>个人初始化频道</small><h2>频道模块</h2><p>这里只显示灯塔已登记、当前版本真实可用的模块。</p></div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="关闭">×</button>
|
||||||
|
</header>
|
||||||
|
<div className="module-library-list">
|
||||||
|
{registry.map(manifest => {
|
||||||
|
const state = channel?.modules.find(module => module.id === manifest.id);
|
||||||
|
const installed = state?.installed === true;
|
||||||
|
const mounted = installed && state?.mounted === true;
|
||||||
|
return (
|
||||||
|
<article className="module-library-card" key={manifest.id}>
|
||||||
|
<div className="module-library-icon" aria-hidden="true"><span /></div>
|
||||||
|
<div className="module-library-copy">
|
||||||
|
<div className="module-library-title"><h3>{manifest.name}</h3><span>{manifest.state === 'LIVE' ? '可用' : manifest.state}</span></div>
|
||||||
|
<code>{manifest.id}</code>
|
||||||
|
<p>{manifest.description}</p>
|
||||||
|
<small>{manifest.dataPolicy}</small>
|
||||||
|
</div>
|
||||||
|
<div className="module-library-actions">
|
||||||
|
{!installed && <button className="primary-button" disabled={busy} onClick={() => onChange(manifest.id, true, true)}>安装到频道</button>}
|
||||||
|
{installed && !mounted && <button className="primary-button" disabled={busy} onClick={() => onChange(manifest.id, true, true)}>打开</button>}
|
||||||
|
{installed && mounted && <button className="secondary-button" disabled={busy} onClick={() => onChange(manifest.id, true, false)}>收起</button>}
|
||||||
|
{installed && <button className="quiet-danger" disabled={busy} onClick={() => onChange(manifest.id, false, false)}>移除入口</button>}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<span>{message || `频道状态修订 ${channel?.revision ?? 0}`}</span>
|
||||||
|
{canUndo && <button type="button" onClick={onUndo} disabled={busy}>撤销上一步</button>}
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,11 +5,13 @@ type RouteId = 'fifth' | 'main' | 'sub' | 'zero' | 'zero-sense';
|
||||||
interface Props {
|
interface Props {
|
||||||
activeRoute: RouteId;
|
activeRoute: RouteId;
|
||||||
knowledgeSelected: boolean;
|
knowledgeSelected: boolean;
|
||||||
|
knowledgeInstalled: boolean;
|
||||||
onRouteSelect: (route: RouteId) => void;
|
onRouteSelect: (route: RouteId) => void;
|
||||||
onKnowledgeSelect: () => void;
|
onKnowledgeSelect: () => void;
|
||||||
onEducationSelect: () => void;
|
onEducationSelect: () => void;
|
||||||
onSettingsOpen: () => void;
|
onSettingsOpen: () => void;
|
||||||
onAccountOpen: () => void;
|
onAccountOpen: () => void;
|
||||||
|
onModuleLibraryOpen: () => void;
|
||||||
channelTitle: string;
|
channelTitle: string;
|
||||||
channelSubtitle: string;
|
channelSubtitle: string;
|
||||||
}
|
}
|
||||||
|
|
@ -48,11 +50,13 @@ function ModuleIcon({ kind }: { kind: 'knowledge' | 'education' }) {
|
||||||
export function PlatformNavigation({
|
export function PlatformNavigation({
|
||||||
activeRoute,
|
activeRoute,
|
||||||
knowledgeSelected,
|
knowledgeSelected,
|
||||||
|
knowledgeInstalled,
|
||||||
onRouteSelect,
|
onRouteSelect,
|
||||||
onKnowledgeSelect,
|
onKnowledgeSelect,
|
||||||
onEducationSelect,
|
onEducationSelect,
|
||||||
onSettingsOpen,
|
onSettingsOpen,
|
||||||
onAccountOpen,
|
onAccountOpen,
|
||||||
|
onModuleLibraryOpen,
|
||||||
channelTitle,
|
channelTitle,
|
||||||
channelSubtitle,
|
channelSubtitle,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
|
@ -111,11 +115,12 @@ export function PlatformNavigation({
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="module-heading"><span>已安装模块</span><button type="button" aria-label="添加模块">+</button></div>
|
<div className="module-heading"><span>已安装模块</span><button type="button" aria-label="添加模块" onClick={onModuleLibraryOpen}>+</button></div>
|
||||||
<nav className="platform-module-list" aria-label="已安装模块">
|
<nav className="platform-module-list" aria-label="已安装模块">
|
||||||
<button type="button" className={knowledgeSelected ? 'selected' : ''} onClick={onKnowledgeSelect}>
|
{knowledgeInstalled && <button type="button" className={knowledgeSelected ? 'selected' : ''} onClick={onKnowledgeSelect}>
|
||||||
<ModuleIcon kind="knowledge" /><span>知识库</span>
|
<ModuleIcon kind="knowledge" /><span>知识库</span>
|
||||||
</button>
|
</button>}
|
||||||
|
{!knowledgeInstalled && <button type="button" className="module-empty-entry" onClick={onModuleLibraryOpen}><span>+</span><span>安装第一个模块</span></button>}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="platform-nav-footer">
|
<div className="platform-nav-footer">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* HoloLake 0.7.1 canonical workspace layer.
|
/* HoloLake 0.8.0 canonical workspace layer.
|
||||||
* This file is intentionally loaded after the inherited component stylesheet.
|
* This file is intentionally loaded after the inherited component stylesheet.
|
||||||
* It is the single layout and theme authority for the platform shell.
|
* It is the single layout and theme authority for the platform shell.
|
||||||
*/
|
*/
|
||||||
|
|
@ -241,6 +241,106 @@ body, #root { background: var(--lake-bg, #07111d); }
|
||||||
|
|
||||||
.storage-sheet, .human-settings { border-color: var(--lake-border); background: var(--lake-panel); color: var(--lake-text); }
|
.storage-sheet, .human-settings { border-color: var(--lake-border); background: var(--lake-panel); color: var(--lake-text); }
|
||||||
.storage-sheet-backdrop, .human-settings-backdrop { background: rgba(2, 8, 14, .56); }
|
.storage-sheet-backdrop, .human-settings-backdrop { background: rgba(2, 8, 14, .56); }
|
||||||
|
|
||||||
|
.module-empty-entry { border: 1px dashed var(--lake-border) !important; background: transparent !important; }
|
||||||
|
.module-empty-entry span:first-child { font-size: 20px; color: var(--lake-accent); }
|
||||||
|
|
||||||
|
.sheet-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1500;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 28px;
|
||||||
|
background: rgba(2, 8, 14, .62);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-library-sheet {
|
||||||
|
width: min(760px, 100%);
|
||||||
|
max-height: min(760px, calc(100vh - 56px));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--lake-border);
|
||||||
|
border-radius: 22px;
|
||||||
|
background: var(--lake-panel);
|
||||||
|
color: var(--lake-text);
|
||||||
|
box-shadow: 0 30px 90px rgba(0, 0, 0, .44);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-library-sheet > header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 28px 30px 22px;
|
||||||
|
border-bottom: 1px solid var(--lake-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-library-sheet > header small { color: var(--lake-accent); font-weight: 700; letter-spacing: .08em; }
|
||||||
|
.module-library-sheet > header h2 { margin: 6px 0 5px; font-size: 27px; letter-spacing: -.02em; }
|
||||||
|
.module-library-sheet > header p { margin: 0; color: var(--lake-muted); }
|
||||||
|
.module-library-sheet > header > button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--lake-surface-2);
|
||||||
|
color: var(--lake-muted);
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-library-list { padding: 22px; overflow: auto; }
|
||||||
|
.module-library-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 52px minmax(0, 1fr) auto;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: start;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid var(--lake-border);
|
||||||
|
border-radius: 17px;
|
||||||
|
background: var(--lake-surface-1);
|
||||||
|
}
|
||||||
|
.module-library-icon {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid var(--lake-border);
|
||||||
|
border-radius: 15px;
|
||||||
|
background: var(--lake-accent-soft);
|
||||||
|
}
|
||||||
|
.module-library-icon span { width: 20px; height: 25px; border: 2px solid var(--lake-accent); border-radius: 3px 7px 7px 3px; }
|
||||||
|
.module-library-title { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.module-library-title h3 { margin: 0; font-size: 18px; }
|
||||||
|
.module-library-title span { padding: 3px 8px; border-radius: 999px; background: color-mix(in srgb, var(--lake-success) 14%, transparent); color: var(--lake-success); font-size: 11px; font-weight: 700; }
|
||||||
|
.module-library-copy code { display: block; margin-top: 5px; color: var(--lake-muted); font-size: 11px; }
|
||||||
|
.module-library-copy p { margin: 12px 0 7px; color: var(--lake-text); line-height: 1.55; }
|
||||||
|
.module-library-copy > small { color: var(--lake-muted); line-height: 1.5; }
|
||||||
|
.module-library-actions { min-width: 118px; display: grid; gap: 8px; }
|
||||||
|
.module-library-actions button { min-height: 36px; white-space: nowrap; }
|
||||||
|
.quiet-danger { border: 0; background: transparent; color: var(--lake-danger); }
|
||||||
|
.module-library-sheet > footer {
|
||||||
|
min-height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 14px 30px;
|
||||||
|
border-top: 1px solid var(--lake-border);
|
||||||
|
color: var(--lake-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.module-library-sheet > footer button { border: 1px solid var(--lake-border); border-radius: 9px; background: var(--lake-surface-2); color: var(--lake-text); padding: 8px 13px; }
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.sheet-backdrop { padding: 12px; align-items: end; }
|
||||||
|
.module-library-sheet { max-height: calc(100vh - 24px); border-radius: 20px 20px 12px 12px; }
|
||||||
|
.module-library-card { grid-template-columns: 44px minmax(0, 1fr); }
|
||||||
|
.module-library-icon { width: 44px; height: 44px; }
|
||||||
|
.module-library-actions { grid-column: 1 / -1; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
.settings-section select, .settings-model-fields input { border-color: var(--lake-border); background: var(--lake-surface); color: var(--lake-text); }
|
.settings-section select, .settings-model-fields input { border-color: var(--lake-border); background: var(--lake-surface); color: var(--lake-text); }
|
||||||
.settings-model-card, .settings-account-card { border-color: var(--lake-border-soft); background: var(--lake-surface); }
|
.settings-model-card, .settings-account-card { border-color: var(--lake-border-soft); background: var(--lake-surface); }
|
||||||
|
|
||||||
|
|
|
||||||
29
product-source/hololake-desktop/RELEASE-0.8.0.md
Normal file
29
product-source/hololake-desktop/RELEASE-0.8.0.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# HoloLake Era Desktop 0.8.0
|
||||||
|
|
||||||
|
---
|
||||||
|
type: release-note
|
||||||
|
status: verified-local
|
||||||
|
development_id: DEV-20260809-002
|
||||||
|
source_baseline: bb96d5eb8913c93bd678d989492c5440c0558a8e
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本版目标
|
||||||
|
|
||||||
|
把 0.7.1 中仅存在于界面内存的“已安装模块”和“收起知识库”升级为初始化频道的真实世界对象,使模块生命周期由 HoloLake 协议、Git 检查点和可撤销回执共同约束。
|
||||||
|
|
||||||
|
## 已实现
|
||||||
|
|
||||||
|
- `HL-MOD-KNOWLEDGE-001` 作为当前首个真实 HoloLake 原生模块进入官方模块注册表;
|
||||||
|
- 个人频道状态写入知识世界仓库的 `.hololake/channel-state.json`,不再写死在导航栏;
|
||||||
|
- 安装、打开、收起和移除模块入口都会形成独立 Git 检查点和频道回执;
|
||||||
|
- 上一步频道动作可以撤销;状态继续变化后拒绝静默覆盖;
|
||||||
|
- 移除知识库只卸载频道入口,不删除文档、历史、服务器绑定或人格连续性资料;
|
||||||
|
- 模块库只显示已注册且当前真实可用的模块,教育行业继续作为光湖分域行业入口;
|
||||||
|
- HoloLake 语言操作入口新增频道状态读取和模块状态动作;写动作仍必须由人类确认;
|
||||||
|
- Agent 确认执行频道动作后,界面从同一频道世界对象刷新,不维护第二份假状态。
|
||||||
|
|
||||||
|
## 现实边界
|
||||||
|
|
||||||
|
- 模块状态先在当前个人知识世界的本地 Git 中形成检查点;配置远端并显式同步后才能称为服务器已托管;
|
||||||
|
- 本版没有宣称企业四域在线、生产服务器部署或人格出生;
|
||||||
|
- 模块商城、签名发行包、跨设备自动恢复和任意自然语言频道布局仍属于后续阶段。
|
||||||
|
|
@ -11,7 +11,7 @@ import { contextBridge, ipcRenderer } from 'electron';
|
||||||
contextBridge.exposeInMainWorld('hololake', {
|
contextBridge.exposeInMainWorld('hololake', {
|
||||||
// 应用信息
|
// 应用信息
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
version: '0.7.1',
|
version: '0.8.0',
|
||||||
|
|
||||||
// 数据目录
|
// 数据目录
|
||||||
getDataPath: () => ipcRenderer.invoke('get-data-path'),
|
getDataPath: () => ipcRenderer.invoke('get-data-path'),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# HoloLake Era 0.8.0 开发因果链
|
||||||
|
|
||||||
|
---
|
||||||
|
type: engineering-causal-chain
|
||||||
|
status: implemented-and-locally-verified
|
||||||
|
development_id: DEV-20260809-002
|
||||||
|
repository: REPO-014
|
||||||
|
branch: main
|
||||||
|
baseline: bb96d5eb8913c93bd678d989492c5440c0558a8e
|
||||||
|
---
|
||||||
|
|
||||||
|
## 为什么做这一轮
|
||||||
|
|
||||||
|
0.7.1 已经具备知识库、Agent、布局、主题和本地/服务器托管入口,但“已安装模块”仍是固定界面文案,“收起模块”只是本次运行的临时状态。重启后状态丢失,Agent 与界面也没有共同的频道事实源,因此尚未满足 HoloLake 初始化频道和模块热插拔蓝图。
|
||||||
|
|
||||||
|
## 本轮采用的系统解释
|
||||||
|
|
||||||
|
1. 模块以灯塔登记的稳定编号存在,界面名称不是身份。
|
||||||
|
2. 初始化频道是持续存在的世界对象,不等于仓库 URL 或 React 页面状态。
|
||||||
|
3. Git 是后台检查点与恢复引擎,不作为普通用户前台。
|
||||||
|
4. 模块可以卸载,用户知识、项目状态和历史不能随模块删除。
|
||||||
|
5. 语言操作与点击操作必须经过同一确定性运行时;写操作继续遵守显性确认。
|
||||||
|
|
||||||
|
## 工程映射
|
||||||
|
|
||||||
|
```text
|
||||||
|
engineering/MODULE-REGISTRY.json
|
||||||
|
→ HL-MOD-KNOWLEDGE-001 官方登记
|
||||||
|
→ HoloLake 模块库只投影 LIVE 模块
|
||||||
|
|
||||||
|
个人频道动作
|
||||||
|
→ /api/channel/patch
|
||||||
|
→ channel-runtime 确定性校验
|
||||||
|
→ .hololake/channel-state.json
|
||||||
|
→ .hololake/receipts/<receipt-id>.json
|
||||||
|
→ Git 检查点
|
||||||
|
→ 界面刷新或撤销
|
||||||
|
|
||||||
|
语言指令
|
||||||
|
→ set_channel_module_state
|
||||||
|
→ 人类确认门
|
||||||
|
→ 同一个 channel-runtime
|
||||||
|
→ 同一个频道状态与回执
|
||||||
|
```
|
||||||
|
|
||||||
|
## 取舍
|
||||||
|
|
||||||
|
- 没有把未开发模块放进模块库冒充可安装能力;当前只登记知识库这一真实模块。
|
||||||
|
- 没有把教育行业降级为个人已安装模块;它仍属于光湖分域。
|
||||||
|
- 没有在模块移除时删除知识目录或远端绑定。
|
||||||
|
- 没有在 UI 中暴露提交、分支和 Forgejo 工程操作;只显示人类可理解的模块状态和撤销结果。
|
||||||
|
- 没有记录或展示模型隐藏思维;本页只保存公开架构依据、实现选择与验收证据。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 安装、打开、收起、移除在重启后仍可从 Git 世界对象恢复;
|
||||||
|
- 每次变化产生独立回执;
|
||||||
|
- 撤销只能恢复仍然对应当前修订的上一步,避免覆盖后续变化;
|
||||||
|
- 移除再恢复的过程中知识文档内容不变;
|
||||||
|
- 未登记模块请求失败关闭;
|
||||||
|
- Agent 写操作仍需确认,确认后界面读取同一状态;
|
||||||
|
- 自动测试、生产构建、打包后服务 API 与 Git 完整性全部通过。
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "hololake-desktop",
|
"name": "hololake-desktop",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hololake-desktop",
|
"name": "hololake-desktop",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"diff": "^9.0.0",
|
"diff": "^9.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hololake-desktop",
|
"name": "hololake-desktop",
|
||||||
"version": "0.7.1",
|
"version": "0.8.0",
|
||||||
"description": "HoloLake Era — 光湖语言操作系统平台",
|
"description": "HoloLake Era — 光湖语言操作系统平台",
|
||||||
"main": "dist-electron/main.cjs",
|
"main": "dist-electron/main.cjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue