feat(auth): 添加异步密码哈希和验证函数
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor(relay): 复用HTTP客户端和请求体序列化结果

feat(kernel): 添加获取单个审批记录的方法

fix(store): 改进SaaS连接错误分类和降级处理

docs: 更新审计文档和系统架构文档

refactor(prompt): 优化SQL查询参数化绑定

refactor(migration): 使用静态SQL和COALESCE更新配置项

feat(commands): 添加审批执行状态追踪和事件通知

chore: 更新启动脚本以支持Admin后台

fix(auth-guard): 优化授权状态管理和错误处理

refactor(db): 使用异步密码哈希函数

refactor(totp): 使用异步密码验证函数

style: 清理无用文件和注释

docs: 更新功能全景和审计文档

refactor(service): 优化HTTP客户端重用和请求处理

fix(connection): 改进SaaS不可用时的降级处理

refactor(handlers): 使用异步密码验证函数

chore: 更新依赖和工具链配置
This commit is contained in:
iven
2026-03-29 21:45:29 +08:00
parent b7ec317d2c
commit 7de294375b
34 changed files with 2041 additions and 894 deletions

View File

@@ -633,7 +633,8 @@ export function AuditLogsPanel() {
setVerificationResult(null);
try {
// Call ZCLAW API to verify the chain
// Call ZCLAW API to verify the chain (only available on GatewayClient)
if (!('verifyAuditLogChain' in client)) throw new Error('KernelClient does not support chain verification');
const result = await client.verifyAuditLogChain(log.id);
const verification: MerkleVerificationResult = {

View File

@@ -86,6 +86,7 @@ function ZclawLogo({ className }: { className?: string }) {
/** 根据运行环境自动选择 SaaS 服务器地址 */
function getSaasUrl(): string {
if (import.meta.env.DEV) return DEV_SAAS_URL;
return isTauriRuntime() ? PRODUCTION_SAAS_URL : DEV_SAAS_URL;
}

View File

@@ -139,12 +139,12 @@ export function SaaSSettings() {
<CloudFeatureRow
name="团队协作"
description="与团队成员共享 Agent 和技能"
status={account?.role === 'admin' || account?.role === 'pro' ? 'active' : 'inactive'}
status={account?.role === 'admin' || account?.role === 'super_admin' ? 'active' : 'inactive'}
/>
<CloudFeatureRow
name="高级分析"
description="使用统计和用量分析仪表板"
status={account?.role === 'admin' || account?.role === 'pro' ? 'active' : 'inactive'}
status={account?.role === 'admin' || account?.role === 'super_admin' ? 'active' : 'inactive'}
/>
</div>
</div>

View File

@@ -23,8 +23,8 @@ export interface SaaSAccountInfo {
username: string;
email: string;
display_name: string;
role: string;
status: string;
role: 'super_admin' | 'admin' | 'user';
status: 'active' | 'disabled' | 'suspended';
totp_enabled: boolean;
created_at: string;
}
@@ -64,6 +64,7 @@ export interface SaaSErrorResponse {
/** Login response from POST /api/v1/auth/login */
export interface SaaSLoginResponse {
token: string;
refresh_token: string;
account: SaaSAccountInfo;
}
@@ -322,8 +323,8 @@ export interface AccountPublic {
username: string;
email: string;
display_name: string;
role: string;
status: string;
role: 'super_admin' | 'admin' | 'user';
status: 'active' | 'disabled' | 'suspended';
totp_enabled: boolean;
last_login_at: string | null;
created_at: string;

View File

@@ -352,8 +352,11 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
// === SaaS Relay Mode ===
// Check connection mode from localStorage (set by saasStore).
// This takes priority over Tauri/Gateway when the user has selected SaaS mode.
// When SaaS is unreachable, gracefully degrade to local kernel mode
// so the desktop app remains functional.
const savedMode = localStorage.getItem('zclaw-connection-mode');
let saasDegraded = false;
if (savedMode === 'saas') {
const { loadSaaSSession, saasClient } = await import('../lib/saas-client');
const session = loadSaaSSession();
@@ -379,13 +382,26 @@ export const useConnectionStore = create<ConnectionStore>((set, get) => {
useSaaSStore.getState().logout();
throw new Error('SaaS 会话已过期,请重新登录');
}
// SaaS unreachable — degrade to local kernel mode
const errMsg = err instanceof Error ? err.message : String(err);
throw new Error(`SaaS 平台连接失败: ${errMsg}`);
log.warn(`SaaS 平台连接失败: ${errMsg} — 降级到本地 Kernel 模式`);
// Mark SaaS as unreachable in store
try {
const { useSaaSStore } = await import('./saasStore');
useSaaSStore.setState({ saasReachable: false });
} catch { /* non-critical */ }
saasDegraded = true;
}
set({ connectionState: 'connected', gatewayVersion: 'saas-relay' });
log.debug('Connected to SaaS relay');
return;
if (!saasDegraded) {
set({ connectionState: 'connected', gatewayVersion: 'saas-relay' });
log.debug('Connected to SaaS relay');
return;
}
// Fall through to Tauri Kernel / Gateway mode
}
// === Internal Kernel Mode (Tauri) ===

View File

@@ -97,7 +97,9 @@ export type SaaSStore = SaaSStateSlice & SaaSActionsSlice;
// === Constants ===
const DEFAULT_SAAS_URL = 'https://saas.zclaw.com';
const DEFAULT_SAAS_URL = import.meta.env.DEV
? 'http://127.0.0.1:8080'
: 'https://saas.zclaw.com';
// === Helpers ===
@@ -218,14 +220,21 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
? err.message
: String(err);
const isNetworkError = message.includes('Failed to fetch')
const isTimeout = message.includes('signal timed out')
|| message.includes('Timeout')
|| message.includes('timed out')
|| message.includes('AbortError');
const isConnectionRefused = message.includes('Failed to fetch')
|| message.includes('NetworkError')
|| message.includes('ECONNREFUSED')
|| message.includes('timeout');
|| message.includes('connection refused');
const userMessage = isNetworkError
? `无法连接 SaaS 服务器: ${requestUrl}`
: message;
const userMessage = isTimeout
? `连接 SaaS 服务器超时,请确认后端服务正在运行: ${requestUrl}`
: isConnectionRefused
? `无法连接到 SaaS 服务器,请确认后端服务已启动: ${requestUrl}`
: message;
set({ isLoading: false, error: userMessage });
throw new Error(userMessage);
@@ -491,7 +500,6 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
const existing = localStorage.getItem(storageKey);
// Diff check: skip if local was modified since last pull
const lastPullKey = `zclaw-config-pull-ts.${config.category}.${config.key}`;
const dirtyKey = `zclaw-config-dirty.${config.category}.${config.key}`;
const lastPulledValue = localStorage.getItem(`zclaw-config-pulled.${config.category}.${config.key}`);