feat(hololake): ship language platform 0.7.0
This commit is contained in:
parent
a360982a9b
commit
5105ec5e32
47 changed files with 4566 additions and 398 deletions
1
product-source/hololake-desktop/.gitignore
vendored
1
product-source/hololake-desktop/.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
node_modules/
|
||||
.superdesign/tmp/
|
||||
dist/
|
||||
dist-electron/
|
||||
release/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# HoloLake Fifth Domain Workspace Design System
|
||||
|
||||
## Product expression
|
||||
|
||||
HoloLake is a calm operational workspace, not a dashboard wall and not a themed chat client. The user should immediately understand three levels: the signed-in human and server, the active channel, and the module currently opened inside that channel.
|
||||
|
||||
## Visual direction
|
||||
|
||||
- Keep the deep lake-night palette, but use it as a quiet frame around readable work surfaces.
|
||||
- Use one cyan accent only for the active state, primary action, and verified connection.
|
||||
- Remove decorative Chinese-character avatars. Never use the character `湖` as an icon or logo.
|
||||
- The HoloLake orbit mark is a small geometric identifier, never a large empty-state illustration.
|
||||
- Prefer open spacing and tonal separation over nested borders and boxed panels.
|
||||
- Avoid gradients, glass effects, excessive pills, oversized titles, and ornamental typography.
|
||||
|
||||
## Hierarchy
|
||||
|
||||
1. Global identity and server session: always discoverable, visually compact.
|
||||
2. Active channel: one clear label, not repeated across the top bar and sidebar.
|
||||
3. Active module: the dominant work context.
|
||||
4. Document or task content: receives most screen space.
|
||||
5. Agent: an on-demand work surface, collapsed by default; model and repository configuration live in Settings.
|
||||
|
||||
## Typography
|
||||
|
||||
- Interface: Inter, PingFang SC, system sans-serif.
|
||||
- Reading content may use Songti SC only inside the document canvas when the user selects a serif preference.
|
||||
- Default interface size 14 px; supporting text 12 px; document body 16-18 px.
|
||||
- Limit interface headings to 20-24 px. Do not use display-size Chinese characters as symbols.
|
||||
|
||||
## Layout and spacing
|
||||
|
||||
- 8 px spacing grid.
|
||||
- Global rail: 56-64 px when collapsed, up to 224 px only when explicitly expanded.
|
||||
- Context sidebar: 240-280 px and collapsible.
|
||||
- Main work surface: flexible, minimum readable width 640 px.
|
||||
- Agent drawer: 360-420 px, overlay or resizable; never compress the document below readable width.
|
||||
- Radius: 8 px controls, 12 px surfaces. Avoid nesting more than one bordered surface.
|
||||
|
||||
## States and language
|
||||
|
||||
- A saved API key is not an online Agent. Show `已保存`, `正在检测`, `可用`, and `不可用` as separate states.
|
||||
- The primary signed-out action is `登录第五域`; after login show the human account and node ID.
|
||||
- Local-only and server-hosted knowledge are explicit choices. Server hosting is unavailable until the human is authenticated.
|
||||
- Errors state the failed layer and a useful next action.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Minimum 4.5:1 text contrast.
|
||||
- Every icon-only control requires an accessible label and visible hover/focus state.
|
||||
- Do not communicate connection state by color alone.
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Shared UI Components
|
||||
|
||||
Framework: React 19 with custom TypeScript components and vanilla CSS. No third-party component library is used.
|
||||
|
||||
## Button families
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/styles/app.css`
|
||||
|
||||
```css
|
||||
.primary-button,
|
||||
.secondary-button,
|
||||
.text-button,
|
||||
.icon-button {
|
||||
min-height: 36px;
|
||||
padding: 0 15px;
|
||||
border-radius: 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.primary-button {
|
||||
border: 1px solid var(--lake-accent);
|
||||
background: var(--lake-accent);
|
||||
color: #06121c;
|
||||
font-weight: 650;
|
||||
}
|
||||
.secondary-button {
|
||||
border: 1px solid var(--lake-border);
|
||||
background: var(--lake-surface-2);
|
||||
color: var(--lake-text-soft);
|
||||
}
|
||||
.text-button,
|
||||
.icon-button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--lake-muted);
|
||||
}
|
||||
```
|
||||
|
||||
## HoloLake orbit mark
|
||||
|
||||
The current shared mark is CSS-only. It must replace every text avatar. The Chinese character `湖` is not a brand asset and must not appear as an avatar or empty-state illustration.
|
||||
|
||||
```css
|
||||
.launcher-orbit {
|
||||
position: relative;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border: 1px solid rgba(69, 183, 204, 0.58);
|
||||
border-radius: 50%;
|
||||
transform: rotate(-16deg);
|
||||
}
|
||||
.launcher-orbit::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 7px -5px;
|
||||
border: 1px solid rgba(69, 183, 204, 0.46);
|
||||
border-radius: 50%;
|
||||
}
|
||||
.launcher-orbit::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--lake-accent);
|
||||
}
|
||||
```
|
||||
|
||||
## StorageLocationSheet
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/components/StorageLocationSheet.tsx`
|
||||
|
||||
Purpose: account/server sign-in and local-versus-server knowledge storage. This is an application-level sheet, not Agent content. Key state: local/server mode, active server session, repository selection, repository creation, sign-out.
|
||||
|
||||
## HumanSettings
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/components/HumanSettings.tsx`
|
||||
|
||||
Purpose: device-local human preferences. Controls language preference, UI font, reading size and appearance. It must remain separate from server credentials and from Agent model configuration.
|
||||
|
||||
## AgentChat
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/components/AgentChat.tsx`
|
||||
|
||||
Purpose: current-channel assistance. The default render must contain only scope, status, conversation and composer. Model and Git engine controls are advanced application settings and must not be rendered in the default Agent surface.
|
||||
|
||||
## Knowledge primitives
|
||||
|
||||
- `DocTree.tsx`: folder/page hierarchy.
|
||||
- `Editor.tsx`: document preview and edit mode.
|
||||
- `SearchBar.tsx`: channel-local document search.
|
||||
- `VersionHistory.tsx`: Git-backed revision history.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# Extractable components
|
||||
|
||||
## PlatformNavigation
|
||||
- Source: `../guanghu-knowledge-base/src/components/PlatformNavigation.tsx`
|
||||
- Category: layout
|
||||
- Description: global domain rail and human account entry
|
||||
- Extractable props: `activeRoute`, `knowledgeSelected`
|
||||
- Hardcoded: HoloLake orbit mark, domain labels, settings/account icons
|
||||
|
||||
## AgentDrawer
|
||||
- Source: `../guanghu-knowledge-base/src/components/AgentChat.tsx`
|
||||
- Category: layout
|
||||
- Description: contextual current-page assistant
|
||||
- Extractable props: `operational`, `model`, `scope`, `isOpen`
|
||||
- Hardcoded: orbit mark, composer layout, status treatment
|
||||
|
||||
## StorageLocationSheet
|
||||
- Source: `../guanghu-knowledge-base/src/components/StorageLocationSheet.tsx`
|
||||
- Category: basic
|
||||
- Description: authenticated server and repository selection
|
||||
- Extractable props: `mode`, `authenticated`, `username`, `selectedRepository`
|
||||
- Hardcoded: privacy explanation and primary action hierarchy
|
||||
|
||||
## HumanSettings
|
||||
- Source: `../guanghu-knowledge-base/src/components/HumanSettings.tsx`
|
||||
- Category: basic
|
||||
- Description: device-local preferences and advanced application settings
|
||||
- Extractable props: `language`, `font`, `readingSize`, `appearance`
|
||||
- Hardcoded: current data boundary explanation
|
||||
|
||||
## OrbitMark
|
||||
- Source: `../guanghu-knowledge-base/src/styles/app.css`
|
||||
- Category: basic
|
||||
- Description: neutral HoloLake brand mark; replaces every `湖` text avatar
|
||||
- Extractable props: `size`, `status`
|
||||
- Hardcoded: cyan orbit geometry
|
||||
52
product-source/hololake-desktop/.superdesign/init/layouts.md
Normal file
52
product-source/hololake-desktop/.superdesign/init/layouts.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Layouts
|
||||
|
||||
## Application shell
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/App.tsx`
|
||||
|
||||
Current render branch:
|
||||
|
||||
```tsx
|
||||
<div className="hololake-shell">
|
||||
<header className="platform-topbar">channel title + document search</header>
|
||||
<div className="platform-body">
|
||||
<PlatformNavigation />
|
||||
<section className="platform-workspace">
|
||||
<div className="knowledge-tabbar" />
|
||||
<div className="knowledge-module-bar" />
|
||||
<div className="knowledge-layout">
|
||||
<aside className="knowledge-sidebar" />
|
||||
<main className="knowledge-content" />
|
||||
</div>
|
||||
{agentPanelOpen && <aside className="agent-drawer"><AgentChat /></aside>}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
The current shell has too many simultaneous horizontal and vertical control bands. Redesign goal: one platform rail, one channel header, one active module canvas. Tabs, storage state, module actions and document navigation must not all compete in separate bars.
|
||||
|
||||
## Platform navigation
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/components/PlatformNavigation.tsx`
|
||||
|
||||
Current content:
|
||||
|
||||
```tsx
|
||||
<aside className="platform-navigation">
|
||||
<div className="platform-brand">HoloLake</div>
|
||||
<button className="channel-selector">Eternal Lake / heartbeat channel</button>
|
||||
<nav className="platform-route-list">five domain entries</nav>
|
||||
<div className="module-heading">installed modules</div>
|
||||
<nav className="platform-module-list">knowledge + education</nav>
|
||||
<div className="platform-nav-footer">settings + account</div>
|
||||
</aside>
|
||||
```
|
||||
|
||||
The rail mixes global domains, the active channel and installed modules. Redesign goal: preserve a compact global rail; move active-channel identity into the workspace header; render modules inside the selected channel.
|
||||
|
||||
## Agent drawer
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/components/AgentChat.tsx`
|
||||
|
||||
Current drawer is 380px and overlays the reading canvas. It shows current scope, model badge, settings, runtime tool count, full model form, full Git form, welcome mark, suggestions and composer. Redesign goal: scope + conversation + composer only; advanced configuration moves to Human Settings.
|
||||
42
product-source/hololake-desktop/.superdesign/init/pages.md
Normal file
42
product-source/hololake-desktop/.superdesign/init/pages.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Page dependency trees
|
||||
|
||||
## Fifth Domain / Knowledge channel
|
||||
|
||||
Entry: `../guanghu-knowledge-base/src/App.tsx`
|
||||
|
||||
Dependencies:
|
||||
|
||||
- `src/App.tsx`
|
||||
- `src/api.ts`
|
||||
- `src/components/PlatformNavigation.tsx`
|
||||
- `src/components/SearchBar.tsx`
|
||||
- `src/components/DocTree.tsx`
|
||||
- `src/components/Editor.tsx`
|
||||
- `src/components/VersionHistory.tsx`
|
||||
- `src/components/AgentChat.tsx`
|
||||
- `src/components/StorageLocationSheet.tsx`
|
||||
- `src/components/HumanSettings.tsx`
|
||||
- `src/styles/app.css`
|
||||
|
||||
Desktop render branch is the only branch. The Electron window minimum is 800×600; the primary design viewport is 1280×860.
|
||||
|
||||
## Server/account entry
|
||||
|
||||
Entry: `src/components/StorageLocationSheet.tsx`
|
||||
|
||||
Dependencies:
|
||||
|
||||
- Electron preload `../hololake-desktop/electron/preload.ts`
|
||||
- Electron IPC implementation `../hololake-desktop/electron/main.ts`
|
||||
- Knowledge remote API `server/index.ts`
|
||||
- Git engine `server/git-engine.ts`
|
||||
- Shared controls from `src/styles/app.css`
|
||||
|
||||
## Human settings
|
||||
|
||||
Entry: `src/components/HumanSettings.tsx`
|
||||
|
||||
Dependencies:
|
||||
|
||||
- localStorage preference contract in the same file
|
||||
- shared overlay/control styling in `src/styles/app.css`
|
||||
21
product-source/hololake-desktop/.superdesign/init/routes.md
Normal file
21
product-source/hololake-desktop/.superdesign/init/routes.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Routes
|
||||
|
||||
This Electron/Vite app uses state-based routes inside `src/App.tsx`, not React Router.
|
||||
|
||||
| State | Surface | Source |
|
||||
| --- | --- | --- |
|
||||
| `fifth` | BingShuo Fifth Domain / Eternal Lake channel | `src/App.tsx` |
|
||||
| `main` | Guanghu Main Domain entry | `src/App.tsx` |
|
||||
| `sub` | Industry subdomain entry | `src/App.tsx` |
|
||||
| `zero` | Experimental domain entry | `src/App.tsx` |
|
||||
| `zero-sense` | Governance/audit domain entry | `src/App.tsx` |
|
||||
| `fifth + knowledge` | Knowledge module | `App.tsx`, `DocTree.tsx`, `Editor.tsx` |
|
||||
| `sub + education` | Education industry staged entry | `src/App.tsx` |
|
||||
|
||||
Application-level overlays:
|
||||
|
||||
- server/account sign-in and repository selection: `StorageLocationSheet.tsx`;
|
||||
- human preferences and advanced settings: `HumanSettings.tsx`;
|
||||
- contextual assistance: `AgentChat.tsx`.
|
||||
|
||||
Target redesign route: desktop `fifth + knowledge`, with Agent open and authenticated-user entry visible.
|
||||
46
product-source/hololake-desktop/.superdesign/init/theme.md
Normal file
46
product-source/hololake-desktop/.superdesign/init/theme.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Theme
|
||||
|
||||
## Compact token summary
|
||||
|
||||
- Background: `#07111d`
|
||||
- Primary surface: `#0d1825`
|
||||
- Secondary surface: `#111e2c`
|
||||
- Reading surface: `#0b1622`
|
||||
- Border: `#263648`
|
||||
- Primary text: `#e8edf3`
|
||||
- Secondary text: `#c8d1dc`
|
||||
- Muted text: `#78879a`
|
||||
- Accent: `#45b7cc`
|
||||
- Success: `#53c69b`
|
||||
- Warning: `#d6aa4f`
|
||||
- Danger: `#e36f72`
|
||||
- UI font: Inter / SF Pro / PingFang SC
|
||||
- Reading heading font: Songti SC / Georgia, used only for document content
|
||||
- Radius: 8–12px controls, 16–18px overlays
|
||||
- Shadows: overlays only; normal workspace relies on borders and surface contrast
|
||||
- Density: compact platform chrome, spacious reading content
|
||||
|
||||
## Raw source tokens
|
||||
|
||||
Source: `../guanghu-knowledge-base/src/styles/app.css`
|
||||
|
||||
```css
|
||||
:root {
|
||||
--lake-bg: #07111d;
|
||||
--lake-surface: #0d1825;
|
||||
--lake-surface-2: #111e2c;
|
||||
--lake-reading: #0b1622;
|
||||
--lake-border: #263648;
|
||||
--lake-border-soft: rgba(119, 148, 174, 0.17);
|
||||
--lake-text: #e8edf3;
|
||||
--lake-text-soft: #c8d1dc;
|
||||
--lake-muted: #78879a;
|
||||
--lake-accent: #45b7cc;
|
||||
--lake-accent-soft: rgba(69, 183, 204, 0.13);
|
||||
--lake-success: #53c69b;
|
||||
--lake-warning: #d6aa4f;
|
||||
--lake-danger: #e36f72;
|
||||
}
|
||||
```
|
||||
|
||||
Design correction: no text glyph is allowed as a logo/avatar. Avoid ornamental serif typography outside the document canvas. Use whitespace, alignment and typography instead of nested bordered boxes.
|
||||
36
product-source/hololake-desktop/RELEASE-0.7.0.md
Normal file
36
product-source/hololake-desktop/RELEASE-0.7.0.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# HoloLake Era Desktop 0.7.0
|
||||
|
||||
## 产品边界
|
||||
|
||||
HoloLake 是光湖语言操作系统平台,不是传统知识库外壳。语言协议核由运行时自动装载,
|
||||
模型是可替换计算服务,知识库是可挂载模块,Forgejo 是代码与版本引擎,个人服务器是
|
||||
当前账号、频道和私人数据的现实锚点。
|
||||
|
||||
## 本版实现
|
||||
|
||||
- 个人第五域由各设备的本机私有配置注入;企业公共灯塔入口与个人节点保持独立;
|
||||
- 企业灯塔通过其真实只读服务返回四域在线、责任绑定与写入拦截状态;
|
||||
- 光湖主域、分域、零域、零感域保持四个独立仓库入口,不伪装成同一仓库的子目录;
|
||||
- 教育行业投影小新初始化频道、智能文档、知识库及表格图表模块状态;
|
||||
- 知识库支持本地文件夹导入、页面树、编辑、历史、删除与本机/服务器托管选择;
|
||||
- HoloLake 语言操作入口通过受控工具读取知识;创建、更新、删除先形成待确认动作;
|
||||
- 语言协议、人格配置和内部恢复材料不进入人类知识树,但仍由模型运行投影和受控搜索使用;
|
||||
- 人类端设置包含语言、字体、正文大小、外观、模型验证、服务器与仓库入口;
|
||||
- 模型密钥与服务器令牌只保存在 macOS 加密存储,切换服务器账号会卸载旧知识库远端。
|
||||
|
||||
## 真实状态边界
|
||||
|
||||
- 企业四域入口当前由服务器报告为 `HOSTED_READ_ONLY`;
|
||||
- 责任人格体未正式绑定时,服务器报告 `BLOCKED_UNTIL_PERSONA_STEWARD_BOUND`;
|
||||
- 本版包含四域初始化源,但不得绕过企业灯塔确认门直接宣称域内写入或主控权已生效;
|
||||
- 安装包不包含个人知识、服务器令牌、模型密钥、SSH 密钥、浏览器资料或私人第五域内容。
|
||||
|
||||
## 验收门
|
||||
|
||||
- 单元测试与生产构建通过;
|
||||
- Agent 模型配置通过真实请求;
|
||||
- Agent 真实对话通过;
|
||||
- 写操作停在待确认门,取消后文件不存在;
|
||||
- JD 第五域与企业灯塔经本机密钥建立真实隧道;
|
||||
- Apple Silicon 与 Intel 包分别签名、校验、启动检查;
|
||||
- 桌面唯一新版完成安装后,旧桌面应用与旧安装包才进入可恢复废纸篓。
|
||||
|
|
@ -29,11 +29,136 @@ const CLIENT_PORT = 5180;
|
|||
const DATA_DIR = path.join(app.getPath('userData'), 'data');
|
||||
const KB_REPO_PATH = path.join(DATA_DIR, 'knowledge-base');
|
||||
const MODEL_CONFIG_PATH = path.join(app.getPath('userData'), 'model-config.json');
|
||||
const SERVER_AUTH_PATH = path.join(app.getPath('userData'), 'server-auth.json');
|
||||
const SERVER_PROFILES_PATH = path.join(app.getPath('userData'), 'server-profiles.json');
|
||||
const GIT_ASKPASS_PATH = path.join(app.getPath('userData'), 'hololake-git-askpass.sh');
|
||||
interface ServerProfileDefinition {
|
||||
id: string;
|
||||
physicalNodeId: string;
|
||||
name: string;
|
||||
purpose: 'personal-fifth-domain' | 'enterprise-lighthouse';
|
||||
sshAlias: string;
|
||||
tunnelPort: number;
|
||||
remoteForgejoPort: number;
|
||||
lighthouseTunnelPort?: number;
|
||||
remoteLighthousePort?: number;
|
||||
channelTitle?: string;
|
||||
channelSubtitle?: string;
|
||||
}
|
||||
|
||||
const PUBLIC_SERVER_PROFILES: Record<string, ServerProfileDefinition> = {
|
||||
'AW-GZ-001': {
|
||||
id: 'AW-GZ-001',
|
||||
physicalNodeId: 'GH-CVM-MAIN-PROD-01',
|
||||
name: '企业灯塔服务器',
|
||||
purpose: 'enterprise-lighthouse',
|
||||
sshAlias: 'gh-enterprise-main',
|
||||
tunnelPort: 13341,
|
||||
remoteForgejoPort: 3341,
|
||||
lighthouseTunnelPort: 18031,
|
||||
remoteLighthousePort: 8031,
|
||||
},
|
||||
};
|
||||
|
||||
function readServerProfiles(): Record<string, ServerProfileDefinition> {
|
||||
const profiles = { ...PUBLIC_SERVER_PROFILES };
|
||||
try {
|
||||
const localProfiles = JSON.parse(fs.readFileSync(SERVER_PROFILES_PATH, 'utf8')) as ServerProfileDefinition[];
|
||||
if (!Array.isArray(localProfiles)) return profiles;
|
||||
for (const profile of localProfiles) {
|
||||
if (
|
||||
!profile || typeof profile.id !== 'string' || typeof profile.physicalNodeId !== 'string'
|
||||
|| typeof profile.name !== 'string' || profile.purpose !== 'personal-fifth-domain'
|
||||
|| typeof profile.sshAlias !== 'string' || !Number.isInteger(profile.tunnelPort)
|
||||
|| !Number.isInteger(profile.remoteForgejoPort)
|
||||
) continue;
|
||||
profiles[profile.id] = profile;
|
||||
}
|
||||
} catch {
|
||||
// 新安装默认只知道企业公共灯塔;个人节点由本机私有配置登记,不进入安装包。
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
function getServerProfile(nodeId: string): ServerProfileDefinition {
|
||||
const profile = readServerProfiles()[nodeId];
|
||||
if (!profile) throw new Error('未登记的服务器节点');
|
||||
return profile;
|
||||
}
|
||||
|
||||
function defaultPersonalServerId(): string | undefined {
|
||||
return Object.values(readServerProfiles()).find(profile => profile.purpose === 'personal-fifth-domain')?.id;
|
||||
}
|
||||
|
||||
function forgejoBaseUrl(profile: ServerProfileDefinition): string {
|
||||
return `http://127.0.0.1:${profile.tunnelPort}`;
|
||||
}
|
||||
|
||||
function lighthouseBaseUrl(profile: ServerProfileDefinition): string {
|
||||
if (!profile.lighthouseTunnelPort) throw new Error('该节点未登记灯塔服务');
|
||||
return `http://127.0.0.1:${profile.lighthouseTunnelPort}`;
|
||||
}
|
||||
|
||||
interface ModelConfigFile {
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
encryptedKey?: string;
|
||||
verifiedAt?: string;
|
||||
}
|
||||
|
||||
interface ServerAuthFile {
|
||||
nodeId: string;
|
||||
username: string;
|
||||
encryptedToken: string;
|
||||
}
|
||||
|
||||
function writeGitAskPass(): void {
|
||||
const content = `#!/bin/sh
|
||||
case "$1" in
|
||||
*Username*) printf '%s\\n' "$HOLOLAKE_FORGEJO_USERNAME" ;;
|
||||
*) printf '%s\\n' "$HOLOLAKE_FORGEJO_TOKEN" ;;
|
||||
esac
|
||||
`;
|
||||
fs.mkdirSync(path.dirname(GIT_ASKPASS_PATH), { recursive: true });
|
||||
fs.writeFileSync(GIT_ASKPASS_PATH, content, { mode: 0o700 });
|
||||
}
|
||||
|
||||
function clearGitCredentialEnvironment(): void {
|
||||
delete process.env.HOLOLAKE_FORGEJO_USERNAME;
|
||||
delete process.env.HOLOLAKE_FORGEJO_TOKEN;
|
||||
delete process.env.GIT_ASKPASS;
|
||||
delete process.env.GIT_TERMINAL_PROMPT;
|
||||
}
|
||||
|
||||
function applyStoredServerAuth(): { nodeId: string; username: string; token: string } | null {
|
||||
try {
|
||||
if (!safeStorage.isEncryptionAvailable()) return null;
|
||||
const stored = JSON.parse(fs.readFileSync(SERVER_AUTH_PATH, 'utf8')) as ServerAuthFile;
|
||||
getServerProfile(stored.nodeId);
|
||||
if (!stored.encryptedToken) return null;
|
||||
const token = safeStorage.decryptString(Buffer.from(stored.encryptedToken, 'base64'));
|
||||
writeGitAskPass();
|
||||
process.env.HOLOLAKE_FORGEJO_USERNAME = stored.username;
|
||||
process.env.HOLOLAKE_FORGEJO_TOKEN = token;
|
||||
process.env.GIT_ASKPASS = GIT_ASKPASS_PATH;
|
||||
process.env.GIT_TERMINAL_PROMPT = '0';
|
||||
return { nodeId: stored.nodeId, username: stored.username, token };
|
||||
} catch {
|
||||
clearGitCredentialEnvironment();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveServerAuth(nodeId: string, username: string, token: string): void {
|
||||
if (!safeStorage.isEncryptionAvailable()) throw new Error('macOS 加密存储当前不可用');
|
||||
fs.mkdirSync(path.dirname(SERVER_AUTH_PATH), { recursive: true });
|
||||
const encryptedToken = safeStorage.encryptString(token).toString('base64');
|
||||
fs.writeFileSync(
|
||||
SERVER_AUTH_PATH,
|
||||
JSON.stringify({ nodeId, username, encryptedToken } satisfies ServerAuthFile),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
applyStoredServerAuth();
|
||||
}
|
||||
|
||||
function readModelConfig(): ModelConfigFile {
|
||||
|
|
@ -50,13 +175,140 @@ function applyModelConfig(): ModelConfigFile {
|
|||
process.env.HOLOLAKE_LLM_MODEL = config.model;
|
||||
if (config.encryptedKey && safeStorage.isEncryptionAvailable()) {
|
||||
process.env.HOLOLAKE_LLM_KEY = safeStorage.decryptString(Buffer.from(config.encryptedKey, 'base64'));
|
||||
} else {
|
||||
delete process.env.HOLOLAKE_LLM_KEY;
|
||||
}
|
||||
process.env.HOLOLAKE_LLM_VERIFIED = config.verifiedAt ? '1' : '0';
|
||||
return config;
|
||||
}
|
||||
|
||||
async function verifyModelConfig(): Promise<{ verifiedAt: string }> {
|
||||
const config = readModelConfig();
|
||||
if (!config.encryptedKey || !safeStorage.isEncryptionAvailable()) throw new Error('请先保存模型密钥');
|
||||
const apiKey = safeStorage.decryptString(Buffer.from(config.encryptedKey, 'base64'));
|
||||
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
messages: [
|
||||
{ role: 'system', content: 'This is a HoloLake model connectivity check. Reply with OK.' },
|
||||
{ role: 'user', content: 'ping' },
|
||||
],
|
||||
max_tokens: 8,
|
||||
temperature: 0,
|
||||
}),
|
||||
signal: AbortSignal.timeout(20000),
|
||||
});
|
||||
const data = await response.json().catch(() => null) as any;
|
||||
if (!response.ok) throw new Error(data?.error?.message || `模型服务返回 HTTP ${response.status}`);
|
||||
if (!data?.choices?.[0]?.message) throw new Error('模型服务没有返回兼容的响应');
|
||||
const verifiedAt = new Date().toISOString();
|
||||
fs.writeFileSync(MODEL_CONFIG_PATH, JSON.stringify({ ...config, verifiedAt }), { mode: 0o600 });
|
||||
applyModelConfig();
|
||||
return { verifiedAt };
|
||||
}
|
||||
|
||||
// ─── 后端服务器 ───
|
||||
|
||||
let serverProcess: ChildProcess | null = null;
|
||||
const serverTunnels = new Map<string, ChildProcess>();
|
||||
|
||||
async function isServerTunnelReady(profile: ServerProfileDefinition): Promise<boolean> {
|
||||
try {
|
||||
const checks: Promise<Response>[] = [
|
||||
fetch(`${forgejoBaseUrl(profile)}/user/login`, { signal: AbortSignal.timeout(1200) }),
|
||||
];
|
||||
if (profile.lighthouseTunnelPort) {
|
||||
checks.push(fetch(`${lighthouseBaseUrl(profile)}/health`, { signal: AbortSignal.timeout(1200) }));
|
||||
}
|
||||
const responses = await Promise.all(checks);
|
||||
return responses.every(response => response.status === 200);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureServerTunnel(profile: ServerProfileDefinition): Promise<void> {
|
||||
if (await isServerTunnelReady(profile)) return;
|
||||
const currentTunnel = serverTunnels.get(profile.id);
|
||||
if (currentTunnel && currentTunnel.exitCode === null) {
|
||||
throw new Error(`${profile.name}连接正在建立,请稍后重试`);
|
||||
}
|
||||
|
||||
const forwards = [
|
||||
'-L', `127.0.0.1:${profile.tunnelPort}:127.0.0.1:${profile.remoteForgejoPort}`,
|
||||
];
|
||||
if (profile.lighthouseTunnelPort && profile.remoteLighthousePort) {
|
||||
forwards.push('-L', `127.0.0.1:${profile.lighthouseTunnelPort}:127.0.0.1:${profile.remoteLighthousePort}`);
|
||||
}
|
||||
const tunnel = spawn('/usr/bin/ssh', [
|
||||
'-N',
|
||||
...forwards,
|
||||
'-o', 'BatchMode=yes',
|
||||
'-o', 'ExitOnForwardFailure=yes',
|
||||
'-o', 'ServerAliveInterval=30',
|
||||
'-o', 'ServerAliveCountMax=3',
|
||||
profile.sshAlias,
|
||||
], {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
serverTunnels.set(profile.id, tunnel);
|
||||
tunnel.once('exit', () => {
|
||||
serverTunnels.delete(profile.id);
|
||||
});
|
||||
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
if (await isServerTunnelReady(profile)) return;
|
||||
if (tunnel.exitCode !== null) break;
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
tunnel.kill('SIGTERM');
|
||||
serverTunnels.delete(profile.id);
|
||||
throw new Error(`无法通过本机登记密钥连接${profile.name}`);
|
||||
}
|
||||
|
||||
function stopServerTunnels(): void {
|
||||
for (const tunnel of serverTunnels.values()) tunnel.kill('SIGTERM');
|
||||
serverTunnels.clear();
|
||||
}
|
||||
|
||||
async function detachKnowledgeRemote(): Promise<void> {
|
||||
try {
|
||||
await fetch(`http://127.0.0.1:${SERVER_PORT}/api/forgejo/remote`, { method: 'DELETE' });
|
||||
} catch {
|
||||
// 本地知识库服务不可用时不阻断账号退出;下次登录仍会重新校验仓库权限。
|
||||
}
|
||||
}
|
||||
|
||||
async function forgejoRequest(
|
||||
nodeId: string,
|
||||
route: string,
|
||||
init: RequestInit = {},
|
||||
auth?: { username?: string; password?: string; token?: string },
|
||||
): Promise<{ response: Response; data: any }> {
|
||||
const profile = getServerProfile(nodeId);
|
||||
await ensureServerTunnel(profile);
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set('Accept', 'application/json');
|
||||
if (init.body) headers.set('Content-Type', 'application/json');
|
||||
if (auth?.token) headers.set('Authorization', `token ${auth.token}`);
|
||||
if (auth?.username && auth.password) {
|
||||
headers.set('Authorization', `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`);
|
||||
}
|
||||
const response = await fetch(`${forgejoBaseUrl(profile)}${route}`, {
|
||||
...init,
|
||||
headers,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
const text = await response.text();
|
||||
let data: any = null;
|
||||
if (text) {
|
||||
try { data = JSON.parse(text); } catch { data = { message: text }; }
|
||||
}
|
||||
return { response, data };
|
||||
}
|
||||
|
||||
function startServer(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -119,6 +371,21 @@ function stopServer(): void {
|
|||
}
|
||||
}
|
||||
|
||||
async function waitForServerReady(): Promise<void> {
|
||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${SERVER_PORT}/api/health`, {
|
||||
signal: AbortSignal.timeout(800),
|
||||
});
|
||||
if (response.ok) return;
|
||||
} catch {
|
||||
// 服务仍在启动,继续读取真实健康端点,不提前打开一个离线界面。
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error('HoloLake 本地服务未在规定时间内就绪');
|
||||
}
|
||||
|
||||
// ─── 窗口 ───
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
|
@ -131,7 +398,7 @@ function createWindow(): void {
|
|||
minHeight: 600,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
backgroundColor: '#f7f8fb',
|
||||
backgroundColor: '#07111d',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.cjs'),
|
||||
contextIsolation: true,
|
||||
|
|
@ -159,11 +426,14 @@ function createWindow(): void {
|
|||
|
||||
ipcMain.handle('get-data-path', () => DATA_DIR);
|
||||
ipcMain.handle('knowledge:import-folder', async () => {
|
||||
const selection = await dialog.showOpenDialog(mainWindow ?? undefined, {
|
||||
const options: Electron.OpenDialogOptions = {
|
||||
title: '导入本地知识文件夹',
|
||||
buttonLabel: '导入到 HoloLake',
|
||||
properties: ['openDirectory'],
|
||||
});
|
||||
};
|
||||
const selection = mainWindow
|
||||
? await dialog.showOpenDialog(mainWindow, options)
|
||||
: await dialog.showOpenDialog(options);
|
||||
if (selection.canceled || !selection.filePaths[0]) {
|
||||
return { cancelled: true, imported: 0, assets: 0, skipped: 0, failed: [] };
|
||||
}
|
||||
|
|
@ -175,6 +445,8 @@ ipcMain.handle('agent:get-config', () => {
|
|||
baseUrl: config.baseUrl,
|
||||
model: config.model,
|
||||
configured: Boolean(config.encryptedKey && safeStorage.isEncryptionAvailable()),
|
||||
operational: Boolean(config.encryptedKey && config.verifiedAt && safeStorage.isEncryptionAvailable()),
|
||||
verifiedAt: config.verifiedAt,
|
||||
};
|
||||
});
|
||||
ipcMain.handle('agent:save-config', (_event, input: { baseUrl: string; model: string; apiKey?: string }) => {
|
||||
|
|
@ -188,10 +460,207 @@ ipcMain.handle('agent:save-config', (_event, input: { baseUrl: string; model: st
|
|||
? safeStorage.encryptString(String(input.apiKey)).toString('base64')
|
||||
: previous.encryptedKey;
|
||||
if (!encryptedKey) throw new Error('请填写模型密钥');
|
||||
const verificationStillValid = !input.apiKey && previous.baseUrl === baseUrl && previous.model === model;
|
||||
fs.mkdirSync(path.dirname(MODEL_CONFIG_PATH), { recursive: true });
|
||||
fs.writeFileSync(MODEL_CONFIG_PATH, JSON.stringify({ baseUrl, model, encryptedKey }), { mode: 0o600 });
|
||||
fs.writeFileSync(MODEL_CONFIG_PATH, JSON.stringify({
|
||||
baseUrl,
|
||||
model,
|
||||
encryptedKey,
|
||||
verifiedAt: verificationStillValid ? previous.verifiedAt : undefined,
|
||||
}), { mode: 0o600 });
|
||||
applyModelConfig();
|
||||
return { baseUrl, model, configured: true };
|
||||
return { baseUrl, model, configured: true, operational: verificationStillValid && Boolean(previous.verifiedAt) };
|
||||
});
|
||||
ipcMain.handle('agent:test-config', () => verifyModelConfig());
|
||||
ipcMain.handle('server:list', async () => {
|
||||
return Promise.all(Object.values(readServerProfiles()).map(async profile => ({
|
||||
id: profile.id,
|
||||
physicalNodeId: profile.physicalNodeId,
|
||||
name: profile.name,
|
||||
purpose: profile.purpose,
|
||||
channelTitle: profile.channelTitle,
|
||||
channelSubtitle: profile.channelSubtitle,
|
||||
connected: await isServerTunnelReady(profile),
|
||||
verified: true,
|
||||
})));
|
||||
});
|
||||
ipcMain.handle('server:connect', async (_event, nodeId: string) => {
|
||||
const profile = getServerProfile(nodeId);
|
||||
await ensureServerTunnel(profile);
|
||||
return {
|
||||
id: profile.id,
|
||||
physicalNodeId: profile.physicalNodeId,
|
||||
name: profile.name,
|
||||
purpose: profile.purpose,
|
||||
channelTitle: profile.channelTitle,
|
||||
channelSubtitle: profile.channelSubtitle,
|
||||
connected: true,
|
||||
verified: true,
|
||||
};
|
||||
});
|
||||
ipcMain.handle('server:domain-registry', async () => {
|
||||
const profile = getServerProfile('AW-GZ-001');
|
||||
try {
|
||||
await ensureServerTunnel(profile);
|
||||
} catch {
|
||||
// 公共灯塔不可达时返回真实离线状态,界面仍可浏览已登记的四域结构。
|
||||
}
|
||||
const connected = await isServerTunnelReady(profile);
|
||||
let lighthouseStatus: any = null;
|
||||
let lighthouseHealth: any = null;
|
||||
if (connected) {
|
||||
const [statusResponse, healthResponse] = await Promise.all([
|
||||
fetch(`${lighthouseBaseUrl(profile)}/v1/status`, { signal: AbortSignal.timeout(3000) }),
|
||||
fetch(`${lighthouseBaseUrl(profile)}/health`, { signal: AbortSignal.timeout(3000) }),
|
||||
]);
|
||||
if (statusResponse.ok) lighthouseStatus = await statusResponse.json();
|
||||
if (healthResponse.ok) lighthouseHealth = await healthResponse.json();
|
||||
}
|
||||
const liveDomains = new Map((lighthouseStatus?.domains || []).map((domain: any) => [domain.id, domain]));
|
||||
const domain = (serverId: string, fallback: Record<string, unknown>) => ({
|
||||
...fallback,
|
||||
serverId,
|
||||
live: liveDomains.get(serverId) || null,
|
||||
});
|
||||
return {
|
||||
nodeId: profile.id,
|
||||
physicalNodeId: profile.physicalNodeId,
|
||||
connected,
|
||||
verified: Boolean(lighthouseStatus && lighthouseHealth),
|
||||
codeChannel: connected ? 'reachable' : 'not-connected',
|
||||
lighthouse: lighthouseHealth ? {
|
||||
mode: lighthouseHealth.mode,
|
||||
mapHash: lighthouseHealth.map_hash,
|
||||
execution: lighthouseHealth.execution,
|
||||
hostState: lighthouseStatus?.host_state,
|
||||
observedAt: lighthouseStatus?.observed_at,
|
||||
} : null,
|
||||
domains: [
|
||||
domain('DOMAIN-MAIN', { id: 'main', number: 'HLDP-DOMAIN-MAIN-001', name: '光湖主域', responsibility: '公共定义、版本发布、广播与模块生态状态', repository: 'domain-main' }),
|
||||
domain('DOMAIN-SUB', { id: 'sub', number: 'HLDP-DOMAIN-SUB-001', name: '光湖分域', responsibility: '行业入口、模块目录与初始化频道路由', repository: 'domain-sub' }),
|
||||
domain('DOMAIN-ZERO', { id: 'zero', number: 'HLDP-DOMAIN-ZERO-001', name: '光湖零域', responsibility: '模块试装、实验、质量验证与可回滚预览', repository: 'domain-zero' }),
|
||||
domain('DOMAIN-ZS', { id: 'zero-sense', number: 'HLDP-DOMAIN-ZEROSENSE-001', name: '光湖零感域', responsibility: '团队身份、责任、权限、审计与个人节点发现', repository: 'domain-zero-sense' }),
|
||||
],
|
||||
};
|
||||
});
|
||||
ipcMain.handle('server:session', async (_event, requestedNodeId?: string) => {
|
||||
const stored = applyStoredServerAuth();
|
||||
const profiles = readServerProfiles();
|
||||
const nodeId = requestedNodeId && profiles[requestedNodeId]
|
||||
? requestedNodeId
|
||||
: stored?.nodeId || defaultPersonalServerId() || '';
|
||||
if (!stored || stored.nodeId !== nodeId) return { authenticated: false, nodeId };
|
||||
try {
|
||||
const { response, data } = await forgejoRequest(nodeId, '/api/v1/user', {}, { token: stored.token });
|
||||
if (!response.ok) {
|
||||
fs.rmSync(SERVER_AUTH_PATH, { force: true });
|
||||
clearGitCredentialEnvironment();
|
||||
return { authenticated: false, nodeId };
|
||||
}
|
||||
return { authenticated: true, nodeId, username: data.login };
|
||||
} catch {
|
||||
return { authenticated: false, nodeId };
|
||||
}
|
||||
});
|
||||
ipcMain.handle('server:login', async (_event, input: { nodeId: string; username: string; password: string }) => {
|
||||
const username = String(input.username || '').trim();
|
||||
const password = String(input.password || '');
|
||||
getServerProfile(input.nodeId);
|
||||
if (!username || !password) throw new Error('请输入账号和密码');
|
||||
|
||||
const basicAuth = { username, password };
|
||||
const identity = await forgejoRequest(input.nodeId, '/api/v1/user', {}, basicAuth);
|
||||
if (!identity.response.ok) throw new Error('账号或密码不正确');
|
||||
|
||||
const tokenName = 'hololake-desktop';
|
||||
const existing = await forgejoRequest(input.nodeId, `/api/v1/users/${encodeURIComponent(username)}/tokens`, {}, basicAuth);
|
||||
if (existing.response.ok && Array.isArray(existing.data)) {
|
||||
for (const token of existing.data) {
|
||||
if (token?.name === tokenName && token?.id) {
|
||||
await forgejoRequest(
|
||||
input.nodeId,
|
||||
`/api/v1/users/${encodeURIComponent(username)}/tokens/${token.id}`,
|
||||
{ method: 'DELETE' },
|
||||
basicAuth,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const created = await forgejoRequest(
|
||||
input.nodeId,
|
||||
`/api/v1/users/${encodeURIComponent(username)}/tokens`,
|
||||
{ method: 'POST', body: JSON.stringify({ name: tokenName, scopes: ['all'] }) },
|
||||
basicAuth,
|
||||
);
|
||||
if (created.response.status !== 201 || !created.data?.sha1) {
|
||||
throw new Error(created.data?.message || '无法建立 HoloLake 登录令牌');
|
||||
}
|
||||
const previous = applyStoredServerAuth();
|
||||
if (!previous || previous.nodeId !== input.nodeId || previous.username !== identity.data.login) {
|
||||
await detachKnowledgeRemote();
|
||||
}
|
||||
saveServerAuth(input.nodeId, username, created.data.sha1);
|
||||
return { authenticated: true, nodeId: input.nodeId, username: identity.data.login };
|
||||
});
|
||||
ipcMain.handle('server:logout', async () => {
|
||||
const stored = applyStoredServerAuth();
|
||||
await detachKnowledgeRemote();
|
||||
fs.rmSync(SERVER_AUTH_PATH, { force: true });
|
||||
clearGitCredentialEnvironment();
|
||||
return { authenticated: false, nodeId: stored?.nodeId || defaultPersonalServerId() || '' };
|
||||
});
|
||||
ipcMain.handle('server:repositories', async () => {
|
||||
const stored = applyStoredServerAuth();
|
||||
if (!stored) throw new Error('请先登录服务器');
|
||||
const { response, data } = await forgejoRequest(stored.nodeId, '/api/v1/user/repos?limit=100', {}, { token: stored.token });
|
||||
if (!response.ok || !Array.isArray(data)) throw new Error(data?.message || '读取服务器仓库失败');
|
||||
return data.map(repo => ({
|
||||
name: repo.name,
|
||||
fullName: repo.full_name,
|
||||
private: repo.private,
|
||||
defaultBranch: repo.default_branch || 'main',
|
||||
}));
|
||||
});
|
||||
ipcMain.handle('server:create-repository', async (_event, input: { name: string; description?: string }) => {
|
||||
const stored = applyStoredServerAuth();
|
||||
if (!stored) throw new Error('请先登录服务器');
|
||||
const name = String(input.name || '').trim();
|
||||
if (!/^[A-Za-z0-9._-]{1,100}$/.test(name)) throw new Error('仓库名称只能使用字母、数字、点、短横线或下划线');
|
||||
const { response, data } = await forgejoRequest(stored.nodeId, '/api/v1/user/repos', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description: String(input.description || ''),
|
||||
private: true,
|
||||
auto_init: true,
|
||||
default_branch: 'main',
|
||||
}),
|
||||
}, { token: stored.token });
|
||||
if (response.status !== 201) throw new Error(data?.message || '创建仓库失败');
|
||||
return {
|
||||
name: data.name,
|
||||
fullName: data.full_name,
|
||||
private: data.private,
|
||||
defaultBranch: data.default_branch || 'main',
|
||||
};
|
||||
});
|
||||
ipcMain.handle('server:git-remote', async (_event, fullName: string) => {
|
||||
if (!/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(String(fullName || ''))) {
|
||||
throw new Error('仓库路径无效');
|
||||
}
|
||||
const stored = applyStoredServerAuth();
|
||||
if (!stored) throw new Error('请先登录服务器');
|
||||
const [owner, repository] = String(fullName).split('/');
|
||||
const access = await forgejoRequest(
|
||||
stored.nodeId,
|
||||
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}`,
|
||||
{},
|
||||
{ token: stored.token },
|
||||
);
|
||||
if (!access.response.ok || access.data?.full_name !== fullName) {
|
||||
throw new Error('当前账号无权访问该仓库');
|
||||
}
|
||||
return `${forgejoBaseUrl(getServerProfile(stored.nodeId))}/${fullName}.git`;
|
||||
});
|
||||
|
||||
// ─── 应用生命周期 ───
|
||||
|
|
@ -200,6 +669,7 @@ app.whenReady().then(async () => {
|
|||
try {
|
||||
applyModelConfig();
|
||||
await startServer();
|
||||
await waitForServerReady();
|
||||
createWindow();
|
||||
} catch (err) {
|
||||
dialog.showErrorBox('启动失败', `知识库引擎启动失败:${err}`);
|
||||
|
|
@ -218,4 +688,5 @@ app.on('window-all-closed', () => {
|
|||
|
||||
app.on('before-quit', () => {
|
||||
stopServer();
|
||||
stopServerTunnels();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { contextBridge, ipcRenderer } from 'electron';
|
|||
contextBridge.exposeInMainWorld('hololake', {
|
||||
// 应用信息
|
||||
platform: process.platform,
|
||||
version: '0.5.1',
|
||||
version: '0.7.0',
|
||||
|
||||
// 数据目录
|
||||
getDataPath: () => ipcRenderer.invoke('get-data-path'),
|
||||
|
|
@ -24,6 +24,21 @@ contextBridge.exposeInMainWorld('hololake', {
|
|||
getConfig: () => ipcRenderer.invoke('agent:get-config'),
|
||||
saveConfig: (config: { baseUrl: string; model: string; apiKey?: string }) =>
|
||||
ipcRenderer.invoke('agent:save-config', config),
|
||||
testConfig: () => ipcRenderer.invoke('agent:test-config'),
|
||||
},
|
||||
|
||||
server: {
|
||||
list: () => ipcRenderer.invoke('server:list'),
|
||||
connect: (nodeId: string) => ipcRenderer.invoke('server:connect', nodeId),
|
||||
domainRegistry: () => ipcRenderer.invoke('server:domain-registry'),
|
||||
session: (nodeId?: string) => ipcRenderer.invoke('server:session', nodeId),
|
||||
login: (input: { nodeId: string; username: string; password: string }) =>
|
||||
ipcRenderer.invoke('server:login', input),
|
||||
logout: () => ipcRenderer.invoke('server:logout'),
|
||||
repositories: () => ipcRenderer.invoke('server:repositories'),
|
||||
createRepository: (input: { name: string; description?: string }) =>
|
||||
ipcRenderer.invoke('server:create-repository', input),
|
||||
gitRemote: (fullName: string) => ipcRenderer.invoke('server:git-remote', fullName),
|
||||
},
|
||||
|
||||
// Forgejo 通过本地知识库 API 管理;此桥只保留非敏感应用信息。
|
||||
|
|
|
|||
190
product-source/hololake-desktop/package-lock.json
generated
190
product-source/hololake-desktop/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "hololake-desktop",
|
||||
"version": "0.5.1",
|
||||
"version": "0.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hololake-desktop",
|
||||
"version": "0.5.1",
|
||||
"version": "0.7.0",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"diff": "^9.0.0",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"concurrently": "^9.1.0",
|
||||
"electron": "^35.0.0",
|
||||
"electron": "^43.3.0",
|
||||
"electron-builder": "^26.0.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"react": "^19.0.0",
|
||||
|
|
@ -316,6 +316,16 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron-internal/extract-zip": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz",
|
||||
"integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/asar": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||
|
|
@ -420,25 +430,61 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||
"integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz",
|
||||
"integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"got": "^11.8.5",
|
||||
"env-paths": "^3.0.0",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"progress": "^2.0.3",
|
||||
"semver": "^6.2.0",
|
||||
"semver": "^7.6.3",
|
||||
"sumchecker": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"global-agent": "^3.0.0"
|
||||
"undici": "^7.24.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/node_modules/env-paths": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get/node_modules/undici": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
|
||||
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/notarize": {
|
||||
|
|
@ -2077,17 +2123,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
|
|
@ -2573,16 +2608,6 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
|
@ -3366,22 +3391,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "35.7.5",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-35.7.5.tgz",
|
||||
"integrity": "sha512-dnL+JvLraKZl7iusXTVTGYs10TKfzUi30uEDTqsmTm0guN9V2tbOjTzyIZbh9n3ygUjgEYyo+igAwMRXIi3IPw==",
|
||||
"version": "43.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-43.3.0.tgz",
|
||||
"integrity": "sha512-nLlvu0WFjftWsSaTkV2B/c4NDuJBspTyXu8vKSQ6vLvFt8uG3NgN49LLKcXddwX0GqVvAQDhciWp+4xOdTdhew==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
"@types/node": "^22.7.7",
|
||||
"extract-zip": "^2.0.1"
|
||||
"@electron-internal/extract-zip": "^1.0.1",
|
||||
"@electron/get": "^5.0.0",
|
||||
"@types/node": "^24.9.0"
|
||||
},
|
||||
"bin": {
|
||||
"electron": "cli.js"
|
||||
"electron": "cli.js",
|
||||
"install-electron": "install.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20.55"
|
||||
"node": ">= 22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder": {
|
||||
|
|
@ -3562,6 +3587,23 @@
|
|||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/@types/node": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
|
||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron/node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
|
|
@ -3815,27 +3857,6 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -3860,16 +3881,6 @@
|
|||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
|
|
@ -4007,21 +4018,6 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
"universalify": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
|
|
@ -5248,13 +5244,6 @@
|
|||
"url": "https://github.com/sponsors/jet2jet"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -7274,17 +7263,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "hololake-desktop",
|
||||
"version": "0.5.1",
|
||||
"description": "HoloLake Era 桌面版 — Git 驱动的知识库管理",
|
||||
"version": "0.7.0",
|
||||
"description": "HoloLake Era — 光湖语言操作系统平台",
|
||||
"main": "dist-electron/main.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"dev:server": "tsx watch ../guanghu-knowledge-base/server/index.ts",
|
||||
"dev:client": "vite",
|
||||
"dev:electron": "electron .",
|
||||
"test": "tsx --test ../guanghu-knowledge-base/server/*.test.ts ../guanghu-knowledge-base/src/*.test.ts",
|
||||
"build:electron": "esbuild electron/main.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist-electron/main.cjs && esbuild electron/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist-electron/preload.cjs",
|
||||
"build:server": "NODE_PATH=./node_modules esbuild ../guanghu-knowledge-base/server/index.ts --bundle --platform=node --format=cjs --outfile=dist-electron/server-bundle.cjs",
|
||||
"build": "vite build && npm run build:electron && npm run build:server",
|
||||
|
|
@ -33,7 +34,7 @@
|
|||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"concurrently": "^9.1.0",
|
||||
"electron": "^35.0.0",
|
||||
"electron": "^43.3.0",
|
||||
"electron-builder": "^26.0.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"react": "^19.0.0",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export default defineConfig({
|
|||
alias: {
|
||||
// 复用知识库前端组件
|
||||
'@': path.resolve(__dirname, '../guanghu-knowledge-base/src'),
|
||||
'react/jsx-dev-runtime': path.resolve(__dirname, 'node_modules/react/jsx-dev-runtime.js'),
|
||||
'react/jsx-runtime': path.resolve(__dirname, 'node_modules/react/jsx-runtime.js'),
|
||||
'react-dom/client': path.resolve(__dirname, 'node_modules/react-dom/client.js'),
|
||||
'react-dom': path.resolve(__dirname, 'node_modules/react-dom/index.js'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue