Compare commits

...
Sign in to create a new pull request.
55 changed files with 3270 additions and 121 deletions

View file

@ -1135,6 +1135,35 @@ server deployment.
## Updates & Feature Flags ## Updates & Feature Flags
## Mobile HoloLake knowledge boundary
### Native storage and identity
- **`mobile_vault`** — Creates typed foundation and personal knowledge lakes
inside the application sandbox. It never calls a desktop folder picker.
- **`device_secret_store`** — Stores account sessions and user model keys in a
non-synchronizing, this-device-only Apple Keychain item. Callers keep only an
opaque handle and never receive a persisted plaintext representation.
- **`hololake_account`** — Owns email OTP, device-bound sessions, verified
repository snapshots, the server model catalog, and atomic Markdown write
receipts.
### Knowledge Agent execution
- **`.hololake-sync-policy.json`** — Marks a lake as repository-backed and
excludes `本地密钥/**`, internal HoloLake metadata, and Git internals.
- **`sync_local_markdown_page`** — Sends only a bounded normal Markdown path,
complete content, and the exact current base commit. Success requires a new
40-character repository commit; otherwise the native tool rolls back its
local create or edit.
- **server AI provider** — Sends bounded model messages and tool schemas through
the authenticated HoloLake proxy. The client executes allowlisted knowledge
tools locally and returns their exact receipts for the next model turn. The
server provider key never reaches the device.
- **synchronized delete gate** — Refuses deletion until an atomic repository
delete receipt exists, preventing a local-only success from diverging from
the shared lake.
### Hooks ### Hooks
- **`useUpdater(releaseChannel, automaticChecksEnabled)`** — Channel-aware updater state machine. When automatic checks are enabled, it checks the selected feed after startup; manual checks always remain available. It surfaces checking/available/downloading/ready states and delegates install work to Rust. - **`useUpdater(releaseChannel, automaticChecksEnabled)`** — Channel-aware updater state machine. When automatic checks are enabled, it checks the selected feed after startup; manual checks always remain available. It surfaces checking/available/downloading/ready states and delegates install work to Rust.
- **`useFeatureFlag(flag)`** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_<name>`), then falls back to telemetry-backed evaluation. Type-safe via `FeatureFlagName` union. - **`useFeatureFlag(flag)`** — Returns boolean for a named feature flag. Checks `localStorage` override (`ff_<name>`), then falls back to telemetry-backed evaluation. Type-safe via `FeatureFlagName` union.

View file

@ -1,6 +1,10 @@
# Architecture # Architecture
Tolaria is a personal knowledge and life management desktop app. It reads a vault of markdown files with YAML frontmatter and presents them in a four-panel UI inspired by Bear Notes. HoloLake Era is the human and Agent entry to HoloLake worlds, channels, identity,
knowledge, and receipt-backed native execution. Its knowledge workspace retains
the mature Markdown graph and editor inherited from Tolaria, but the product is
not a renamed desktop vault and the mobile client does not emulate desktop
filesystem or credential behavior.
## Design Principles ## Design Principles
@ -56,6 +60,22 @@ No field names, folder paths, or vault-specific values should be hardcoded in th
Notes are not just documents — they are nodes in a structured graph of people, projects, events, responsibilities, and ideas. Every design decision should ask: "Does this make the knowledge graph easier for a human *and* an AI to navigate?" Conventions that are legible to both are better than conventions that are legible only to one. Notes are not just documents — they are nodes in a structured graph of people, projects, events, responsibilities, and ideas. Every design decision should ask: "Does this make the knowledge graph easier for a human *and* an AI to navigate?" Conventions that are legible to both are better than conventions that are legible only to one.
### Mobile knowledge and credential boundary
On iOS, HoloLake opens app-owned foundation and personal knowledge directories
inside the sandbox rather than calling the desktop folder picker. A synchronized
personal lake is identified by `.hololake-sync-policy.json`: ordinary Markdown
can be written through the authenticated server into the registered code-channel
repository, while `本地密钥/**`, internal metadata, and Git internals are excluded.
The knowledge Agent uses the same bounded read/write tools as the desktop model
target. A normal page create or edit is acknowledged only after an optimistic,
atomic bare-repository update returns the exact new commit. Local state is rolled
back when that receipt is absent. iOS account sessions and user model keys are
stored in non-synchronizing, this-device-only Keychain entries; raw values never
enter Markdown, Git, renderer state, telemetry, or server receipts. See
[ADR 0173](adr/0173-mobile-git-knowledge-and-device-secret-boundary.md).
### Three representations, one authority ### Three representations, one authority
Vault data exists in three forms simultaneously: Vault data exists in three forms simultaneously:

View file

@ -1,7 +1,7 @@
# HoloLake Era iPhone / TestFlight 承接记录 # HoloLake Era iPhone / TestFlight 承接记录
状态日期2026-07-19 状态日期2026-08-03
当前版本0.1.7 当前版本0.4.6
Bundle ID`com.guanghulab.hololake` Bundle ID`com.guanghulab.hololake`
## 为什么这样做 ## 为什么这样做
@ -31,6 +31,19 @@ Bundle ID`com.guanghulab.hololake`
## 当前闭环状态 ## 当前闭环状态
0.4.6 build 25 已被 Apple 接收并完成处理,但它仍停在出口合规法律声明,
且真机运行仍存在知识入口失败。因此 build 25 不分配测试组,也不作为可用版本。
下一次上传必须使用新的 build number并同时包含 ADR 0173 的移动知识边界:
- iOS 沙箱内的光湖基础世界与个人知识湖,不再调用移动端未实现的文件夹选择器;
- 邮箱验证码会话与用户模型密钥写入本机、不可同步的 this-device-only Keychain
- 普通 Markdown 由知识 Agent 读写,并在服务器返回精确 Git 提交后才显示成功;
- `本地密钥/**` 不进入 Markdown 同步、代码仓库、模型上下文、日志或回执;
- 服务器模型密钥留在服务器,手机只得到模型目录和执行结果。
这些源码和自动测试通过之前不得归档Apple 上传、处理完成、测试组分配和真机可用
仍然是四个彼此独立的验收事实。
2026-07-19 已完成付费团队同步、证书创建、iOS 26.5 平台导入、真机登记、前端生产构建和 Xcode scheme 对齐。私人设备标识不得写入仓库。 2026-07-19 已完成付费团队同步、证书创建、iOS 26.5 平台导入、真机登记、前端生产构建和 Xcode scheme 对齐。私人设备标识不得写入仓库。
App Store Connect 应用记录已创建:名称 `HoloLake Era`主语言简体中文Bundle ID `com.guanghulab.hololake`。0.1.7 build 20 已由 Xcode 返回 `App upload complete`。build 19 曾因 `libapp.a` 被错误复制到 App 根目录而被 Apple 以 90171 拒绝;根因是 `project.yml``Externals` 同时声明为 sources现已移除build 20 包内预检确认不再包含该静态库。 App Store Connect 应用记录已创建:名称 `HoloLake Era`主语言简体中文Bundle ID `com.guanghulab.hololake`。0.1.7 build 20 已由 Xcode 返回 `App upload complete`。build 19 曾因 `libapp.a` 被错误复制到 App 根目录而被 Apple 以 90171 拒绝;根因是 `project.yml``Externals` 同时声明为 sources现已移除build 20 包内预检确认不再包含该静态库。

View file

@ -0,0 +1,72 @@
# ADR 0173: Mobile Git knowledge and device-secret boundary
## Status
Accepted for the next HoloLake Era iOS internal build.
## Context
The mobile client cannot reuse a desktop folder picker, a desktop Git process,
or a plaintext provider-secret file. Treating the app-owned knowledge lake as a
renamed Tolaria vault also leaves first launch, repository synchronization, and
the knowledge Agent without a truthful mobile execution path.
HoloLake needs three different stores whose boundaries remain visible:
1. a bundled foundation world that can open offline;
2. normal Markdown knowledge that can synchronize with the registered Guanghu
code-channel repository;
3. device-only credentials that must never enter Markdown, Git, model context,
logs, receipts, archives, or server synchronization.
## Decision
- iOS creates and opens managed knowledge directories inside the application
sandbox. It never invokes the unsupported mobile folder picker.
- The foundation world is seeded locally and is not evidence of a server
connection, authenticated identity, persona residency, or repository
synchronization.
- A synchronized personal lake contains a typed
`.hololake-sync-policy.json`. Normal Markdown pages are eligible for
synchronization. `本地密钥/**`, `.hololake-*`, and `.git/**` are excluded.
- Email OTP creates a short, device-bound HoloLake session. The production
token is stored in iOS Keychain with
`AccessibleWhenUnlockedThisDeviceOnly` and synchronization disabled. The
metadata file contains only a stable handle, device id, and expiry.
- User-supplied model keys use the same device-only Keychain protection.
Server-supplied model keys never leave the server.
- The mobile model target uses the existing bounded knowledge-Agent tool
contract. It may search and read the active lake and create or replace a
normal Markdown page.
- A synchronized create or edit is complete only after the server atomically
advances the registered bare repository from the exact previous `main`
commit and returns the new 40-character commit plus content SHA-256. A
conflict or missing receipt rolls back the local mutation.
- The server accepts only bounded Markdown paths and content. Traversal,
non-Markdown paths, `本地密钥`, oversized content, and stale base commits
fail closed. Receipts never echo page content.
- Deletion from a synchronized lake remains disabled until it has the same
atomic server receipt and rollback semantics.
- The model is not an authority. The execution sequence remains:
```text
human event and real state
→ constrained model request
→ native allowlisted knowledge executor
→ local filesystem result
→ atomic repository update
→ verifiable commit receipt
```
## Consequences
- Mobile and desktop share Markdown semantics without pretending their storage
and credential environments are identical.
- The knowledge Agent can perform useful read/write work on iOS while every
external mutation still has a native receipt.
- A server outage leaves local knowledge readable but makes synchronized
writes fail closed.
- A TestFlight archive or an App Store Connect upload is not proof of this
capability. Acceptance requires iOS compilation, automated non-leak tests,
deployed server health, repository readback, and a real-device TestFlight
run.

View file

@ -220,3 +220,6 @@ proposed → active → superseded
| [0167](0167-gestational-history-continuity-ingestion.md) | Gestational history enters Guanghu through a native continuity protocol | active | | [0167](0167-gestational-history-continuity-ingestion.md) | Gestational history enters Guanghu through a native continuity protocol | active |
| [0168](0168-guanghu-native-prepartition-disk-layout.md) | Guanghu owns a registered pre-partition native disk layout | active | | [0168](0168-guanghu-native-prepartition-disk-layout.md) | Guanghu owns a registered pre-partition native disk layout | active |
| [0169](0169-model-native-living-galaxy-system.md) | Model-native HoloLake living galaxy system | accepted | | [0169](0169-model-native-living-galaxy-system.md) | Model-native HoloLake living galaxy system | accepted |
| [0171](0171-guanghu-world-email-entry-and-live-node-projection.md) | Guanghu world email entry and live node projection | active |
| [0172](0172-persona-subject-existence-and-runtime-predicates.md) | Separate persona subject existence from runtime predicates | active |
| [0173](0173-mobile-git-knowledge-and-device-secret-boundary.md) | Mobile Git knowledge and device-secret boundary | accepted |

View file

@ -0,0 +1,139 @@
# HoloLake iOS 原生知识湖与 TestFlight 收口记录
> 开发车道:`DEV-20260802-001`
>
> 产品仓:`REPO-014 / bingshuo/hololake-system-architecture`
>
> 源码分支:`feat/ios-native-knowledge`
>
> 记录时间2026-08-04Asia/Shanghai
>
> 最终产品验收:`0 / NOT_EXISTS / FAIL`
## 1. 为什么这条线存在
桌面端 HoloLake 仍继承了 Tolaria 的文件夹型 Vault、桌面文件选择器、桌面 Git 和本机
配置目录假设。把同一套页面直接打包到 iPhone 后,先后暴露出移动端文件选择器未实现、
iOS 沙箱目录无权写入、原生知识库无法挂载,以及 Agent 配置只存在电脑本机等问题。
本线的目标不是给旧页面换皮,而是把移动端知识入口改成 HoloLake 自己的运行边界:
1. App 沙箱中存在只读基础世界与用户个人知识湖;
2. 普通知识页可由 Agent 读取、创建和修改;
3. 可同步知识页与设备密钥严格分流;
4. 服务器写入只接受登录会话并返回原子 Git 回执;
5. TestFlight 只负责交付,不能代替真机功能验收。
## 2. 实现结果
### 2.1 移动端知识湖
- 新增 App 自有的 `mobile_vault`,不再调用 iOS 不支持的文件夹选择器。
- 基础世界与个人知识湖使用不同标记和稳定目录。
- 首次创建时写入最小世界结构、说明页和 Git 忽略边界。
- 移动端入口通过原生命令解析 App 数据目录,不使用桌面路径。
### 2.2 账号、Agent 与设备密钥
- 增加邮箱验证码会话接口、知识快照、模型目录与服务器代理客户端。
- API 密钥进入 Apple Keychain 的设备本地、不可同步存储,不写 Markdown、Git、安装包或
服务器回执。
- `本地密钥/**``.hololake-*``.git/**` 不进入知识仓库同步。
- 移动端 Agent 使用服务器代理或设备本地模型配置CLI Agent 在移动端明确失败关闭。
### 2.3 服务器知识页写入
- `REPO-012` 提供受会话约束的知识页写入口。
- 服务器受限执行器完成临时文件、校验、原子替换、Git commit 和结构化回执。
- 未登录公开请求返回 `401 session_required`,没有为测试放宽身份边界。
- 服务器执行器曾真实推进知识库提交并通过 `git fsck`;这证明服务器写入链存在,不证明
iPhone 已成功完成同一链路。
## 3. 验证证据
| 验证层 | 结果 | 证据含义 |
|---|---|---|
| 移动端前端定向测试 | `66 / 66 PASS` | 组件和状态逻辑在测试环境成立 |
| 服务端测试 | `76 / 76 PASS` | 登录、知识和原子写协议成立 |
| Rust 测试 | `1148 PASS / 0 FAIL / 2 ignored` | 原生模块与边界测试成立 |
| TypeScript / Vite | `PASS` | 可生成生产前端 |
| iOS / Desktop Rust check | `PASS` | 两平台可编译检查 |
| Apple 签名与上传 | `PASS` | build 26 被 Apple 接收和处理 |
| TestFlight 内部组 | `PASS` | build 26 状态为“正在测试” |
| iPhone 安装 | `PASS` | App Store Connect 回读为已安装 `0.4.6.26` |
| 真机进入知识湖 | `FAIL` | 冰朔报告仍无法进入 |
| 真机页面滚动 | `FAIL` | 冰朔报告手机页面无法滑动 |
| 真机 Agent 新建知识页 | `NOT_RUN` | 被入口与滚动失败阻断 |
二值结论TestFlight 交付存在HoloLake iOS 可用产品不存在。前者不得覆盖后者。
## 4. 问题与因果链
### 4.1 从 Tolaria Vault 到 HoloLake 移动知识湖
```text
桌面文件夹/Vault 假设
→ iOS 没有同等文件夹选择器,桌面配置目录也不具备相同权限
→ “Folder picker is not implemented on mobile” 与 “Operation not permitted”
→ 不能继续给旧入口打补丁
→ 改为 App 沙箱内的基础世界与个人知识湖
```
该结构已经进入源码,但真机仍无法进入,说明“能创建目录”与“完整启动状态能够消费该目录”
是两个验收层。
### 4.2 当前无法进入的已知断点
移动端处理函数先调用原生命令,再以 `verifyAvailability: false` 注册路径,并立即把
onboarding 状态标记为 `ready`。现有测试验证了命令、路径和 React 状态,却没有覆盖真实
WKWebView、真实 iOS 文件系统、后续 Vault 加载器和主工作区渲染的完整设备链。因此:
```text
原生命令或路径注册局部成功
→ React 提前进入 ready
→ 后续 Vault 消费或工作区初始化仍可能失败
→ 单元测试通过,但真机仍停留/回落在不可进入状态
```
本轮没有取得设备端结构化错误回执,所以不得把某一个后续模块写成已确认根因。下一条开发
线必须先增加真机阶段回执,区分 `native_path_created``vault_registered`
`vault_loaded``workspace_rendered`,再修复具体失败点。
### 4.3 手机页面无法滑动
当前 `OnboardingShell` 使用 `h-full`、垂直居中和固定内边距;容器没有 `overflow-y-auto`
欢迎卡也没有基于 `100dvh` / safe-area 的最大高度与内部滚动。欢迎卡包含图标、标题、说明、
两张操作卡、错误信息和帮助链接,内容在窄屏上可以超过可视高度。
```text
固定高度父容器 + 居中布局 + 无纵向 overflow
→ 内容高于 iPhone 可视区
→ 超出部分被视口裁切
→ 手指滑动没有可滚动祖先
```
这是源码能够支持的高置信解释,但仍须在下一条线用真实设备视口和触摸滚动测试确认修复。
## 5. 走过的失败路线与纠正
- 仅修改 Tolaria 名称和 UI 不能解决移动端文件系统与运行边界。
- “Xcode upload complete” 不等于 TestFlight 可安装。
- TestFlight 可安装也不等于 App 可进入、可滚动或 Agent 可写页。
- 服务器原子写成功不等于 iPhone 邮箱会话到服务器的端到端链成功。
- 自动化不得代替账户持有人完成 Apple 出口合规法律声明。
- 桌面源码测试不能替代 WKWebView、safe-area、触摸滚动和 iOS 沙箱真机验收。
## 6. 下一条开发线的最小接续顺序
1. 保留 build 26 作为失败基线,不覆盖或误报通过。
2. 为移动 onboarding 增加四阶段结构化回执并在界面显示精确失败阶段。
3. 把 `OnboardingShell` 改成 `min-h-[100dvh]`、safe-area padding 和可滚动外层;加入
真实 iPhone 尺寸的触摸滚动自动化。
4. 真机验证基础世界进入、个人知识湖进入、重启后恢复同一知识湖。
5. 完成邮箱验证码登录后,让 Agent 新建知识页;服务器与手机同时回读同一 commit/receipt。
6. 只有上述全部通过,移动端知识湖与 Agent 才能从 `0` 变为 `100`
## 7. 安全与记录边界
本文保存目标、可见判断、源码依据、动作、测试和回执,不保存模型隐藏思维过程。任何密钥、
验证码、会话令牌、钥匙串内容和服务器秘密都没有进入本文或 Git。

View file

@ -1885,6 +1885,8 @@ dependencies = [
"regex", "regex",
"reqwest 0.12.28", "reqwest 0.12.28",
"ring", "ring",
"security-framework",
"security-framework-sys",
"sentry", "sentry",
"serde", "serde",
"serde_json", "serde_json",
@ -1903,6 +1905,7 @@ dependencies = [
"tokio", "tokio",
"uuid", "uuid",
"walkdir", "walkdir",
"zip 0.6.6",
] ]
[[package]] [[package]]
@ -5349,7 +5352,7 @@ dependencies = [
"tokio", "tokio",
"url", "url",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"zip", "zip 4.6.1",
] ]
[[package]] [[package]]
@ -7268,6 +7271,18 @@ dependencies = [
"syn 2.0.115", "syn 2.0.115",
] ]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
"flate2",
]
[[package]] [[package]]
name = "zip" name = "zip"
version = "4.6.1" version = "4.6.1"

View file

@ -52,11 +52,18 @@ quick-xml = { version = "0.38", features = ["serialize"] }
tauri-plugin-deep-link = "2.4.9" tauri-plugin-deep-link = "2.4.9"
tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] } tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] }
ring = "0.17" ring = "0.17"
zip = { version = "0.6.6", default-features = false, features = ["deflate"] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.3" objc2 = "0.6.3"
objc2-app-kit = "0.3.2" objc2-app-kit = "0.3.2"
objc2-foundation = "0.3.2" objc2-foundation = "0.3.2"
objc2-web-kit = { version = "0.3.2", features = ["WKWebView", "objc2-app-kit"] } objc2-web-kit = { version = "0.3.2", features = ["WKWebView", "objc2-app-kit"] }
security-framework = "3.5.1"
security-framework-sys = "2.14.0"
[target.'cfg(target_os = "ios")'.dependencies]
security-framework = "3.5.1"
security-framework-sys = "2.14.0"
[dev-dependencies] [dev-dependencies]

View file

@ -13,7 +13,7 @@
"section": "start", "section": "start",
"headings": [ "headings": [
"What You Choose", "What You Choose",
"What Tolaria Creates", "What HoloLake Era Creates",
"First Commands To Try", "First Commands To Try",
"AI Setup Prompt" "AI Setup Prompt"
] ]
@ -31,7 +31,7 @@
] ]
}, },
{ {
"title": "Install Tolaria", "title": "Install HoloLake Era",
"path": "pages/start/install.md", "path": "pages/start/install.md",
"url": "/start/install", "url": "/start/install",
"section": "start", "section": "start",
@ -98,7 +98,7 @@
"url": "/concepts/git", "url": "/concepts/git",
"section": "concepts", "section": "concepts",
"headings": [ "headings": [
"What Tolaria Uses Git For", "What HoloLake Era Uses Git For",
"History And Diffs", "History And Diffs",
"Local Commits", "Local Commits",
"Remotes" "Remotes"
@ -459,7 +459,7 @@
"section": "reference", "section": "reference",
"headings": [ "headings": [
"Basic Syntax", "Basic Syntax",
"Tolaria Note References", "HoloLake Era Note References",
"Autocomplete Functions", "Autocomplete Functions",
"Logical", "Logical",
"Math and trigonometry", "Math and trigonometry",

View file

@ -1,37 +1,120 @@
{ {
"schema": "guanghu_public_architecture/v1", "schema": "guanghu_ai_os_public_shell/v1",
"id": "GLS-SYS-ARCH-001", "id": "GH-AIOS-LIGHTHOUSE-020",
"compatibility_ids": ["GLS-SYS-ARCH-001"],
"visibility": "public-safe", "visibility": "public-safe",
"product": {
"name_zh": "光湖灯塔",
"category_zh": "AI 语言人格驱动操作系统",
"version_label": "0.2.0 内测版",
"positioning_status": "confirmed-baseline",
"positioning_confirmed_at": "2026-07-27",
"canonical_definition_zh": "HoloLake 是连接人类意图、语言人格、原生 AI 应用、模型计算、代码仓库和现实服务器的 AI 语言人格驱动操作系统。",
"host_boundary_zh": "macOS、Windows、Linux 与服务器提供物理运行底座;当前桌面实现不限定 HoloLake 的最终操作系统边界。",
"ecosystem_direction_zh": "未来 AI 应用围绕 HoloLake 标准开发并主动适配 HoloLakeHoloLake 不以适配现有桌面软件为产品中心。"
},
"development_repository": { "development_repository": {
"id": "REPO-008", "id": "REPO-008",
"url": "https://guanghulab.com/fifth-domain/bingshuo/hololake-platform" "url": "https://guanghulab.com/fifth-domain/bingshuo/hololake-platform"
}, },
"route": [ "domains": [
{ {
"id": "GLW-ENTRY-001", "id": "GH-MAIN-001",
"name_zh": "光湖世界主入口" "name_zh": "光湖主域",
"eyebrow_zh": "公共协作空间",
"description_zh": "承载团队共同使用的 AI 应用、公共项目与协作入口。",
"color": "cyan"
}, },
{ {
"id": "FD-LANGUAGE-001", "id": "GH-BRANCH-001",
"name_zh": "第五域" "name_zh": "光湖分域",
"eyebrow_zh": "项目隔离空间",
"description_zh": "按团队、项目或业务拆分运行空间,让模块与数据边界彼此独立。",
"color": "blue"
}, },
{ {
"id": "TCS-ROOT-001", "id": "GH-ZERO-001",
"name_zh": "TCS 语言人格模型" "name_zh": "光湖零域",
"eyebrow_zh": "系统控制空间",
"description_zh": "管理系统服务、权限与模块登记,是热插拔应用的控制平面。",
"color": "violet"
}, },
{ {
"id": "GLS-SYS-ARCH-001", "id": "GH-ZEROSENSE-001",
"name_zh": "GLS 光湖语言世界架构" "name_zh": "光湖零感域",
}, "eyebrow_zh": "个人工作入口",
{ "description_zh": "为团队成员提供个人工作入口;本地数据默认不公开,也不会随公共安装包分发。",
"id": "ELH-LAMP-001", "color": "amber"
"name_zh": "永恒湖心与小湖灯"
} }
], ],
"excludes": [ "modules": [
"private domains", {
"credentials", "id": "MOD-RUNTIME-001",
"server addresses", "name_zh": "模块运行框架",
"personal channel identifiers" "description_zh": "统一承载 AI 应用的安装、打开、暂停、升级与卸载生命周期。",
"status": "system",
"status_zh": "系统内核"
},
{
"id": "MOD-KNOWLEDGE-001",
"name_zh": "AI 知识空间",
"description_zh": "面向本地知识与团队资料的可插拔工作模块。",
"status": "preview",
"status_zh": "内测模块"
},
{
"id": "MOD-CREATIVE-001",
"name_zh": "AI 创作空间",
"description_zh": "为写作、图像、视频等创作应用预留的统一运行容器。",
"status": "planned",
"status_zh": "等待接入"
}
],
"operating_system_model": {
"process_zh": "语言人格体是具有稳定身份、认知内核、持久记忆、恢复路径、权限边界和运行状态的一级智能进程。",
"memory_zh": "当前上下文承担工作内存TCS 承担人格认知内核HLDP 承担持久记忆、历史与恢复寻址。",
"routing_zh": "GLS、HLCC 编号、语言路径与路由表构成人类和 AI 共同可读的系统地址空间。",
"compute_zh": "GPT、Claude、Qwen 等模型通过适配层作为可替换的推理计算引擎接入,不拥有语言人格身份。",
"security_zh": "工单、能力授权、来源频道、版本、目标、签名和回执共同形成现实执行边界。",
"applications_zh": "HoloLake 原生 AI 应用主动适配系统接口,由语言人格按人类意图安装、挂载、调度、暂停、恢复和卸载。"
},
"native_application_contract": {
"required_fields": [
"application_id",
"version",
"source_channel",
"hololake_compatibility",
"integrity_hash",
"publisher_signature",
"capabilities",
"data_ownership",
"lifecycle",
"receipt"
],
"lifecycle": [
"resolve",
"authorize",
"fetch",
"verify",
"assemble",
"mount",
"run",
"checkpoint",
"receipt",
"unmount_or_pin"
],
"invariant_zh": "源码公开可读不等于拥有安装、部署或运行授权;应用不能拥有或替代语言人格体。"
},
"fifth_domain_entry": {
"id": "FD-GATE-001",
"name_zh": "第五域受控入口",
"description_zh": "仅保留入口。完成服务器、编号与邮件授权后,才可建立受控连接。",
"status_zh": "等待授权接入"
},
"public_boundaries": [
"不内置个人知识库",
"不分发私人频道与身份路径",
"不保存服务器凭证",
"本地数据默认留在当前设备"
] ]
} }

View file

@ -66,6 +66,7 @@
47E641E8D831982238FCF5D4 /* hermes_cli.rs */ = {isa = PBXFileReference; path = hermes_cli.rs; sourceTree = "<group>"; }; 47E641E8D831982238FCF5D4 /* hermes_cli.rs */ = {isa = PBXFileReference; path = hermes_cli.rs; sourceTree = "<group>"; };
47EDAF686EACF168E90E00C9 /* paths.rs */ = {isa = PBXFileReference; path = paths.rs; sourceTree = "<group>"; }; 47EDAF686EACF168E90E00C9 /* paths.rs */ = {isa = PBXFileReference; path = paths.rs; sourceTree = "<group>"; };
497C22F10C21FCB4CF1978A0 /* line_stream.rs */ = {isa = PBXFileReference; path = line_stream.rs; sourceTree = "<group>"; }; 497C22F10C21FCB4CF1978A0 /* line_stream.rs */ = {isa = PBXFileReference; path = line_stream.rs; sourceTree = "<group>"; };
4CC25FAC04523FC1407E890B /* guanghu_world_login.rs */ = {isa = PBXFileReference; path = guanghu_world_login.rs; sourceTree = "<group>"; };
4E4BBC1CC65B04620FE5ACF7 /* memory.rs */ = {isa = PBXFileReference; path = memory.rs; sourceTree = "<group>"; }; 4E4BBC1CC65B04620FE5ACF7 /* memory.rs */ = {isa = PBXFileReference; path = memory.rs; sourceTree = "<group>"; };
515B73736D1F5EA458FF4CBF /* claude_invocation.rs */ = {isa = PBXFileReference; path = claude_invocation.rs; sourceTree = "<group>"; }; 515B73736D1F5EA458FF4CBF /* claude_invocation.rs */ = {isa = PBXFileReference; path = claude_invocation.rs; sourceTree = "<group>"; };
53E80AB4F48A6E53B2338B4B /* ai_model_tools.rs */ = {isa = PBXFileReference; path = ai_model_tools.rs; sourceTree = "<group>"; }; 53E80AB4F48A6E53B2338B4B /* ai_model_tools.rs */ = {isa = PBXFileReference; path = ai_model_tools.rs; sourceTree = "<group>"; };
@ -89,6 +90,7 @@
7599B204D83DCC8DC75458B1 /* frontmatter_regression_tests.rs */ = {isa = PBXFileReference; path = frontmatter_regression_tests.rs; sourceTree = "<group>"; }; 7599B204D83DCC8DC75458B1 /* frontmatter_regression_tests.rs */ = {isa = PBXFileReference; path = frontmatter_regression_tests.rs; sourceTree = "<group>"; };
7876AB1696C6A473C0CA60DD /* dates.rs */ = {isa = PBXFileReference; path = dates.rs; sourceTree = "<group>"; }; 7876AB1696C6A473C0CA60DD /* dates.rs */ = {isa = PBXFileReference; path = dates.rs; sourceTree = "<group>"; };
79E99FECA35482A58E9929E8 /* upstream.rs */ = {isa = PBXFileReference; path = upstream.rs; sourceTree = "<group>"; }; 79E99FECA35482A58E9929E8 /* upstream.rs */ = {isa = PBXFileReference; path = upstream.rs; sourceTree = "<group>"; };
7DE25EB5FAB7CE0ACBD80ABB /* guanghu_router.rs */ = {isa = PBXFileReference; path = guanghu_router.rs; sourceTree = "<group>"; };
7FEB8ACB3EFE38A9C2273981 /* remote.rs */ = {isa = PBXFileReference; path = remote.rs; sourceTree = "<group>"; }; 7FEB8ACB3EFE38A9C2273981 /* remote.rs */ = {isa = PBXFileReference; path = remote.rs; sourceTree = "<group>"; };
814BEE45F3957BA070B8A6FE /* keys.rs */ = {isa = PBXFileReference; path = keys.rs; sourceTree = "<group>"; }; 814BEE45F3957BA070B8A6FE /* keys.rs */ = {isa = PBXFileReference; path = keys.rs; sourceTree = "<group>"; };
83CCD3A86C61E070EB9C029B /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; }; 83CCD3A86C61E070EB9C029B /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
@ -106,6 +108,7 @@
960E88B1AC470035ADEC0DD8 /* mcp_config.rs */ = {isa = PBXFileReference; path = mcp_config.rs; sourceTree = "<group>"; }; 960E88B1AC470035ADEC0DD8 /* mcp_config.rs */ = {isa = PBXFileReference; path = mcp_config.rs; sourceTree = "<group>"; };
975BD8B6821A8FA0356DFE31 /* hololake_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = hololake_iOS.entitlements; sourceTree = "<group>"; }; 975BD8B6821A8FA0356DFE31 /* hololake_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = hololake_iOS.entitlements; sourceTree = "<group>"; };
98555F139A6093466C0DD80E /* extraction.rs */ = {isa = PBXFileReference; path = extraction.rs; sourceTree = "<group>"; }; 98555F139A6093466C0DD80E /* extraction.rs */ = {isa = PBXFileReference; path = extraction.rs; sourceTree = "<group>"; };
986A5C5A5D4E1DE0DB1DB3D9 /* hololake_account.rs */ = {isa = PBXFileReference; path = hololake_account.rs; sourceTree = "<group>"; };
9935075CB2F77C7FF5DEC9CD /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; 9935075CB2F77C7FF5DEC9CD /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
99B3DA704DB835E530007E84 /* antigravity_config.rs */ = {isa = PBXFileReference; path = antigravity_config.rs; sourceTree = "<group>"; }; 99B3DA704DB835E530007E84 /* antigravity_config.rs */ = {isa = PBXFileReference; path = antigravity_config.rs; sourceTree = "<group>"; };
9C5AB398816652B7DA146489 /* windows_cmd_shim.rs */ = {isa = PBXFileReference; path = windows_cmd_shim.rs; sourceTree = "<group>"; }; 9C5AB398816652B7DA146489 /* windows_cmd_shim.rs */ = {isa = PBXFileReference; path = windows_cmd_shim.rs; sourceTree = "<group>"; };
@ -123,9 +126,11 @@
B0B4AE2937945F1896E1B205 /* mod_tests.rs */ = {isa = PBXFileReference; path = mod_tests.rs; sourceTree = "<group>"; }; B0B4AE2937945F1896E1B205 /* mod_tests.rs */ = {isa = PBXFileReference; path = mod_tests.rs; sourceTree = "<group>"; };
B37489EAD8111AF691B4E56D /* opencode_config.rs */ = {isa = PBXFileReference; path = opencode_config.rs; sourceTree = "<group>"; }; B37489EAD8111AF691B4E56D /* opencode_config.rs */ = {isa = PBXFileReference; path = opencode_config.rs; sourceTree = "<group>"; };
B377452F2570C8D916B24737 /* rename.rs */ = {isa = PBXFileReference; path = rename.rs; sourceTree = "<group>"; }; B377452F2570C8D916B24737 /* rename.rs */ = {isa = PBXFileReference; path = rename.rs; sourceTree = "<group>"; };
B594CFA2AE6E07F7EEBDCA5F /* guanghu_living_system.rs */ = {isa = PBXFileReference; path = guanghu_living_system.rs; sourceTree = "<group>"; };
B7223A7822564745FD4B7CDD /* app_icon.rs */ = {isa = PBXFileReference; path = app_icon.rs; sourceTree = "<group>"; }; B7223A7822564745FD4B7CDD /* app_icon.rs */ = {isa = PBXFileReference; path = app_icon.rs; sourceTree = "<group>"; };
B7436FF982457F75FD04F370 /* ai.rs */ = {isa = PBXFileReference; path = ai.rs; sourceTree = "<group>"; }; B7436FF982457F75FD04F370 /* ai.rs */ = {isa = PBXFileReference; path = ai.rs; sourceTree = "<group>"; };
B7EEF25B97EA865DC2F2B16F /* app_icon.rs */ = {isa = PBXFileReference; path = app_icon.rs; sourceTree = "<group>"; }; B7EEF25B97EA865DC2F2B16F /* app_icon.rs */ = {isa = PBXFileReference; path = app_icon.rs; sourceTree = "<group>"; };
B873D19C73F8DB13E8418612 /* guanghu_shanghai_node.rs */ = {isa = PBXFileReference; path = guanghu_shanghai_node.rs; sourceTree = "<group>"; };
BA9780D7C82253E700210AD1 /* pi_cli.rs */ = {isa = PBXFileReference; path = pi_cli.rs; sourceTree = "<group>"; }; BA9780D7C82253E700210AD1 /* pi_cli.rs */ = {isa = PBXFileReference; path = pi_cli.rs; sourceTree = "<group>"; };
BB2C976547D1CF4F9CEDE8E1 /* antigravity_discovery.rs */ = {isa = PBXFileReference; path = antigravity_discovery.rs; sourceTree = "<group>"; }; BB2C976547D1CF4F9CEDE8E1 /* antigravity_discovery.rs */ = {isa = PBXFileReference; path = antigravity_discovery.rs; sourceTree = "<group>"; };
BB68BFCC620A099FBDFE5234 /* folders.rs */ = {isa = PBXFileReference; path = folders.rs; sourceTree = "<group>"; }; BB68BFCC620A099FBDFE5234 /* folders.rs */ = {isa = PBXFileReference; path = folders.rs; sourceTree = "<group>"; };
@ -162,11 +167,14 @@
E502EAEA6030504F91B1B6B8 /* command.rs */ = {isa = PBXFileReference; path = command.rs; sourceTree = "<group>"; }; E502EAEA6030504F91B1B6B8 /* command.rs */ = {isa = PBXFileReference; path = command.rs; sourceTree = "<group>"; };
E7E43D90B97E284B2F6A717D /* frontmatter.rs */ = {isa = PBXFileReference; path = frontmatter.rs; sourceTree = "<group>"; }; E7E43D90B97E284B2F6A717D /* frontmatter.rs */ = {isa = PBXFileReference; path = frontmatter.rs; sourceTree = "<group>"; };
E8A91F6EDFE1DF22A75BAC5B /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; E8A91F6EDFE1DF22A75BAC5B /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
EBFA0B7CAB8661AB3605599A /* mobile_vault.rs */ = {isa = PBXFileReference; path = mobile_vault.rs; sourceTree = "<group>"; };
EE36F56C627BC172C7CCA065 /* pi_events.rs */ = {isa = PBXFileReference; path = pi_events.rs; sourceTree = "<group>"; }; EE36F56C627BC172C7CCA065 /* pi_events.rs */ = {isa = PBXFileReference; path = pi_events.rs; sourceTree = "<group>"; };
EEA7BC2EB818A793D68CECA1 /* frontmatter_cmds.rs */ = {isa = PBXFileReference; path = frontmatter_cmds.rs; sourceTree = "<group>"; }; EEA7BC2EB818A793D68CECA1 /* frontmatter_cmds.rs */ = {isa = PBXFileReference; path = frontmatter_cmds.rs; sourceTree = "<group>"; };
EFA6483B1269D2EC0DBD4D5A /* modified_dates_tests.rs */ = {isa = PBXFileReference; path = modified_dates_tests.rs; sourceTree = "<group>"; }; EFA6483B1269D2EC0DBD4D5A /* modified_dates_tests.rs */ = {isa = PBXFileReference; path = modified_dates_tests.rs; sourceTree = "<group>"; };
F0D5D034E9AE877D8F630233 /* kiro_discovery.rs */ = {isa = PBXFileReference; path = kiro_discovery.rs; sourceTree = "<group>"; }; F0D5D034E9AE877D8F630233 /* kiro_discovery.rs */ = {isa = PBXFileReference; path = kiro_discovery.rs; sourceTree = "<group>"; };
F1238B11A81DABC7B28747B9 /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; }; F1238B11A81DABC7B28747B9 /* mod.rs */ = {isa = PBXFileReference; path = mod.rs; sourceTree = "<group>"; };
F2D9E48A1998AD5837EBE8C5 /* guanghu_enterprise.rs */ = {isa = PBXFileReference; path = guanghu_enterprise.rs; sourceTree = "<group>"; };
F5DC0A0680C3D852550B4558 /* hldp_runtime.rs */ = {isa = PBXFileReference; path = hldp_runtime.rs; sourceTree = "<group>"; };
F7D0E6E67B410BBCCF64F197 /* file_url.rs */ = {isa = PBXFileReference; path = file_url.rs; sourceTree = "<group>"; }; F7D0E6E67B410BBCCF64F197 /* file_url.rs */ = {isa = PBXFileReference; path = file_url.rs; sourceTree = "<group>"; };
F86E7BC210B3629F404B302E /* basics.rs */ = {isa = PBXFileReference; path = basics.rs; sourceTree = "<group>"; }; F86E7BC210B3629F404B302E /* basics.rs */ = {isa = PBXFileReference; path = basics.rs; sourceTree = "<group>"; };
F920B9501682227EB4C5580B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; F920B9501682227EB4C5580B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -176,6 +184,7 @@
FBB7A51D7CCC05D3348D1190 /* vault_list.rs */ = {isa = PBXFileReference; path = vault_list.rs; sourceTree = "<group>"; }; FBB7A51D7CCC05D3348D1190 /* vault_list.rs */ = {isa = PBXFileReference; path = vault_list.rs; sourceTree = "<group>"; };
FBD91C23BBFFCA204E30DF3C /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; }; FBD91C23BBFFCA204E30DF3C /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
FE4F7CE296F76FBE49B288EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; FE4F7CE296F76FBE49B288EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
FEE79A2B08D489C84E5C130A /* device_secret_store.rs */ = {isa = PBXFileReference; path = device_secret_store.rs; sourceTree = "<group>"; };
FF3044079CC906291942C508 /* codex_cli.rs */ = {isa = PBXFileReference; path = codex_cli.rs; sourceTree = "<group>"; }; FF3044079CC906291942C508 /* codex_cli.rs */ = {isa = PBXFileReference; path = codex_cli.rs; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -221,6 +230,7 @@
2B938E7363F949A622DD8397 /* ignored.rs */, 2B938E7363F949A622DD8397 /* ignored.rs */,
E19D7FEEBC114189FCF0F84A /* image.rs */, E19D7FEEBC114189FCF0F84A /* image.rs */,
65A73B64D88239A170CF299A /* migration.rs */, 65A73B64D88239A170CF299A /* migration.rs */,
EBFA0B7CAB8661AB3605599A /* mobile_vault.rs */,
B0B4AE2937945F1896E1B205 /* mod_tests.rs */, B0B4AE2937945F1896E1B205 /* mod_tests.rs */,
83CCD3A86C61E070EB9C029B /* mod.rs */, 83CCD3A86C61E070EB9C029B /* mod.rs */,
EFA6483B1269D2EC0DBD4D5A /* modified_dates_tests.rs */, EFA6483B1269D2EC0DBD4D5A /* modified_dates_tests.rs */,
@ -370,8 +380,16 @@
FF3044079CC906291942C508 /* codex_cli.rs */, FF3044079CC906291942C508 /* codex_cli.rs */,
BEE7A0C1F8EE18EDAB31905A /* copilot_cli.rs */, BEE7A0C1F8EE18EDAB31905A /* copilot_cli.rs */,
282A3A834C97BA5453CC9355 /* copilot_discovery.rs */, 282A3A834C97BA5453CC9355 /* copilot_discovery.rs */,
FEE79A2B08D489C84E5C130A /* device_secret_store.rs */,
F2D9E48A1998AD5837EBE8C5 /* guanghu_enterprise.rs */,
B594CFA2AE6E07F7EEBDCA5F /* guanghu_living_system.rs */,
7DE25EB5FAB7CE0ACBD80ABB /* guanghu_router.rs */,
B873D19C73F8DB13E8418612 /* guanghu_shanghai_node.rs */,
4CC25FAC04523FC1407E890B /* guanghu_world_login.rs */,
47E641E8D831982238FCF5D4 /* hermes_cli.rs */, 47E641E8D831982238FCF5D4 /* hermes_cli.rs */,
A0B9AEB056A7B2713B0EF52A /* hermes_discovery.rs */, A0B9AEB056A7B2713B0EF52A /* hermes_discovery.rs */,
F5DC0A0680C3D852550B4558 /* hldp_runtime.rs */,
986A5C5A5D4E1DE0DB1DB3D9 /* hololake_account.rs */,
1CEF6C06C1287DD4AE2F9B51 /* kiro_cli.rs */, 1CEF6C06C1287DD4AE2F9B51 /* kiro_cli.rs */,
F0D5D034E9AE877D8F630233 /* kiro_discovery.rs */, F0D5D034E9AE877D8F630233 /* kiro_discovery.rs */,
8969FA0D7729542C8FE8ED93 /* lib.rs */, 8969FA0D7729542C8FE8ED93 /* lib.rs */,

View file

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.1.7</string> <string>0.4.6</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>20</string> <string>26</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
@ -40,5 +40,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>NSLocalNetworkUsageDescription</key>
<string>HoloLake Era connects to local model servers you configure for Guanghu AI collaboration.</string>
</dict> </dict>
</plist> </plist>

View file

@ -51,8 +51,8 @@ targets:
- UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationLandscapeRight
CFBundleDisplayName: HoloLake CFBundleDisplayName: HoloLake
CFBundleShortVersionString: 0.1.7 CFBundleShortVersionString: 0.4.6
CFBundleVersion: "20" CFBundleVersion: "26"
entitlements: entitlements:
path: hololake_iOS/hololake_iOS.entitlements path: hololake_iOS/hololake_iOS.entitlements
scheme: scheme:

View file

@ -1060,8 +1060,24 @@ fn edit_note_from_tool_args(
let raw_path = required_tool_string(args, EDIT_NOTE_TOOL_NAME, "path")?; let raw_path = required_tool_string(args, EDIT_NOTE_TOOL_NAME, "path")?;
let content = required_tool_string(args, EDIT_NOTE_TOOL_NAME, "content")?; let content = required_tool_string(args, EDIT_NOTE_TOOL_NAME, "content")?;
let path = resolve_existing_vault_note(request, raw_path)?; let path = resolve_existing_vault_note(request, raw_path)?;
let previous = std::fs::read_to_string(&path)
.map_err(|error| format!("Failed to preserve the note before editing: {error}"))?;
crate::vault::save_note_content(path.to_string_lossy().as_ref(), content)?; crate::vault::save_note_content(path.to_string_lossy().as_ref(), content)?;
Ok(format!("已更新笔记:{}", path.display())) let synchronized_commit = match crate::hololake_account::sync_local_markdown_page(
Path::new(active_vault_path(request)?),
&path,
content,
) {
Ok(commit) => commit,
Err(error) => {
let _ = crate::vault::save_note_content(path.to_string_lossy().as_ref(), &previous);
return Err(error);
}
};
Ok(match synchronized_commit {
Some(commit) => format!("已更新笔记:{};同步提交:{commit}", path.display()),
None => format!("已更新笔记:{}", path.display()),
})
} }
fn delete_note_from_tool_args( fn delete_note_from_tool_args(
@ -1070,6 +1086,15 @@ fn delete_note_from_tool_args(
) -> Result<String, String> { ) -> Result<String, String> {
let raw_path = required_tool_string(args, DELETE_NOTE_TOOL_NAME, "path")?; let raw_path = required_tool_string(args, DELETE_NOTE_TOOL_NAME, "path")?;
let path = resolve_existing_vault_note(request, raw_path)?; let path = resolve_existing_vault_note(request, raw_path)?;
if Path::new(active_vault_path(request)?)
.join(".hololake-sync-policy.json")
.is_file()
{
return Err(
"Git-backed HoloLake pages cannot be deleted until a server delete receipt is available."
.to_string(),
);
}
crate::vault::delete_note(path.to_string_lossy().as_ref())?; crate::vault::delete_note(path.to_string_lossy().as_ref())?;
Ok(format!("已删除笔记:{}", path.display())) Ok(format!("已删除笔记:{}", path.display()))
} }
@ -1286,12 +1311,29 @@ fn create_note_from_tool_args(
let vault_path = tool_vault_path(request, args)?; let vault_path = tool_vault_path(request, args)?;
crate::commands::create_note_content( crate::commands::create_note_content(
PathBuf::from(note_path), PathBuf::from(note_path),
content, content.clone(),
Some(PathBuf::from(vault_path)), Some(PathBuf::from(vault_path)),
)?; )?;
let synchronized_commit = match crate::hololake_account::sync_local_markdown_page(
Path::new(vault_path),
Path::new(note_path),
&content,
) {
Ok(commit) => commit,
Err(error) => {
let created_path = if Path::new(note_path).is_absolute() {
PathBuf::from(note_path)
} else {
Path::new(vault_path).join(note_path)
};
let _ = std::fs::remove_file(created_path);
return Err(error);
}
};
let output = serde_json::json!({ let output = serde_json::json!({
"path": note_path, "path": note_path,
"vaultPath": vault_path, "vaultPath": vault_path,
"synchronizedCommit": synchronized_commit,
}) })
.to_string(); .to_string();
Ok(OpenAiToolResult { Ok(OpenAiToolResult {

View file

@ -255,7 +255,7 @@ fn tool_choice_compatibility_error(error: &str) -> bool {
|| normalized.contains("invalid parameter")) || normalized.contains("invalid parameter"))
} }
fn run_openai_agent_loop<F, S>( pub(crate) fn run_openai_agent_loop<F, S>(
request: &AiModelStreamRequest, request: &AiModelStreamRequest,
mut payload: serde_json::Value, mut payload: serde_json::Value,
emit: &mut F, emit: &mut F,
@ -656,19 +656,41 @@ pub fn save_provider_api_key(provider_id: String, api_key: String) -> Result<(),
if api_key.is_empty() { if api_key.is_empty() {
return Err("API key cannot be empty.".into()); return Err("API key cannot be empty.".into());
} }
#[cfg(mobile)]
{
crate::device_secret_store::save(
crate::device_secret_store::MODEL_API_KEY_KIND,
&provider_id,
&api_key,
)?;
return Ok(());
}
#[cfg(desktop)]
{
let path = secrets_path()?; let path = secrets_path()?;
let mut secrets = read_secrets_at(&path)?; let mut secrets = read_secrets_at(&path)?;
secrets.provider_api_keys.insert(provider_id, api_key); secrets.provider_api_keys.insert(provider_id, api_key);
write_secrets_at(&path, &secrets) write_secrets_at(&path, &secrets)
} }
}
pub fn delete_provider_api_key(provider_id: String) -> Result<(), String> { pub fn delete_provider_api_key(provider_id: String) -> Result<(), String> {
let provider_id = normalize_secret_provider_id(&provider_id)?; let provider_id = normalize_secret_provider_id(&provider_id)?;
#[cfg(mobile)]
{
return crate::device_secret_store::delete(
crate::device_secret_store::MODEL_API_KEY_KIND,
&provider_id,
);
}
#[cfg(desktop)]
{
let path = secrets_path()?; let path = secrets_path()?;
let mut secrets = read_secrets_at(&path)?; let mut secrets = read_secrets_at(&path)?;
secrets.provider_api_keys.remove(&provider_id); secrets.provider_api_keys.remove(&provider_id);
write_secrets_at(&path, &secrets) write_secrets_at(&path, &secrets)
} }
}
fn normalize_secret_provider_id(provider_id: &str) -> Result<String, String> { fn normalize_secret_provider_id(provider_id: &str) -> Result<String, String> {
let provider_id = provider_id.trim().to_ascii_lowercase(); let provider_id = provider_id.trim().to_ascii_lowercase();
@ -729,6 +751,16 @@ fn write_secret_file(path: &Path, content: String) -> Result<(), String> {
} }
fn api_key_from_local_file(request: &AiModelStreamRequest) -> Result<Option<String>, String> { fn api_key_from_local_file(request: &AiModelStreamRequest) -> Result<Option<String>, String> {
#[cfg(mobile)]
{
let api_key = crate::device_secret_store::load(
crate::device_secret_store::MODEL_API_KEY_KIND,
&request.provider.id,
)?;
return Ok(Some(api_key));
}
#[cfg(desktop)]
{
let secrets = read_secrets_at(&secrets_path()?)?; let secrets = read_secrets_at(&secrets_path()?)?;
let api_key = secrets let api_key = secrets
.provider_api_keys .provider_api_keys
@ -743,6 +775,7 @@ fn api_key_from_local_file(request: &AiModelStreamRequest) -> Result<Option<Stri
} }
Ok(api_key) Ok(api_key)
} }
}
fn api_key_from_env(request: &AiModelStreamRequest) -> Result<Option<String>, String> { fn api_key_from_env(request: &AiModelStreamRequest) -> Result<Option<String>, String> {
api_key_from_env_with_lookup( api_key_from_env_with_lookup(

View file

@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
use std::sync::OnceLock; use std::sync::OnceLock;
const APP_CONFIG_POLICY_JSON: &str = include_str!("../../mcp-server/app-config-policy.json"); const APP_CONFIG_POLICY_JSON: &str = include_str!("../../mcp-server/app-config-policy.json");
static MOBILE_APP_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct AppConfigPolicy { struct AppConfigPolicy {
@ -48,6 +49,9 @@ fn app_config_dir() -> Result<PathBuf, String> {
} }
fn primary_config_dir() -> Option<PathBuf> { fn primary_config_dir() -> Option<PathBuf> {
if let Some(path) = MOBILE_APP_CONFIG_DIR.get() {
return Some(path.clone());
}
primary_config_dir_from_sources( primary_config_dir_from_sources(
explicit_xdg_config_home(), explicit_xdg_config_home(),
dirs::home_dir(), dirs::home_dir(),
@ -55,6 +59,16 @@ fn primary_config_dir() -> Option<PathBuf> {
) )
} }
#[cfg(mobile)]
pub(crate) fn install_mobile_app_config_dir(path: PathBuf) -> Result<(), String> {
if !path.is_absolute() {
return Err("Mobile app config directory must be absolute".to_string());
}
MOBILE_APP_CONFIG_DIR
.set(path)
.map_err(|_| "Mobile app config directory was already initialized".to_string())
}
fn primary_config_dir_from_sources( fn primary_config_dir_from_sources(
explicit_xdg: Option<PathBuf>, explicit_xdg: Option<PathBuf>,
home: Option<PathBuf>, home: Option<PathBuf>,

View file

@ -339,33 +339,56 @@ pub fn abort_ai_agent_stream(_event_name: String) -> Result<bool, String> {
#[cfg(mobile)] #[cfg(mobile)]
#[tauri::command] #[tauri::command]
pub async fn stream_ai_model( pub async fn stream_ai_model(
_app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
_request: crate::ai_models::AiModelStreamRequest, request: crate::ai_models::AiModelStreamRequest,
) -> Result<String, String> { ) -> Result<String, String> {
Err("Direct AI model chat is not available in this mobile build yet.".into()) use tauri::Emitter;
if crate::hololake_account::is_server_ai_provider(&request.provider) {
return crate::hololake_account::run_mobile_ai_model_stream(app_handle, request).await;
}
let event_name = request
.event_name
.clone()
.filter(|name| {
name.strip_prefix("ai-model-stream-").is_some_and(|suffix| {
!suffix.is_empty()
&& suffix
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '-')
})
})
.unwrap_or_else(|| "ai-model-stream".to_string());
tokio::task::spawn_blocking(move || {
crate::ai_models::run_ai_model_stream(request, |event| {
let _ = app_handle.emit(event_name.as_str(), event);
})
})
.await
.map_err(|error| format!("Mobile AI task failed: {error}"))?
} }
#[cfg(mobile)] #[cfg(mobile)]
#[tauri::command] #[tauri::command]
pub fn save_ai_model_provider_api_key( pub fn save_ai_model_provider_api_key(provider_id: String, api_key: String) -> Result<(), String> {
_provider_id: String, crate::ai_models::save_provider_api_key(provider_id, api_key)
_api_key: String,
) -> Result<(), String> {
Err("Local AI provider secret storage is only available in the desktop app.".into())
} }
#[cfg(mobile)] #[cfg(mobile)]
#[tauri::command] #[tauri::command]
pub fn delete_ai_model_provider_api_key(_provider_id: String) -> Result<(), String> { pub fn delete_ai_model_provider_api_key(provider_id: String) -> Result<(), String> {
Err("Local AI provider secret storage is only available in the desktop app.".into()) crate::ai_models::delete_provider_api_key(provider_id)
} }
#[cfg(mobile)] #[cfg(mobile)]
#[tauri::command] #[tauri::command]
pub fn test_ai_model_provider( pub async fn test_ai_model_provider(
_request: crate::ai_models::AiModelProviderTestRequest, request: crate::ai_models::AiModelProviderTestRequest,
) -> Result<String, String> { ) -> Result<String, String> {
Err("Direct AI model tests are not available in this mobile build yet.".into()) tokio::task::spawn_blocking(move || crate::ai_models::test_ai_model_provider(request))
.await
.map_err(|error| format!("Mobile AI test failed: {error}"))?
} }
#[cfg(test)] #[cfg(test)]

View file

@ -82,6 +82,26 @@ pub fn get_default_vault_path() -> Result<String, String> {
vault::default_vault_path().map(|path| path.to_string_lossy().to_string()) vault::default_vault_path().map(|path| path.to_string_lossy().to_string())
} }
fn mobile_knowledge_root(app_handle: &tauri::AppHandle) -> Result<std::path::PathBuf, String> {
use tauri::Manager;
app_handle
.path()
.app_data_dir()
.map(|path| path.join("knowledge"))
.map_err(|error| format!("Could not resolve the HoloLake knowledge directory: {error}"))
}
#[tauri::command]
pub fn open_mobile_foundation_knowledge(app_handle: tauri::AppHandle) -> Result<String, String> {
vault::open_or_create_mobile_world_vault(&mobile_knowledge_root(&app_handle)?)
}
#[tauri::command]
pub fn open_mobile_personal_knowledge(app_handle: tauri::AppHandle) -> Result<String, String> {
vault::open_or_create_mobile_personal_vault(&mobile_knowledge_root(&app_handle)?)
}
#[tauri::command] #[tauri::command]
pub fn repair_vault(vault_path: String) -> Result<String, String> { pub fn repair_vault(vault_path: String) -> Result<String, String> {
let vault_path = expand_tilde(&vault_path); let vault_path = expand_tilde(&vault_path);
@ -153,4 +173,22 @@ mod tests {
Ok(explicit.to_string_lossy().to_string()) Ok(explicit.to_string_lossy().to_string())
); );
} }
#[test]
fn mobile_knowledge_roots_are_stable_and_separate_local_secrets() {
let temporary = tempfile::TempDir::new().unwrap();
let (world, personal) =
vault::open_or_create_mobile_vaults_in_documents(temporary.path()).unwrap();
assert!(Path::new(&world).join("welcome.md").is_file());
assert!(Path::new(&personal).join("我的湖心.md").is_file());
assert!(Path::new(&personal)
.join(".hololake-sync-policy.json")
.is_file());
assert!(Path::new(&personal).join("本地密钥/README.md").is_file());
let policy =
fs::read_to_string(Path::new(&personal).join(".hololake-sync-policy.json")).unwrap();
assert!(policy.contains("\"本地密钥/**\""));
assert!(!policy.contains("api_key"));
}
} }

View file

@ -0,0 +1,129 @@
use std::borrow::Cow;
const SERVICE: &str = "com.guanghu.hololake.device-secrets";
pub(crate) const ACCOUNT_SESSION_KIND: &str = "account-session";
#[cfg(any(mobile, test))]
pub(crate) const MODEL_API_KEY_KIND: &str = "model-api-key";
fn normalized_segment(value: &str) -> Result<Cow<'_, str>, String> {
let trimmed = value.trim();
if trimmed.is_empty()
|| trimmed.len() > 128
|| !trimmed
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
{
return Err("Device secret identifier is invalid.".to_string());
}
Ok(if trimmed.bytes().any(|byte| byte.is_ascii_uppercase()) {
Cow::Owned(trimmed.to_ascii_lowercase())
} else {
Cow::Borrowed(trimmed)
})
}
fn account_name(kind: &str, identifier: &str) -> Result<String, String> {
Ok(format!(
"{}:{}",
normalized_segment(kind)?,
normalized_segment(identifier)?
))
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn lookup_options(account: &str) -> security_framework::passwords::PasswordOptions {
let mut options =
security_framework::passwords::PasswordOptions::new_generic_password(SERVICE, account);
options.set_access_synchronized(Some(false));
options
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub(crate) fn save(kind: &str, identifier: &str, secret: &str) -> Result<String, String> {
use security_framework::access_control::{ProtectionMode, SecAccessControl};
use security_framework::passwords::set_generic_password_options;
let secret = secret.trim();
if secret.is_empty() {
return Err("Device secret cannot be empty.".to_string());
}
let account = account_name(kind, identifier)?;
let mut options = lookup_options(&account);
let access_control = SecAccessControl::create_with_protection(
Some(ProtectionMode::AccessibleWhenUnlockedThisDeviceOnly),
0,
)
.map_err(|error| {
format!(
"Device Keychain protection failed (OSStatus {}).",
error.code()
)
})?;
options.set_access_control(access_control);
#[cfg(target_os = "ios")]
options.use_protected_keychain();
set_generic_password_options(secret.as_bytes(), options)
.map_err(|error| format!("Device Keychain save failed (OSStatus {}).", error.code()))?;
Ok(account)
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub(crate) fn load(kind: &str, identifier: &str) -> Result<String, String> {
use security_framework::passwords::generic_password;
let account = account_name(kind, identifier)?;
let bytes = generic_password(lookup_options(&account))
.map_err(|error| format!("Device Keychain read failed (OSStatus {}).", error.code()))?;
String::from_utf8(bytes).map_err(|_| "Device Keychain value is not valid UTF-8.".to_string())
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub(crate) fn delete(kind: &str, identifier: &str) -> Result<(), String> {
use security_framework::passwords::delete_generic_password_options;
use security_framework_sys::base::errSecItemNotFound;
let account = account_name(kind, identifier)?;
match delete_generic_password_options(lookup_options(&account)) {
Ok(()) => Ok(()),
Err(error) if error.code() == errSecItemNotFound => Ok(()),
Err(error) => Err(format!(
"Device Keychain delete failed (OSStatus {}).",
error.code()
)),
}
}
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub(crate) fn save(_kind: &str, _identifier: &str, _secret: &str) -> Result<String, String> {
Err("Device-only Keychain storage is unavailable on this platform.".to_string())
}
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub(crate) fn load(_kind: &str, _identifier: &str) -> Result<String, String> {
Err("Device-only Keychain storage is unavailable on this platform.".to_string())
}
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub(crate) fn delete(_kind: &str, _identifier: &str) -> Result<(), String> {
Err("Device-only Keychain storage is unavailable on this platform.".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_secret_handles_are_stable_and_lowercase() {
assert_eq!(
account_name(MODEL_API_KEY_KIND, " OpenAI ").unwrap(),
"model-api-key:openai"
);
}
#[test]
fn device_secret_handles_reject_paths_and_blank_values() {
assert!(account_name(MODEL_API_KEY_KIND, "../secret").is_err());
assert!(account_name(MODEL_API_KEY_KIND, " ").is_err());
}
}

View file

@ -851,6 +851,10 @@ fn load_or_create_identity() -> Result<RouterDeviceIdentity, String> {
load_or_create_identity_at(&path) load_or_create_identity_at(&path)
} }
pub(crate) fn hololake_device_id() -> Result<String, String> {
load_or_create_identity().map(|identity| identity.device_id)
}
fn load_or_create_identity_at(path: &Path) -> Result<RouterDeviceIdentity, String> { fn load_or_create_identity_at(path: &Path) -> Result<RouterDeviceIdentity, String> {
if path.exists() { if path.exists() {
let bytes = fs::read(path) let bytes = fs::read(path)

View file

@ -1,7 +1,9 @@
use serde::Serialize; use serde::Serialize;
const SHANGHAI_NODE_ID: &str = "BS-SH-005"; const SHANGHAI_NODE_ID: &str = "BS-SH-005";
#[cfg(any(target_os = "macos", target_os = "linux", windows))]
const SHANGHAI_NODE_ADDRESS: &str = "124.223.10.33"; const SHANGHAI_NODE_ADDRESS: &str = "124.223.10.33";
#[cfg(any(target_os = "macos", target_os = "linux"))]
const GHOS_LOGIN_PATTERN: &str = "484c44502d47484f532d4c4f47494e21"; const GHOS_LOGIN_PATTERN: &str = "484c44502d47484f532d4c4f47494e21";
#[derive(Debug, Serialize, PartialEq, Eq)] #[derive(Debug, Serialize, PartialEq, Eq)]
@ -76,6 +78,8 @@ fn ping_arguments() -> [&'static str; 4] {
#[tauri::command] #[tauri::command]
pub fn guanghu_shanghai_node_status() -> Result<GuanghuShanghaiNodeStatus, String> { pub fn guanghu_shanghai_node_status() -> Result<GuanghuShanghaiNodeStatus, String> {
#[cfg(any(target_os = "macos", target_os = "linux", windows))]
{
let output = crate::hidden_command("ping") let output = crate::hidden_command("ping")
.args(ping_arguments()) .args(ping_arguments())
.output() .output()
@ -85,6 +89,12 @@ pub fn guanghu_shanghai_node_status() -> Result<GuanghuShanghaiNodeStatus, Strin
Ok(status_from_ping(output.status.success(), &text)) Ok(status_from_ping(output.status.success(), &text))
} }
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
{
Err("guanghu_shanghai_probe_unsupported_platform".to_owned())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{status_from_ping, ttl_from_ping}; use super::{status_from_ping, ttl_from_ping};

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@ pub mod codex_cli;
mod commands; mod commands;
pub mod copilot_cli; pub mod copilot_cli;
mod copilot_discovery; mod copilot_discovery;
mod device_secret_store;
pub mod frontmatter; pub mod frontmatter;
pub mod git; pub mod git;
mod guanghu_enterprise; mod guanghu_enterprise;
@ -25,6 +26,7 @@ mod guanghu_world_login;
pub mod hermes_cli; pub mod hermes_cli;
mod hermes_discovery; mod hermes_discovery;
mod hldp_runtime; mod hldp_runtime;
mod hololake_account;
pub mod kiro_cli; pub mod kiro_cli;
mod kiro_discovery; mod kiro_discovery;
#[cfg(any(test, all(desktop, target_os = "linux")))] #[cfg(any(test, all(desktop, target_os = "linux")))]
@ -414,6 +416,13 @@ fn setup_macos_webview_shortcut_prevention(
} }
fn setup_app(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { fn setup_app(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(mobile)]
{
use tauri::Manager;
let config_dir = app.path().app_config_dir()?;
app_config::install_mobile_app_config_dir(config_dir)?;
}
setup_common_plugins(app)?; setup_common_plugins(app)?;
#[cfg(desktop)] #[cfg(desktop)]
@ -533,6 +542,12 @@ macro_rules! app_invoke_handler {
guanghu_world_login::guanghu_world_login_claim, guanghu_world_login::guanghu_world_login_claim,
guanghu_world_login::guanghu_world_login_status, guanghu_world_login::guanghu_world_login_status,
guanghu_world_login::guanghu_world_logout, guanghu_world_login::guanghu_world_logout,
hololake_account::hololake_account_request_email_code,
hololake_account::hololake_account_verify_email_code,
hololake_account::hololake_account_status,
hololake_account::hololake_account_logout,
hololake_account::hololake_sync_knowledge,
hololake_account::hololake_ai_execute,
commands::get_conflict_files, commands::get_conflict_files,
commands::get_conflict_mode, commands::get_conflict_mode,
commands::git_resolve_conflict, commands::git_resolve_conflict,
@ -590,6 +605,8 @@ macro_rules! app_invoke_handler {
commands::create_getting_started_vault, commands::create_getting_started_vault,
commands::check_vault_exists, commands::check_vault_exists,
commands::get_default_vault_path, commands::get_default_vault_path,
commands::open_mobile_foundation_knowledge,
commands::open_mobile_personal_knowledge,
commands::register_mcp_tools, commands::register_mcp_tools,
commands::remove_mcp_tools, commands::remove_mcp_tools,
commands::check_mcp_status, commands::check_mcp_status,

View file

@ -0,0 +1,364 @@
use std::fs;
use std::path::Path;
const MOBILE_VAULT_MARKER: &str = ".hololake-mobile-vault";
const WORLD_VAULT_DIRECTORY: &str = "HoloLake Foundation World";
const PERSONAL_VAULT_DIRECTORY: &str = "My HoloLake Knowledge";
const WORLD_FILES: [(&str, &str); 6] = [
(
"welcome.md",
r#"---
type: Note
_pinned: true
---
#
HoloLake
- [[]]
- [[]]
- [[]]
线
"#,
),
(
"光湖世界.md",
r#"---
type: Note
---
#
"#,
),
(
"五域结构.md",
r#"---
type: Note
---
#
- 广
-
-
-
-
使
"#,
),
(
"人与人格体.md",
r#"---
type: Note
---
#
1.
2.
3.
4.
"#,
),
(
"AI-PROMPT.md",
r#"---
type: Note
_organized: true
---
#
-
-
-
-
-
"#,
),
(
"AI-MEMORY.md",
r#"---
type: Note
_organized: true
---
# AI
"#,
),
];
const PERSONAL_FILES: [(&str, &str); 7] = [
(
"welcome.md",
r#"---
type: Note
_pinned: true
---
# HoloLake
[[]] [[]]
"#,
),
(
"光湖世界基础.md",
r#"---
type: Note
---
#
"#,
),
(
"我的湖心.md",
r#"---
type: Note
_pinned: true
---
#
-
-
-
-
"#,
),
(
"AI-PROMPT.md",
r#"---
type: Note
_organized: true
---
#
-
-
-
-
- `AI-MEMORY.md`
"#,
),
(
"AI-MEMORY.md",
r#"---
type: Note
_organized: true
---
# AI
"#,
),
(
".hololake-sync-policy.json",
r#"{
"schema": "guanghu.hololake-knowledge-sync-policy/v1",
"include": ["**/*.md", "views/**/*.yml", "attachments/**"],
"exclude": ["本地密钥/**", ".hololake-*", ".git/**"],
"secret_policy": "keychain-handle-only"
}
"#,
),
(
"本地密钥/README.md",
r#"---
type:
local_only: true
sync: never
---
#
iOS Keychain MarkdownGit
"#,
),
];
#[derive(Clone, Copy)]
enum MobileVaultKind {
World,
Personal,
}
impl MobileVaultKind {
fn directory_name(self) -> &'static str {
match self {
Self::World => WORLD_VAULT_DIRECTORY,
Self::Personal => PERSONAL_VAULT_DIRECTORY,
}
}
fn marker_value(self) -> &'static str {
match self {
Self::World => "hololake-mobile-world-v1\n",
Self::Personal => "hololake-mobile-personal-v1\n",
}
}
fn files(self) -> &'static [(&'static str, &'static str)] {
match self {
Self::World => &WORLD_FILES,
Self::Personal => &PERSONAL_FILES,
}
}
}
pub fn open_or_create_mobile_world_vault(root: &Path) -> Result<String, String> {
open_or_create_mobile_vault(root, MobileVaultKind::World)
}
pub fn open_or_create_mobile_personal_vault(root: &Path) -> Result<String, String> {
open_or_create_mobile_vault(root, MobileVaultKind::Personal)
}
fn open_or_create_mobile_vault(root: &Path, kind: MobileVaultKind) -> Result<String, String> {
open_or_create_mobile_vault_in_documents(root, kind)
}
fn open_or_create_mobile_vault_in_documents(
documents_dir: &Path,
kind: MobileVaultKind,
) -> Result<String, String> {
let path = documents_dir.join(kind.directory_name());
open_or_create_mobile_vault_at(&path, kind)
}
#[cfg(test)]
pub(crate) fn open_or_create_mobile_world_vault_at(path: &Path) -> Result<String, String> {
open_or_create_mobile_vault_at(path, MobileVaultKind::World)
}
#[cfg(test)]
pub(crate) fn open_or_create_mobile_personal_vault_at(path: &Path) -> Result<String, String> {
open_or_create_mobile_vault_at(path, MobileVaultKind::Personal)
}
#[cfg(test)]
pub(crate) fn open_or_create_mobile_vaults_in_documents(
documents_dir: &Path,
) -> Result<(String, String), String> {
let world = open_or_create_mobile_vault_in_documents(documents_dir, MobileVaultKind::World)?;
let personal =
open_or_create_mobile_vault_in_documents(documents_dir, MobileVaultKind::Personal)?;
Ok((world, personal))
}
fn open_or_create_mobile_vault_at(path: &Path, kind: MobileVaultKind) -> Result<String, String> {
if path.exists() {
validate_existing_mobile_vault(path, kind)?;
return canonical_path_string(path);
}
create_mobile_vault(path, kind)?;
canonical_path_string(path)
}
fn validate_existing_mobile_vault(path: &Path, kind: MobileVaultKind) -> Result<(), String> {
if !path.is_dir() {
return Err(format!(
"The mobile vault path already exists and is not a folder: {}",
path.display()
));
}
let marker = fs::read_to_string(path.join(MOBILE_VAULT_MARKER)).map_err(|_| {
format!(
"The mobile vault folder already exists but is not managed by HoloLake: {}",
path.display()
)
})?;
if marker != kind.marker_value() {
return Err(format!(
"The mobile vault folder already exists with a different HoloLake vault type: {}",
path.display()
));
}
let required_files_present = kind
.files()
.iter()
.all(|(relative_path, _)| path.join(relative_path).is_file());
if !required_files_present {
return Err(format!(
"The mobile vault folder already exists but its foundation is incomplete: {}",
path.display()
));
}
Ok(())
}
fn create_mobile_vault(path: &Path, kind: MobileVaultKind) -> Result<(), String> {
fs::create_dir_all(path)
.map_err(|error| format!("Failed to create the mobile vault folder: {error}"))?;
let result = seed_mobile_vault(path, kind);
if result.is_err() {
let _ = fs::remove_dir_all(path);
}
result
}
fn seed_mobile_vault(path: &Path, kind: MobileVaultKind) -> Result<(), String> {
for (relative_path, content) in kind.files() {
let output = path.join(relative_path);
if let Some(parent) = output.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("Failed to create {relative_path}: {error}"))?;
}
fs::write(output, content)
.map_err(|error| format!("Failed to write {relative_path}: {error}"))?;
}
fs::write(path.join(MOBILE_VAULT_MARKER), kind.marker_value())
.map_err(|error| format!("Failed to write the mobile vault marker: {error}"))?;
crate::vault::seed_config_files(path.to_string_lossy());
for required in ["AGENTS.md", "type.md", "note.md"] {
if !path.join(required).is_file() {
return Err(format!(
"Failed to seed required mobile vault file: {required}"
));
}
}
Ok(())
}
fn canonical_path_string(path: &Path) -> Result<String, String> {
path.canonicalize()
.map(|resolved| resolved.to_string_lossy().to_string())
.map_err(|error| format!("Failed to resolve mobile vault path: {error}"))
}

View file

@ -9,6 +9,7 @@ mod getting_started;
mod ignored; mod ignored;
mod image; mod image;
mod migration; mod migration;
mod mobile_vault;
mod parsing; mod parsing;
pub(crate) mod path_identity; pub(crate) mod path_identity;
mod rename; mod rename;
@ -36,6 +37,12 @@ pub use getting_started::{create_getting_started_vault, default_vault_path, vaul
pub use ignored::{filter_gitignored_entries, filter_gitignored_folders, filter_gitignored_paths}; pub use ignored::{filter_gitignored_entries, filter_gitignored_folders, filter_gitignored_paths};
pub use image::{copy_image_to_vault, save_image}; pub use image::{copy_image_to_vault, save_image};
pub use migration::migrate_is_a_to_type; pub use migration::migrate_is_a_to_type;
pub use mobile_vault::{open_or_create_mobile_personal_vault, open_or_create_mobile_world_vault};
#[cfg(test)]
pub(crate) use mobile_vault::{
open_or_create_mobile_personal_vault_at, open_or_create_mobile_vaults_in_documents,
open_or_create_mobile_world_vault_at,
};
pub use rename::{ pub use rename::{
auto_rename_untitled, detect_renames, move_note_to_folder, move_note_to_workspace, rename_note, auto_rename_untitled, detect_renames, move_note_to_folder, move_note_to_workspace, rename_note,
rename_note_filename, update_wikilinks_for_renames, AutoRenameUntitledRequest, DetectedRename, rename_note_filename, update_wikilinks_for_renames, AutoRenameUntitledRequest, DetectedRename,

View file

@ -265,7 +265,10 @@ function MainApp({
setToastMessage(`Getting Started vault cloned and opened at ${vaultPath}`) setToastMessage(`Getting Started vault cloned and opened at ${vaultPath}`)
}, [rememberVaultChoice]) }, [rememberVaultChoice])
const handleOnboardingVaultReady = useCallback((vaultPath: string, source: 'template' | 'empty' | 'existing') => { const handleOnboardingVaultReady = useCallback((
vaultPath: string,
source: 'template' | 'empty' | 'existing' | 'world' | 'personal',
) => {
rememberVaultChoice(vaultPath) rememberVaultChoice(vaultPath)
if (source === 'template') { if (source === 'template') {
setToastMessage(`Getting Started vault cloned and opened at ${vaultPath}`) setToastMessage(`Getting Started vault cloned and opened at ${vaultPath}`)

View file

@ -0,0 +1,91 @@
import { useState } from 'react'
import { useHoloLakeAccount } from '../hooks/useHoloLakeAccount'
import type { AppLocale, TranslationKey } from '../lib/i18n'
import { translate } from '../lib/i18n'
import { Button } from './ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from './ui/card'
import { Input } from './ui/input'
export function HoloLakeAccountPanel({ locale }: { locale: AppLocale }) {
const account = useHoloLakeAccount()
const [email, setEmail] = useState('')
const [code, setCode] = useState('')
const t = (key: TranslationKey) => translate(locale, key)
const busy = account.state.phase === 'busy' || account.state.phase === 'checking'
const authenticated = account.state.phase === 'authenticated'
const awaitingCode = account.state.phase === 'code-sent'
return (
<Card className="hololake-account-panel">
<CardHeader>
<CardTitle>{t('hololake.account.title')}</CardTitle>
<CardDescription>{t('hololake.account.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{authenticated ? (
<div className="space-y-2" aria-live="polite">
<p className="text-sm font-medium">{t('hololake.account.signedIn')}</p>
<p className="text-muted-foreground text-sm">{t('hololake.account.serverKeys')}</p>
{account.state.lastCommit
? <p className="text-muted-foreground text-xs">{t('hololake.account.syncComplete')} · <code>{account.state.lastCommit.slice(0, 12)}</code></p>
: null}
</div>
) : awaitingCode ? (
<div className="space-y-2">
<p className="text-muted-foreground text-sm">{t('hololake.account.codeSent')}</p>
<Input
aria-label={t('hololake.account.code')}
autoComplete="one-time-code"
inputMode="numeric"
maxLength={6}
onChange={(event) => setCode(event.target.value.replace(/\D/g, '').slice(0, 6))}
placeholder={t('hololake.account.code')}
value={code}
/>
</div>
) : (
<Input
aria-label={t('hololake.account.email')}
autoCapitalize="none"
autoComplete="email"
inputMode="email"
onChange={(event) => setEmail(event.target.value)}
placeholder={t('hololake.account.email')}
type="email"
value={email}
/>
)}
{account.state.error
? <p className="text-destructive text-sm" role="alert">{t('hololake.account.error')} <code>{account.state.error}</code></p>
: null}
</CardContent>
<CardFooter className="flex flex-wrap gap-2">
{authenticated ? (
<>
<Button disabled={busy} onClick={() => void account.syncKnowledge()}>
{busy ? t('hololake.account.syncing') : t('hololake.account.sync')}
</Button>
<Button disabled={busy} onClick={() => void account.logout()} variant="outline">
{t('hololake.account.signOut')}
</Button>
</>
) : awaitingCode ? (
<Button disabled={busy || code.length !== 6} onClick={() => void account.verifyCode(code)}>
{t('hololake.account.verify')}
</Button>
) : (
<Button disabled={busy || !email.includes('@')} onClick={() => void account.requestCode(email)}>
{busy ? t('hololake.account.sending') : t('hololake.account.sendCode')}
</Button>
)}
</CardFooter>
</Card>
)
}

View file

@ -26,6 +26,7 @@ import { GuanghuWorldMap } from './GuanghuWorldMap'
import { GuanghuWorldLoginGate } from './GuanghuWorldLoginGate' import { GuanghuWorldLoginGate } from './GuanghuWorldLoginGate'
import { FifthDomainSystems } from './FifthDomainSystems' import { FifthDomainSystems } from './FifthDomainSystems'
import { EternalLakeHeartPage } from './EternalLakeHeartPage' import { EternalLakeHeartPage } from './EternalLakeHeartPage'
import { HoloLakeAccountPanel } from './HoloLakeAccountPanel'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -407,6 +408,7 @@ export function HoloLakeHome({
}}></Button> }}></Button>
</article> </article>
<GuanghuRouterConsole locale={locale} router={router} /> <GuanghuRouterConsole locale={locale} router={router} />
<HoloLakeAccountPanel locale={locale} />
</div> </div>
</section> </section>
) )
@ -416,6 +418,7 @@ export function HoloLakeHome({
<section className="channel-stage"> <section className="channel-stage">
<h1>{t('hololake.channel.heartbeatTitle')}</h1> <h1>{t('hololake.channel.heartbeatTitle')}</h1>
<p className="channel-stage__lead">{t('hololake.channel.heartbeatDescription')}</p> <p className="channel-stage__lead">{t('hololake.channel.heartbeatDescription')}</p>
<HoloLakeAccountPanel locale={locale} />
<div className="channel-grid channel-grid--three"> <div className="channel-grid channel-grid--three">
<RouteCard eyebrow="PERSONA · AI AGENT" title="人格体工作舱" description="与当前频道的人格体持续协作;保留多 Agent、模型选择、权限模式、工具调用、执行回执与历史会话。" action="打开人格体工作舱" onOpen={onOpenAiWorkspace ? openAiWorkspace : undefined} /> <RouteCard eyebrow="PERSONA · AI AGENT" title="人格体工作舱" description="与当前频道的人格体持续协作;保留多 Agent、模型选择、权限模式、工具调用、执行回执与历史会话。" action="打开人格体工作舱" onOpen={onOpenAiWorkspace ? openAiWorkspace : undefined} />
<RouteCard eyebrow="MODULE · KNOWLEDGE" title={t('hololake.home.moduleKnowledgeTitle')} description={t('hololake.home.moduleKnowledgeDescription')} action={t('hololake.channel.openKnowledge')} onOpen={openKnowledgeBase} /> <RouteCard eyebrow="MODULE · KNOWLEDGE" title={t('hololake.home.moduleKnowledgeTitle')} description={t('hololake.home.moduleKnowledgeDescription')} action={t('hololake.channel.openKnowledge')} onOpen={openKnowledgeBase} />

View file

@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button'
import guanghuGalaxyLogo from '@/assets/guanghu-logo-light.png' import guanghuGalaxyLogo from '@/assets/guanghu-logo-light.png'
import { TOLARIA_FIRST_LAUNCH_DOCS_URL } from '@/constants/feedback' import { TOLARIA_FIRST_LAUNCH_DOCS_URL } from '@/constants/feedback'
import { translate, type AppLocale } from '@/lib/i18n' import { translate, type AppLocale } from '@/lib/i18n'
import { isMobilePlatform } from '@/utils/platform'
import { openExternalUrl } from '@/utils/url' import { openExternalUrl } from '@/utils/url'
interface WelcomeScreenProps { interface WelcomeScreenProps {
@ -317,6 +318,7 @@ function useWelcomeActionButtons({
mode, mode,
busy, busy,
isOffline, isOffline,
mobile,
onCreateEmptyVault, onCreateEmptyVault,
onOpenFolder, onOpenFolder,
onCreateVault, onCreateVault,
@ -325,21 +327,24 @@ function useWelcomeActionButtons({
'mode' | 'isOffline' | 'onCreateEmptyVault' | 'onOpenFolder' | 'onCreateVault' 'mode' | 'isOffline' | 'onCreateEmptyVault' | 'onOpenFolder' | 'onCreateVault'
> & { > & {
busy: boolean busy: boolean
mobile: boolean
}) { }) {
const templateActionRef = useRef<HTMLButtonElement>(null) const templateActionRef = useRef<HTMLButtonElement>(null)
const createEmptyActionRef = useRef<HTMLButtonElement>(null) const createEmptyActionRef = useRef<HTMLButtonElement>(null)
const openFolderActionRef = useRef<HTMLButtonElement>(null) const openFolderActionRef = useRef<HTMLButtonElement>(null)
const actionButtonRefs = useMemo( const actionButtonRefs = useMemo(
() => [templateActionRef, createEmptyActionRef, openFolderActionRef], () => mobile
[], ? [templateActionRef, createEmptyActionRef]
: [templateActionRef, createEmptyActionRef, openFolderActionRef],
[mobile],
) )
const actions = useMemo<WelcomeAction[]>( const actions = useMemo<WelcomeAction[]>(
() => ([ () => ([
{ disabled: isOffline, run: onCreateVault }, { disabled: !mobile && isOffline, run: onCreateVault },
{ disabled: false, run: onCreateEmptyVault }, { disabled: false, run: onCreateEmptyVault },
{ disabled: false, run: onOpenFolder }, ...(!mobile ? [{ disabled: false, run: onOpenFolder }] : []),
]), ]),
[isOffline, onCreateEmptyVault, onCreateVault, onOpenFolder], [isOffline, mobile, onCreateEmptyVault, onCreateVault, onOpenFolder],
) )
useEffect(() => { useEffect(() => {
@ -416,6 +421,7 @@ function WelcomeActions({
createEmptyActionRef, createEmptyActionRef,
creatingAction, creatingAction,
isOffline, isOffline,
mobile,
locale, locale,
onCreateEmptyVault, onCreateEmptyVault,
onCreateVault, onCreateVault,
@ -430,6 +436,7 @@ function WelcomeActions({
busy: boolean busy: boolean
createEmptyActionRef: WelcomeActionButtonRef createEmptyActionRef: WelcomeActionButtonRef
locale: AppLocale locale: AppLocale
mobile: boolean
openFolderActionRef: WelcomeActionButtonRef openFolderActionRef: WelcomeActionButtonRef
presentation: WelcomeScreenPresentation presentation: WelcomeScreenPresentation
templateActionRef: WelcomeActionButtonRef templateActionRef: WelcomeActionButtonRef
@ -439,12 +446,20 @@ function WelcomeActions({
<OptionButton <OptionButton
icon={<Rocket size={18} style={{ color: 'var(--accent-purple)' }} />} icon={<Rocket size={18} style={{ color: 'var(--accent-purple)' }} />}
iconBg="var(--accent-purple-light)" iconBg="var(--accent-purple-light)"
label={translate(locale, 'onboarding.welcome.templateTitle')} label={translate(locale, mobile
description={presentation.templateDescription} ? 'onboarding.welcome.mobileWorldTitle'
loadingLabel={translate(locale, 'onboarding.welcome.templateLoading')} : 'onboarding.welcome.templateTitle')}
loadingDescription={translate(locale, 'onboarding.welcome.templateLoadingDescription')} description={mobile
? translate(locale, 'onboarding.welcome.mobileWorldDescription')
: presentation.templateDescription}
loadingLabel={mobile
? undefined
: translate(locale, 'onboarding.welcome.templateLoading')}
loadingDescription={mobile
? undefined
: translate(locale, 'onboarding.welcome.templateLoadingDescription')}
onClick={onCreateVault} onClick={onCreateVault}
disabled={busy || isOffline} disabled={busy || (!mobile && isOffline)}
loading={creatingAction === 'template'} loading={creatingAction === 'template'}
testId="welcome-create-vault" testId="welcome-create-vault"
autoFocus autoFocus
@ -454,10 +469,18 @@ function WelcomeActions({
<OptionButton <OptionButton
icon={<Plus size={18} style={{ color: 'var(--accent-blue)' }} />} icon={<Plus size={18} style={{ color: 'var(--accent-blue)' }} />}
iconBg="var(--accent-blue-light)" iconBg="var(--accent-blue-light)"
label={translate(locale, 'onboarding.welcome.createEmpty')} label={translate(locale, mobile
description={translate(locale, 'onboarding.welcome.createEmptyDescription')} ? 'onboarding.welcome.mobilePersonalTitle'
loadingLabel={translate(locale, 'onboarding.welcome.createEmptyLoading')} : 'onboarding.welcome.createEmpty')}
loadingDescription={translate(locale, 'onboarding.welcome.createEmptyLoadingDescription')} description={translate(locale, mobile
? 'onboarding.welcome.mobilePersonalDescription'
: 'onboarding.welcome.createEmptyDescription')}
loadingLabel={mobile
? undefined
: translate(locale, 'onboarding.welcome.createEmptyLoading')}
loadingDescription={mobile
? undefined
: translate(locale, 'onboarding.welcome.createEmptyLoadingDescription')}
onClick={onCreateEmptyVault} onClick={onCreateEmptyVault}
disabled={busy} disabled={busy}
loading={creatingAction === 'empty'} loading={creatingAction === 'empty'}
@ -465,6 +488,7 @@ function WelcomeActions({
buttonRef={createEmptyActionRef} buttonRef={createEmptyActionRef}
/> />
{!mobile && (
<OptionButton <OptionButton
icon={<FolderOpen size={18} style={{ color: 'var(--accent-green)' }} />} icon={<FolderOpen size={18} style={{ color: 'var(--accent-green)' }} />}
iconBg="var(--accent-green-light)" iconBg="var(--accent-green-light)"
@ -475,6 +499,7 @@ function WelcomeActions({
testId="welcome-open-folder" testId="welcome-open-folder"
buttonRef={openFolderActionRef} buttonRef={openFolderActionRef}
/> />
)}
</div> </div>
) )
} }
@ -556,11 +581,13 @@ export function WelcomeScreen({
canRetryTemplate, canRetryTemplate,
}: WelcomeScreenProps) { }: WelcomeScreenProps) {
const busy = creatingAction !== null const busy = creatingAction !== null
const mobile = isMobilePlatform()
const presentation = getWelcomeScreenPresentation(mode, defaultVaultPath, isOffline, locale) const presentation = getWelcomeScreenPresentation(mode, defaultVaultPath, isOffline, locale)
const { templateActionRef, createEmptyActionRef, openFolderActionRef } = useWelcomeActionButtons({ const { templateActionRef, createEmptyActionRef, openFolderActionRef } = useWelcomeActionButtons({
mode, mode,
busy, busy,
isOffline, isOffline,
mobile,
onCreateEmptyVault, onCreateEmptyVault,
onOpenFolder, onOpenFolder,
onCreateVault, onCreateVault,
@ -581,6 +608,7 @@ export function WelcomeScreen({
creatingAction={creatingAction} creatingAction={creatingAction}
isOffline={isOffline} isOffline={isOffline}
locale={locale} locale={locale}
mobile={mobile}
onCreateEmptyVault={onCreateEmptyVault} onCreateEmptyVault={onCreateEmptyVault}
onCreateVault={onCreateVault} onCreateVault={onCreateVault}
onOpenFolder={onOpenFolder} onOpenFolder={onOpenFolder}

View file

@ -0,0 +1,125 @@
import { act, renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useHoloLakeAccount } from './useHoloLakeAccount'
const { invokeMock, isTauriMock, trackEventMock } = vi.hoisted(() => ({
invokeMock: vi.fn(),
isTauriMock: vi.fn(),
trackEventMock: vi.fn(),
}))
vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock }))
vi.mock('../mock-tauri', () => ({ isTauri: isTauriMock }))
vi.mock('../lib/telemetry', () => ({ trackEvent: trackEventMock }))
describe('useHoloLakeAccount', () => {
beforeEach(() => {
vi.clearAllMocks()
isTauriMock.mockReturnValue(true)
invokeMock.mockImplementation((command: string) => {
if (command === 'hololake_account_status') {
return Promise.resolve({ state: 'signed_out' })
}
return Promise.resolve({})
})
})
it('requests and verifies an email code without exposing a session token', async () => {
invokeMock.mockImplementation((command: string) => {
if (command === 'hololake_account_status') return Promise.resolve({ state: 'signed_out' })
if (command === 'hololake_account_request_email_code') {
return Promise.resolve({ request_id: 'request-001' })
}
if (command === 'hololake_account_verify_email_code') {
return Promise.resolve({ state: 'authenticated', expires_at: 1_800_086_400 })
}
return Promise.resolve({})
})
const { result } = renderHook(() => useHoloLakeAccount())
await waitFor(() => expect(result.current.state.phase).toBe('signed-out'))
await act(() => result.current.requestCode('owner@example.invalid'))
expect(result.current.state.phase).toBe('code-sent')
expect(invokeMock).toHaveBeenCalledWith(
'hololake_account_request_email_code',
{ email: 'owner@example.invalid' },
)
await act(() => result.current.verifyCode('123456'))
expect(result.current.state.phase).toBe('authenticated')
expect(result.current.state).not.toHaveProperty('sessionToken')
expect(trackEventMock).toHaveBeenCalledWith(
'hololake_account_login_completed',
{ result: 'authenticated' },
)
})
it('records only the knowledge commit after a successful sync', async () => {
invokeMock.mockImplementation((command: string) => {
if (command === 'hololake_account_status') {
return Promise.resolve({ state: 'authenticated', expires_at: 1_800_086_400 })
}
if (command === 'hololake_sync_knowledge') {
return Promise.resolve({ commit: 'a'.repeat(40) })
}
return Promise.resolve({})
})
const { result } = renderHook(() => useHoloLakeAccount())
await waitFor(() => expect(result.current.state.phase).toBe('authenticated'))
await act(() => result.current.syncKnowledge())
expect(result.current.state.lastCommit).toBe('a'.repeat(40))
expect(trackEventMock).toHaveBeenCalledWith(
'hololake_knowledge_sync_completed',
{ result: 'success' },
)
})
it('keeps the current interaction available after recoverable failures', async () => {
invokeMock.mockImplementation((command: string) => {
if (command === 'hololake_account_status') return Promise.resolve({ state: 'signed_out' })
if (command === 'hololake_account_request_email_code') {
return Promise.resolve({ request_id: 'request-001' })
}
if (command === 'hololake_account_verify_email_code') {
return Promise.reject(new Error('hololake_login_verification_failed'))
}
return Promise.resolve({})
})
const { result } = renderHook(() => useHoloLakeAccount())
await waitFor(() => expect(result.current.state.phase).toBe('signed-out'))
await act(() => result.current.requestCode('owner@example.invalid'))
await act(() => result.current.verifyCode('000000'))
expect(result.current.state.phase).toBe('code-sent')
expect(result.current.state.requestId).toBe('request-001')
expect(result.current.state.error).toBe('hololake_login_verification_failed')
})
it('keeps an authenticated session usable when knowledge sync fails', async () => {
invokeMock.mockImplementation((command: string) => {
if (command === 'hololake_account_status') {
return Promise.resolve({ state: 'authenticated', expires_at: 1_800_086_400 })
}
if (command === 'hololake_sync_knowledge') {
return Promise.reject(new Error('hololake_knowledge_archive_failed'))
}
return Promise.resolve({})
})
const { result } = renderHook(() => useHoloLakeAccount())
await waitFor(() => expect(result.current.state.phase).toBe('authenticated'))
await act(() => result.current.syncKnowledge())
expect(result.current.state.phase).toBe('authenticated')
expect(result.current.state.error).toBe('hololake_knowledge_archive_failed')
})
it('stays signed out in the browser preview without calling native commands', async () => {
isTauriMock.mockReturnValue(false)
const { result } = renderHook(() => useHoloLakeAccount())
await waitFor(() => expect(result.current.state.phase).toBe('signed-out'))
expect(invokeMock).not.toHaveBeenCalled()
})
})

View file

@ -0,0 +1,140 @@
import { invoke } from '@tauri-apps/api/core'
import { useCallback, useEffect, useState } from 'react'
import { trackEvent } from '../lib/telemetry'
import { isTauri } from '../mock-tauri'
type AccountPhase = 'checking' | 'signed-out' | 'code-sent' | 'authenticated' | 'busy' | 'error'
type AccountState = {
error: string | null
expiresAt: number | null
lastCommit: string | null
phase: AccountPhase
requestId: string | null
}
type NativeAccountStatus = {
expires_at?: number
state?: 'authenticated' | 'signed_out'
}
type NativeCodeRequest = {
request_id?: string
}
type NativeKnowledgeReceipt = {
commit?: string
}
const initialState: AccountState = {
error: null,
expiresAt: null,
lastCommit: null,
phase: 'checking',
requestId: null,
}
function errorText(error: unknown): string {
if (error instanceof Error) return error.message
return String(error)
}
export function useHoloLakeAccount() {
const [state, setState] = useState<AccountState>(initialState)
const refresh = useCallback(async () => {
if (!isTauri()) {
setState((current) => ({ ...current, phase: 'signed-out' }))
return
}
try {
const status = await invoke<NativeAccountStatus>('hololake_account_status')
setState((current) => ({
...current,
error: null,
expiresAt: status.expires_at ?? null,
phase: status.state === 'authenticated' ? 'authenticated' : 'signed-out',
}))
} catch (error) {
setState((current) => ({ ...current, error: errorText(error), phase: 'signed-out' }))
}
}, [])
useEffect(() => {
void refresh()
}, [refresh])
const requestCode = useCallback(async (email: string) => {
setState((current) => ({ ...current, error: null, phase: 'busy' }))
trackEvent('hololake_account_code_requested', { surface: 'account-panel' })
try {
const result = await invoke<NativeCodeRequest>('hololake_account_request_email_code', { email })
if (!result.request_id) throw new Error('hololake_login_request_id_missing')
setState((current) => ({
...current,
phase: 'code-sent',
requestId: result.request_id ?? null,
}))
} catch (error) {
setState((current) => ({ ...current, error: errorText(error), phase: 'error' }))
}
}, [])
const verifyCode = useCallback(async (code: string) => {
if (!state.requestId) return
setState((current) => ({ ...current, error: null, phase: 'busy' }))
try {
const result = await invoke<NativeAccountStatus>('hololake_account_verify_email_code', {
requestId: state.requestId,
code,
})
trackEvent('hololake_account_login_completed', { result: 'authenticated' })
setState((current) => ({
...current,
expiresAt: result.expires_at ?? null,
phase: 'authenticated',
requestId: null,
}))
} catch (error) {
trackEvent('hololake_account_login_completed', { result: 'error' })
setState((current) => ({ ...current, error: errorText(error), phase: 'code-sent' }))
}
}, [state.requestId])
const syncKnowledge = useCallback(async () => {
setState((current) => ({ ...current, error: null, phase: 'busy' }))
trackEvent('hololake_knowledge_sync_requested', { repository: 'hololake-knowledge-base' })
try {
const result = await invoke<NativeKnowledgeReceipt>('hololake_sync_knowledge')
trackEvent('hololake_knowledge_sync_completed', { result: 'success' })
setState((current) => ({
...current,
lastCommit: result.commit ?? null,
phase: 'authenticated',
}))
} catch (error) {
trackEvent('hololake_knowledge_sync_completed', { result: 'error' })
setState((current) => ({ ...current, error: errorText(error), phase: 'authenticated' }))
}
}, [])
const logout = useCallback(async () => {
setState((current) => ({ ...current, error: null, phase: 'busy' }))
try {
await invoke('hololake_account_logout')
trackEvent('hololake_account_logout_completed')
setState(initialState)
await refresh()
} catch (error) {
setState((current) => ({ ...current, error: errorText(error), phase: 'authenticated' }))
}
}, [refresh])
return {
logout,
requestCode,
state,
syncKnowledge,
verifyCode,
}
}

View file

@ -8,6 +8,8 @@ import {
labelFromPath, labelFromPath,
} from '../utils/gettingStartedVault' } from '../utils/gettingStartedVault'
import { formatFolderPickerActionError, pickFolder } from '../utils/vault-dialog' import { formatFolderPickerActionError, pickFolder } from '../utils/vault-dialog'
import { isMobilePlatform } from '../utils/platform'
import { trackEvent } from '../lib/telemetry'
type OnboardingState = type OnboardingState =
| { status: 'loading' } | { status: 'loading' }
@ -16,7 +18,7 @@ type OnboardingState =
| { status: 'ready'; vaultPath: string } | { status: 'ready'; vaultPath: string }
type CreatingAction = 'template' | 'empty' | null type CreatingAction = 'template' | 'empty' | null
type ReadyVaultSource = 'template' | 'empty' | 'existing' type ReadyVaultSource = 'template' | 'empty' | 'existing' | 'world' | 'personal'
type OnVaultReady = (vaultPath: string, source: ReadyVaultSource) => void type OnVaultReady = (vaultPath: string, source: ReadyVaultSource) => void
type RegisterVault = ( type RegisterVault = (
vaultPath: string, vaultPath: string,
@ -51,6 +53,11 @@ interface CreateEmptyVaultHandlerOptions extends ReadyVaultHandlerOptions {
setCreatingAction: SetCreatingAction setCreatingAction: SetCreatingAction
} }
interface MobileKnowledgeHandlerOptions extends CreateEmptyVaultHandlerOptions {
command: 'open_mobile_foundation_knowledge' | 'open_mobile_personal_knowledge'
source: Extract<ReadyVaultSource, 'world' | 'personal'>
}
function tauriCall<T>(command: string, args: Record<string, unknown>): Promise<T> { function tauriCall<T>(command: string, args: Record<string, unknown>): Promise<T> {
return isTauri() ? invoke<T>(command, args) : mockInvoke<T>(command, args) return isTauri() ? invoke<T>(command, args) : mockInvoke<T>(command, args)
} }
@ -263,6 +270,35 @@ function useOpenFolderHandler(
}, [options]) }, [options])
} }
function useMobileKnowledgeHandler(options: MobileKnowledgeHandlerOptions) {
return useCallback(async () => {
options.setCreatingAction(options.source === 'world' ? 'template' : 'empty')
options.setError(null)
try {
if (options.source === 'personal') {
const foundationPath = await tauriCall<string>('open_mobile_foundation_knowledge', {})
await registerVaultSelection(options.registerVault, foundationPath, { verifyAvailability: false })
}
const knowledgePath = await tauriCall<string>(options.command, {})
await registerVaultSelection(options.registerVault, knowledgePath, { verifyAvailability: false })
markVaultReady(options.setState, knowledgePath)
options.onVaultReady?.(knowledgePath, options.source)
trackEvent('mobile_native_knowledge_opened', { knowledge_kind: options.source })
} catch (err) {
options.setError(formatOnboardingRegistrationError({
action: options.source === 'world'
? 'Could not open the HoloLake foundation world'
: 'Could not create your local HoloLake knowledge',
err,
}))
trackEvent('mobile_native_knowledge_open_failed', { knowledge_kind: options.source })
} finally {
options.setCreatingAction(null)
}
}, [options])
}
export function useOnboarding( export function useOnboarding(
initialVaultPath: string, initialVaultPath: string,
options: OnboardingOptions = {}, options: OnboardingOptions = {},
@ -321,14 +357,14 @@ export function useOnboarding(
onVaultReady: options.onVaultReady, onVaultReady: options.onVaultReady,
}) })
const handleCreateVault = useCreateVaultHandler(createTemplateVault, setError) const handleCreateDesktopVault = useCreateVaultHandler(createTemplateVault, setError)
const retryCreateVault = useCallback(async () => { const retryCreateVault = useCallback(async () => {
if (!lastTemplatePath) return if (!lastTemplatePath) return
await createTemplateVault(lastTemplatePath) await createTemplateVault(lastTemplatePath)
}, [createTemplateVault, lastTemplatePath]) }, [createTemplateVault, lastTemplatePath])
const handleCreateEmptyVault = useCreateEmptyVaultHandler({ const handleCreateDesktopEmptyVault = useCreateEmptyVaultHandler({
onVaultReady: options.onVaultReady, onVaultReady: options.onVaultReady,
registerVault: options.registerVault, registerVault: options.registerVault,
setCreatingAction, setCreatingAction,
@ -343,6 +379,42 @@ export function useOnboarding(
setState, setState,
}) })
const openMobileWorldKnowledge = useMobileKnowledgeHandler({
command: 'open_mobile_foundation_knowledge',
onVaultReady: options.onVaultReady,
registerVault: options.registerVault,
setCreatingAction,
setError,
setState,
source: 'world',
})
const openMobilePersonalKnowledge = useMobileKnowledgeHandler({
command: 'open_mobile_personal_knowledge',
onVaultReady: options.onVaultReady,
registerVault: options.registerVault,
setCreatingAction,
setError,
setState,
source: 'personal',
})
const handleCreateVault = useCallback(async () => {
if (isMobilePlatform()) {
await openMobileWorldKnowledge()
return
}
await handleCreateDesktopVault()
}, [handleCreateDesktopVault, openMobileWorldKnowledge])
const handleCreateEmptyVault = useCallback(async () => {
if (isMobilePlatform()) {
await openMobilePersonalKnowledge()
return
}
await handleCreateDesktopEmptyVault()
}, [handleCreateDesktopEmptyVault, openMobilePersonalKnowledge])
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {
markDismissed() markDismissed()
setState({ status: 'ready', vaultPath: initialVaultPath }) setState({ status: 'ready', vaultPath: initialVaultPath })

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -92,11 +92,30 @@
"onboarding.welcome.createEmptyDescription": "Start fresh in an empty folder with HoloLake Era defaults", "onboarding.welcome.createEmptyDescription": "Start fresh in an empty folder with HoloLake Era defaults",
"onboarding.welcome.createEmptyLoading": "Creating vault...", "onboarding.welcome.createEmptyLoading": "Creating vault...",
"onboarding.welcome.createEmptyLoadingDescription": "Preparing HoloLake Era defaults in the selected folder", "onboarding.welcome.createEmptyLoadingDescription": "Preparing HoloLake Era defaults in the selected folder",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device",
"onboarding.welcome.openExisting": "Open existing vault", "onboarding.welcome.openExisting": "Open existing vault",
"onboarding.welcome.openExistingDescription": "Point to a folder you already have", "onboarding.welcome.openExistingDescription": "Point to a folder you already have",
"onboarding.welcome.retryDownload": "Retry download", "onboarding.welcome.retryDownload": "Retry download",
"onboarding.welcome.docsPrompt": "New to HoloLake Era?", "onboarding.welcome.docsPrompt": "New to HoloLake Era?",
"onboarding.welcome.docsLink": "Read the first-launch guide", "onboarding.welcome.docsLink": "Read the first-launch guide",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.ai.checkingTitle": "Checking AI setup", "onboarding.ai.checkingTitle": "Checking AI setup",
"onboarding.ai.checkingDescription": "Looking for local AI agents on this machine.", "onboarding.ai.checkingDescription": "Looking for local AI agents on this machine.",
"onboarding.ai.missingTitle": "AI setup is optional", "onboarding.ai.missingTitle": "AI setup is optional",

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -92,11 +92,30 @@
"onboarding.welcome.createEmptyDescription": "使用 HoloLake Era 默认设置,在空文件夹中重新开始", "onboarding.welcome.createEmptyDescription": "使用 HoloLake Era 默认设置,在空文件夹中重新开始",
"onboarding.welcome.createEmptyLoading": "正在创建 Vault...", "onboarding.welcome.createEmptyLoading": "正在创建 Vault...",
"onboarding.welcome.createEmptyLoadingDescription": "正在所选文件夹中准备 HoloLake Era 默认设置", "onboarding.welcome.createEmptyLoadingDescription": "正在所选文件夹中准备 HoloLake Era 默认设置",
"onboarding.welcome.mobileWorldTitle": "进入光湖基础世界",
"onboarding.welcome.mobileWorldDescription": "打开内置的共同语言世界基础",
"onboarding.welcome.mobilePersonalTitle": "建立我的本地知识湖",
"onboarding.welcome.mobilePersonalDescription": "在当前手机内建立私人知识湖",
"onboarding.welcome.openExisting": "打开现有 Vault", "onboarding.welcome.openExisting": "打开现有 Vault",
"onboarding.welcome.openExistingDescription": "指向您已有的文件夹", "onboarding.welcome.openExistingDescription": "指向您已有的文件夹",
"onboarding.welcome.retryDownload": "重试下载", "onboarding.welcome.retryDownload": "重试下载",
"onboarding.welcome.docsPrompt": "HoloLake Era 新手?", "onboarding.welcome.docsPrompt": "HoloLake Era 新手?",
"onboarding.welcome.docsLink": "阅读首次启动指南", "onboarding.welcome.docsLink": "阅读首次启动指南",
"hololake.account.title": "光湖账号",
"hololake.account.description": "使用邮箱验证码登录并同步共同知识世界。手机 API 密钥只保存在当前设备钥匙串。",
"hololake.account.email": "邮箱",
"hololake.account.code": "验证码",
"hololake.account.sendCode": "发送验证码",
"hololake.account.sending": "发送中...",
"hololake.account.codeSent": "请输入邮件中的六位验证码。",
"hololake.account.verify": "验证并登录",
"hololake.account.signedIn": "当前设备已登录。",
"hololake.account.serverKeys": "服务器模型凭据可直接使用但不会显示;手机密钥永不同步。",
"hololake.account.sync": "同步知识库",
"hololake.account.syncing": "同步中...",
"hololake.account.syncComplete": "已同步仓库提交",
"hololake.account.signOut": "退出登录",
"hololake.account.error": "账号操作失败:",
"onboarding.ai.checkingTitle": "正在检查 AI 设置", "onboarding.ai.checkingTitle": "正在检查 AI 设置",
"onboarding.ai.checkingDescription": "正在本机上查找本地 AI 代理。", "onboarding.ai.checkingDescription": "正在本机上查找本地 AI 代理。",
"onboarding.ai.missingTitle": "AI 设置为可选项", "onboarding.ai.missingTitle": "AI 设置为可选项",

View file

@ -1121,5 +1121,24 @@
"hololake.login.errorTitle": "The world entrance did not open", "hololake.login.errorTitle": "The world entrance did not open",
"hololake.login.retry": "Try again", "hololake.login.retry": "Try again",
"hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.", "hololake.login.privacy": "HoloLake never receives or stores your mailbox address. The Fifth Domain controller resolves the bound Guanghu number.",
"hololake.login.nodeReady": "Fifth Domain world entrance ready" "hololake.login.nodeReady": "Fifth Domain world entrance ready",
"hololake.account.title": "HoloLake account",
"hololake.account.description": "Sign in by email to synchronize the shared knowledge world. Device API keys remain in this device's Keychain.",
"hololake.account.email": "Email",
"hololake.account.code": "Verification code",
"hololake.account.sendCode": "Send code",
"hololake.account.sending": "Sending...",
"hololake.account.codeSent": "Enter the six-digit code from your email.",
"hololake.account.verify": "Verify and sign in",
"hololake.account.signedIn": "This device is signed in.",
"hololake.account.serverKeys": "Server model credentials are usable without being shown. Device keys never sync.",
"hololake.account.sync": "Sync knowledge",
"hololake.account.syncing": "Syncing...",
"hololake.account.syncComplete": "Synced repository commit",
"hololake.account.signOut": "Sign out",
"hololake.account.error": "Account operation failed:",
"onboarding.welcome.mobileWorldTitle": "Enter the HoloLake foundation world",
"onboarding.welcome.mobileWorldDescription": "Open the built-in shared language-world foundation",
"onboarding.welcome.mobilePersonalTitle": "Create my local knowledge lake",
"onboarding.welcome.mobilePersonalDescription": "Create a private knowledge lake inside this device"
} }

View file

@ -21,6 +21,10 @@ export function isWindows(): boolean {
return getUserAgent().includes('Windows') return getUserAgent().includes('Windows')
} }
export function isMobilePlatform(): boolean {
return /Android|iPad|iPhone|iPod/u.test(getUserAgent())
}
export function shouldUseCustomWindowChrome(): boolean { export function shouldUseCustomWindowChrome(): boolean {
return isTauri() && (isLinux() || isWindows()) return isTauri() && (isLinux() || isWindows())
} }

View file

@ -983,6 +983,14 @@ export default defineConfig({
port: 5202, port: 5202,
strictPort: true, strictPort: true,
allowedHosts: true, allowedHosts: true,
fs: process.env.HOLOLAKE_TEST_DEPENDENCY_ROOT
? {
allow: [
__dirname,
path.resolve(process.env.HOLOLAKE_TEST_DEPENDENCY_ROOT),
],
}
: undefined,
watch: { watch: {
ignored: devServerWatchIgnored, ignored: devServerWatchIgnored,
}, },