feat(desktop): isolate private data by account
This commit is contained in:
parent
3e317745b5
commit
1d2a8a2f63
12 changed files with 143 additions and 60 deletions
|
|
@ -15,7 +15,9 @@
|
||||||
},
|
},
|
||||||
"registry": {
|
"registry": {
|
||||||
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
|
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
|
||||||
"location": "TAURI_APP_DATA_CODE_CHANNEL_V1",
|
"location": "TAURI_APP_DATA_ACCOUNTS_V1_HASHED_ACCOUNT_CODE_CHANNEL_V1",
|
||||||
|
"authenticated_account_required": true,
|
||||||
|
"cross_account_projection_allowed": false,
|
||||||
"stored_fields_include_credentials": false,
|
"stored_fields_include_credentials": false,
|
||||||
"atomic_write": true,
|
"atomic_write": true,
|
||||||
"restart_readback": true
|
"restart_readback": true
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
"state": "LOCAL_INSTALLED_RUNTIME_ACCEPTED_PUBLIC_RELEASE_PENDING",
|
"state": "LOCAL_INSTALLED_RUNTIME_ACCEPTED_PUBLIC_RELEASE_PENDING",
|
||||||
"native_storage": {
|
"native_storage": {
|
||||||
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
|
"owner": "HOLOLAKE_NATIVE_RUST_CORE",
|
||||||
"location": "TAURI_APP_DATA_KNOWLEDGE_V1",
|
"location": "TAURI_APP_DATA_ACCOUNTS_V1_HASHED_ACCOUNT_KNOWLEDGE_V1",
|
||||||
|
"authenticated_account_required": true,
|
||||||
|
"cross_account_projection_allowed": false,
|
||||||
"engine": "LOCAL_GIT_WITH_DOCUMENT_TREE",
|
"engine": "LOCAL_GIT_WITH_DOCUMENT_TREE",
|
||||||
"webview_direct_filesystem_access": false,
|
"webview_direct_filesystem_access": false,
|
||||||
"automatic_server_upload": false
|
"automatic_server_upload": false
|
||||||
|
|
@ -39,7 +41,7 @@
|
||||||
},
|
},
|
||||||
"legacy_compatibility": {
|
"legacy_compatibility": {
|
||||||
"source": "HOLOLAKE_ERA_0_8_KNOWLEDGE_DATA",
|
"source": "HOLOLAKE_ERA_0_8_KNOWLEDGE_DATA",
|
||||||
"mode": "READ_ONLY_SEPARATE_ROOT",
|
"mode": "NOT_AUTO_PROJECTED_EXPLICIT_OWNER_MIGRATION_ONLY",
|
||||||
"in_place_migration": false,
|
"in_place_migration": false,
|
||||||
"source_modification_allowed": false,
|
"source_modification_allowed": false,
|
||||||
"tolaria_surface_used": false
|
"tolaria_surface_used": false
|
||||||
|
|
@ -51,7 +53,7 @@
|
||||||
"folder_import_search_and_restart_readback": true,
|
"folder_import_search_and_restart_readback": true,
|
||||||
"public_signed_runtime_acceptance": false,
|
"public_signed_runtime_acceptance": false,
|
||||||
"legacy_data_migrated": false,
|
"legacy_data_migrated": false,
|
||||||
"legacy_data_available_read_only": true,
|
"legacy_data_available_read_only": false,
|
||||||
"deduplication_runtime_tested": true,
|
"deduplication_runtime_tested": true,
|
||||||
"idempotent_import_runtime_tested": true,
|
"idempotent_import_runtime_tested": true,
|
||||||
"native_edit_runtime_tested": true,
|
"native_edit_runtime_tested": true,
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,31 @@ const codeContract = JSON.parse(read('contracts/code-channel.json'))
|
||||||
const provenance = JSON.parse(read('audit/knowledge-workspace-migration-provenance.json'))
|
const provenance = JSON.parse(read('audit/knowledge-workspace-migration-provenance.json'))
|
||||||
const knowledgeRust = read('src-tauri/src/knowledge_base.rs')
|
const knowledgeRust = read('src-tauri/src/knowledge_base.rs')
|
||||||
const codeRust = read('src-tauri/src/code_channel.rs')
|
const codeRust = read('src-tauri/src/code_channel.rs')
|
||||||
|
const authenticatedStorageRust = read('src-tauri/src/authenticated_storage.rs')
|
||||||
const lib = read('src-tauri/src/lib.rs')
|
const lib = read('src-tauri/src/lib.rs')
|
||||||
const ui = read('src/main.tsx')
|
const ui = read('src/main.tsx')
|
||||||
|
|
||||||
test('knowledge workspace is HoloLake-owned and keeps the legacy source read-only', () => {
|
test('knowledge workspace is HoloLake-owned, account-scoped, and never auto-projects legacy data', () => {
|
||||||
assert.equal(knowledgeContract.native_storage.owner, 'HOLOLAKE_NATIVE_RUST_CORE')
|
assert.equal(knowledgeContract.native_storage.owner, 'HOLOLAKE_NATIVE_RUST_CORE')
|
||||||
assert.equal(knowledgeContract.legacy_compatibility.mode, 'READ_ONLY_SEPARATE_ROOT')
|
assert.equal(knowledgeContract.native_storage.authenticated_account_required, true)
|
||||||
|
assert.equal(knowledgeContract.native_storage.cross_account_projection_allowed, false)
|
||||||
|
assert.equal(knowledgeContract.legacy_compatibility.mode, 'NOT_AUTO_PROJECTED_EXPLICIT_OWNER_MIGRATION_ONLY')
|
||||||
assert.equal(knowledgeContract.legacy_compatibility.in_place_migration, false)
|
assert.equal(knowledgeContract.legacy_compatibility.in_place_migration, false)
|
||||||
assert.equal(knowledgeContract.legacy_compatibility.tolaria_surface_used, false)
|
assert.equal(knowledgeContract.legacy_compatibility.tolaria_surface_used, false)
|
||||||
assert.equal(provenance.legacy_data_boundary.modified, false)
|
assert.equal(provenance.legacy_data_boundary.modified, false)
|
||||||
assert.match(knowledgeRust, /join\("knowledge-v1"\)/)
|
assert.match(knowledgeRust, /account_storage_root\(app, "knowledge-v1"\)/)
|
||||||
|
assert.match(authenticatedStorageRust, /HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED/)
|
||||||
|
assert.match(authenticatedStorageRust, /accounts-v1/)
|
||||||
|
assert.doesNotMatch(authenticatedStorageRust, /join\(&session\.username\)/)
|
||||||
assert.match(knowledgeRust, /source_docs_root/)
|
assert.match(knowledgeRust, /source_docs_root/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('code channel uses the same authenticated account boundary', () => {
|
||||||
|
assert.equal(codeContract.registry.authenticated_account_required, true)
|
||||||
|
assert.equal(codeContract.registry.cross_account_projection_allowed, false)
|
||||||
|
assert.match(codeRust, /account_storage_root\(app, "code-channel-v1"\)/)
|
||||||
|
})
|
||||||
|
|
||||||
test('folder import is bounded, idempotent, conflict-safe and creates a local Git receipt', () => {
|
test('folder import is bounded, idempotent, conflict-safe and creates a local Git receipt', () => {
|
||||||
assert.equal(knowledgeContract.folder_import.maximum_files, 1000)
|
assert.equal(knowledgeContract.folder_import.maximum_files, 1000)
|
||||||
assert.equal(knowledgeContract.folder_import.symlinks_followed, false)
|
assert.equal(knowledgeContract.folder_import.symlinks_followed, false)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
//! 登录后的账号隔离存储根。
|
||||||
|
//!
|
||||||
|
//! 私人频道、知识库和代码频道不得在编号与仓库账号验证完成前读取,
|
||||||
|
//! 也不得让两个仓库账号共享同一份本机数据目录。
|
||||||
|
|
||||||
|
use ring::digest::{digest, SHA256};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
use crate::code_repo_login::{self, LoginSession};
|
||||||
|
|
||||||
|
pub(crate) fn account_storage_root(app: &AppHandle, namespace: &str) -> Result<PathBuf, String> {
|
||||||
|
let session = code_repo_login::current_login_session(app)?
|
||||||
|
.ok_or_else(|| "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED".to_string())?;
|
||||||
|
let account_id = account_scope_id(&session)?;
|
||||||
|
let root = app
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
|
||||||
|
.join("accounts-v1")
|
||||||
|
.join(account_id)
|
||||||
|
.join(validate_namespace(namespace)?);
|
||||||
|
fs::create_dir_all(&root)
|
||||||
|
.map_err(|error| format!("HOLOLAKE_ACCOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
||||||
|
.map_err(|error| format!("HOLOLAKE_ACCOUNT_STORAGE_PERMISSION_FAILED: {error}"))?;
|
||||||
|
}
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_namespace(namespace: &str) -> Result<&str, String> {
|
||||||
|
if !namespace.is_empty()
|
||||||
|
&& namespace.len() <= 48
|
||||||
|
&& namespace
|
||||||
|
.chars()
|
||||||
|
.all(|item| item.is_ascii_lowercase() || item.is_ascii_digit() || item == '-')
|
||||||
|
{
|
||||||
|
Ok(namespace)
|
||||||
|
} else {
|
||||||
|
Err("HOLOLAKE_ACCOUNT_STORAGE_NAMESPACE_INVALID".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn account_scope_id(session: &LoginSession) -> Result<String, String> {
|
||||||
|
if session.username.is_empty() || session.host.is_empty() || session.domain.is_empty() {
|
||||||
|
return Err("HOLOLAKE_LOGIN_SESSION_SCOPE_INVALID".into());
|
||||||
|
}
|
||||||
|
let material = format!("{}\0{}\0{}", session.domain, session.host, session.username);
|
||||||
|
let value = digest(&SHA256, material.as_bytes());
|
||||||
|
Ok(format!("account-{}", hex(value.as_ref())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn session(username: &str) -> LoginSession {
|
||||||
|
LoginSession {
|
||||||
|
username: username.into(),
|
||||||
|
host: "guanghulab.com".into(),
|
||||||
|
domain: "FIFTH_DOMAIN".into(),
|
||||||
|
signed_in_at_unix_ms: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_accounts_never_share_a_storage_scope() {
|
||||||
|
assert_ne!(
|
||||||
|
account_scope_id(&session("bingshuo")).unwrap(),
|
||||||
|
account_scope_id(&session("another-human")).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn account_scope_is_stable_and_does_not_expose_the_username() {
|
||||||
|
let first = account_scope_id(&session("bingshuo")).unwrap();
|
||||||
|
assert_eq!(first, account_scope_id(&session("bingshuo")).unwrap());
|
||||||
|
assert!(!first.contains("bingshuo"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_dialog::DialogExt;
|
use tauri_plugin_dialog::DialogExt;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -175,11 +175,7 @@ pub async fn read_code_channel_file(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn code_channel_root(app: &AppHandle) -> Result<PathBuf, String> {
|
fn code_channel_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
let root = app
|
let root = crate::authenticated_storage::account_storage_root(app, "code-channel-v1")?;
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
|
|
||||||
.join("code-channel-v1");
|
|
||||||
ensure_root(&root)?;
|
ensure_root(&root)?;
|
||||||
Ok(root)
|
Ok(root)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::fs::{self, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::AppHandle;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const SESSION_SCHEMA: &str = "hololake.direct-local-session/v1";
|
const SESSION_SCHEMA: &str = "hololake.direct-local-session/v1";
|
||||||
|
|
@ -192,11 +192,7 @@ pub async fn append_direct_local_session_event(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn direct_session_root(app: &AppHandle) -> Result<PathBuf, String> {
|
pub(crate) fn direct_session_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
let app_data = app
|
let root = crate::authenticated_storage::account_storage_root(app, "direct-local-session-v1")?;
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?;
|
|
||||||
let root = app_data.join("direct-local-session-v1");
|
|
||||||
fs::create_dir_all(&root)
|
fs::create_dir_all(&root)
|
||||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
|
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||||
root.canonicalize()
|
root.canonicalize()
|
||||||
|
|
|
||||||
|
|
@ -452,22 +452,10 @@ pub async fn select_and_import_knowledge_folder(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn knowledge_roots(app: &AppHandle) -> Result<(PathBuf, Option<PathBuf>), String> {
|
fn knowledge_roots(app: &AppHandle) -> Result<(PathBuf, Option<PathBuf>), String> {
|
||||||
let native = app
|
let native = crate::authenticated_storage::account_storage_root(app, "knowledge-v1")?;
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
|
|
||||||
.join("knowledge-v1");
|
|
||||||
ensure_native_knowledge_root(&native)?;
|
ensure_native_knowledge_root(&native)?;
|
||||||
let legacy = dirs::home_dir()
|
// 旧全局知识库不得自动投影给任一新登录账号;后续只允许用户显式迁移。
|
||||||
.map(|home| {
|
Ok((native, None))
|
||||||
home.join("Library")
|
|
||||||
.join("Application Support")
|
|
||||||
.join("hololake-desktop")
|
|
||||||
.join("data")
|
|
||||||
.join("knowledge-base")
|
|
||||||
})
|
|
||||||
.filter(|path| path.join("docs").is_dir());
|
|
||||||
Ok((native, legacy))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写私有文件的跨平台选项:unix 上只许本人读写,Windows 上不做额外权限设置。
|
/// 写私有文件的跨平台选项:unix 上只许本人读写,Windows 上不做额外权限设置。
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mod circular_lake_membrane;
|
mod circular_lake_membrane;
|
||||||
|
mod authenticated_storage;
|
||||||
mod code_channel;
|
mod code_channel;
|
||||||
mod code_repo_login;
|
mod code_repo_login;
|
||||||
mod direct_local_broker;
|
mod direct_local_broker;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::AppHandle;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1";
|
const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1";
|
||||||
|
|
@ -148,11 +148,7 @@ pub async fn transition_personal_channel_task(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn personal_channel_database(app: &AppHandle) -> Result<PathBuf, String> {
|
fn personal_channel_database(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
let app_data = app
|
let root = crate::authenticated_storage::account_storage_root(app, "personal-channel-v1")?;
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?;
|
|
||||||
let root = app_data.join("personal-channel-v1");
|
|
||||||
create_private_directory(&root)?;
|
create_private_directory(&root)?;
|
||||||
Ok(root.join("personal-channel.sqlite3"))
|
Ok(root.join("personal-channel.sqlite3"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use std::fs::{self, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::AppHandle;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const JOURNAL_SCHEMA: &str = "hololake.pncc-stage-one-receipt-projection-journal/v1";
|
const JOURNAL_SCHEMA: &str = "hololake.pncc-stage-one-receipt-projection-journal/v1";
|
||||||
|
|
@ -90,12 +90,7 @@ pub async fn query_pncc_receipt_projection(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result<PathBuf, String> {
|
pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
let root = app
|
let root = crate::authenticated_storage::account_storage_root(app, "pncc-receipt-projection-v1")?;
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.map_err(|error| format!("PNCC_APP_DATA_UNAVAILABLE: {error}"))?
|
|
||||||
.join("pncc-stage-one-v1")
|
|
||||||
.join("receipt-projection");
|
|
||||||
fs::create_dir_all(&root)
|
fs::create_dir_all(&root)
|
||||||
.map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))?;
|
.map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||||
root.canonicalize()
|
root.canonicalize()
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::process::{Command, Output};
|
use std::process::{Command, Output};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{AppHandle, Manager};
|
use tauri::AppHandle;
|
||||||
use tauri_plugin_dialog::DialogExt;
|
use tauri_plugin_dialog::DialogExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -153,12 +153,7 @@ pub async fn confirm_pncc_repository_mount(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, String> {
|
pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
let root = app
|
let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-mounts-v1")?;
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.map_err(|error| format!("PNCC_APP_DATA_UNAVAILABLE: {error}"))?
|
|
||||||
.join("pncc-stage-one-v1")
|
|
||||||
.join("repository-mounts");
|
|
||||||
fs::create_dir_all(&root)
|
fs::create_dir_all(&root)
|
||||||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||||
root.canonicalize()
|
root.canonicalize()
|
||||||
|
|
@ -166,12 +161,7 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, Str
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pncc_repository_candidate_root(app: &AppHandle) -> Result<PathBuf, String> {
|
fn pncc_repository_candidate_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
let root = app
|
let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-candidates-v1")?;
|
||||||
.path()
|
|
||||||
.app_data_dir()
|
|
||||||
.map_err(|error| format!("PNCC_APP_DATA_UNAVAILABLE: {error}"))?
|
|
||||||
.join("pncc-stage-one-v1")
|
|
||||||
.join("repository-candidates");
|
|
||||||
fs::create_dir_all(&root)
|
fs::create_dir_all(&root)
|
||||||
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_STORAGE_UNAVAILABLE: {error}"))?;
|
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||||
root.canonicalize()
|
root.canonicalize()
|
||||||
|
|
|
||||||
|
|
@ -718,6 +718,15 @@ function HoloLakeApp() {
|
||||||
// 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。
|
// 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。
|
||||||
setLoginRising(true)
|
setLoginRising(true)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1300))
|
await new Promise((resolve) => setTimeout(resolve, 1300))
|
||||||
|
// 账号切换时先清空上一账号的内存投影,绝不让旧页面在新会话首帧闪现。
|
||||||
|
setPersonal(previewPersonal)
|
||||||
|
setKnowledge(previewKnowledge)
|
||||||
|
setCodeChannels(previewCode)
|
||||||
|
setActiveDocument(null)
|
||||||
|
setActiveChannel(null)
|
||||||
|
setCodeTree(null)
|
||||||
|
setActiveCodeFile(null)
|
||||||
|
setReceipts([])
|
||||||
setRepoLogin({ username: receipt.username, host: receipt.host, domain: receipt.domain, signedInAtUnixMs: Date.now() })
|
setRepoLogin({ username: receipt.username, host: receipt.host, domain: receipt.domain, signedInAtUnixMs: Date.now() })
|
||||||
setLoginRising(false)
|
setLoginRising(false)
|
||||||
setLoginPassword('')
|
setLoginPassword('')
|
||||||
|
|
@ -726,6 +735,14 @@ function HoloLakeApp() {
|
||||||
}
|
}
|
||||||
const signOutRepo = async () => {
|
const signOutRepo = async () => {
|
||||||
try { await invoke('sign_out_code_repo_login') } catch { /* 登出以本机清场为准 */ }
|
try { await invoke('sign_out_code_repo_login') } catch { /* 登出以本机清场为准 */ }
|
||||||
|
setPersonal(previewPersonal)
|
||||||
|
setKnowledge(previewKnowledge)
|
||||||
|
setCodeChannels(previewCode)
|
||||||
|
setActiveDocument(null)
|
||||||
|
setActiveChannel(null)
|
||||||
|
setCodeTree(null)
|
||||||
|
setActiveCodeFile(null)
|
||||||
|
setReceipts([])
|
||||||
setRepoLogin(null)
|
setRepoLogin(null)
|
||||||
}
|
}
|
||||||
const initializeIdentity = async (event: React.FormEvent) => {
|
const initializeIdentity = async (event: React.FormEvent) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue