fix(desktop): DeerFlow UI — ChatArea refactor + ai-elements + dead CSS cleanup

ChatArea retry button uses setInput instead of direct sendToGateway,
fix bootstrap spinner stuck for non-logged-in users,
remove dead CSS (aurora-title/sidebar-open/quick-action-chips),
add ai components (ReasoningBlock/StreamingText/ChatMode/ModelSelector/TaskProgress),
add ClassroomPlayer + ResizableChatLayout + artifact panel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-02 19:24:44 +08:00
parent d40c4605b2
commit 28299807b6
70 changed files with 4938 additions and 618 deletions

View File

@@ -3,6 +3,10 @@
*
* 为 ZCLAW 前端操作提供统一的审计日志记录功能。
* 记录关键操作Hand 触发、Agent 创建等)到本地存储。
*
* @reserved This module is reserved for future audit logging integration.
* It is not currently imported by any component. When audit logging is needed,
* import { logAudit, logAuditSuccess, logAuditFailure } from this module.
*/
import { createLogger } from './logger';

View File

@@ -0,0 +1,142 @@
/**
* Classroom Adapter
*
* Bridges the old ClassroomData type (ClassroomPreviewer) with the new
* Classroom type (ClassroomPlayer + Tauri backend).
*/
import type { Classroom, GeneratedScene } from '../types/classroom';
import { SceneType, TeachingStyle, DifficultyLevel } from '../types/classroom';
import type { ClassroomData, ClassroomScene } from '../components/ClassroomPreviewer';
// ---------------------------------------------------------------------------
// Old → New (ClassroomData → Classroom)
// ---------------------------------------------------------------------------
/**
* Convert a legacy ClassroomData to the new Classroom format.
* Used when opening ClassroomPlayer from Pipeline result previews.
*/
export function adaptToClassroom(data: ClassroomData): Classroom {
const scenes: GeneratedScene[] = data.scenes.map((scene, index) => ({
id: scene.id,
outlineId: `outline-${index}`,
content: {
title: scene.title,
sceneType: mapSceneType(scene.type),
content: {
heading: scene.content.heading ?? scene.title,
key_points: scene.content.bullets ?? [],
description: scene.content.explanation,
quiz: scene.content.quiz ?? undefined,
},
actions: [],
durationSeconds: scene.duration ?? 60,
notes: scene.narration,
},
order: index,
})) as GeneratedScene[];
return {
id: data.id,
title: data.title,
description: data.subject,
topic: data.subject,
style: TeachingStyle.Lecture,
level: mapDifficulty(data.difficulty),
totalDuration: data.duration * 60,
objectives: [],
scenes,
agents: [],
metadata: {
generatedAt: new Date(data.createdAt).getTime(),
version: '1.0',
custom: {},
},
};
}
// ---------------------------------------------------------------------------
// New → Old (Classroom → ClassroomData)
// ---------------------------------------------------------------------------
/**
* Convert a new Classroom to the legacy ClassroomData format.
* Used when rendering ClassroomPreviewer from new pipeline results.
*/
export function adaptToClassroomData(classroom: Classroom): ClassroomData {
const scenes: ClassroomScene[] = classroom.scenes.map((scene) => {
const data = scene.content.content as Record<string, unknown>;
return {
id: scene.id,
title: scene.content.title,
type: mapToLegacySceneType(scene.content.sceneType),
content: {
heading: (data?.heading as string) ?? scene.content.title,
bullets: (data?.key_points as string[]) ?? [],
explanation: (data?.description as string) ?? '',
quiz: (data?.quiz as ClassroomScene['content']['quiz']) ?? undefined,
},
narration: scene.content.notes,
duration: scene.content.durationSeconds,
};
});
return {
id: classroom.id,
title: classroom.title,
subject: classroom.topic,
difficulty: mapToLegacyDifficulty(classroom.level),
duration: Math.ceil(classroom.totalDuration / 60),
scenes,
outline: {
sections: classroom.scenes.map((scene) => ({
title: scene.content.title,
scenes: [scene.id],
})),
},
createdAt: new Date(classroom.metadata.generatedAt).toISOString(),
};
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function mapSceneType(type: ClassroomScene['type']): SceneType {
switch (type) {
case 'title': return SceneType.Slide;
case 'content': return SceneType.Slide;
case 'quiz': return SceneType.Quiz;
case 'interactive': return SceneType.Interactive;
case 'summary': return SceneType.Text;
default: return SceneType.Slide;
}
}
function mapToLegacySceneType(sceneType: string): ClassroomScene['type'] {
switch (sceneType) {
case 'quiz': return 'quiz';
case 'interactive': return 'interactive';
case 'text': return 'summary';
default: return 'content';
}
}
function mapDifficulty(difficulty: string): DifficultyLevel {
switch (difficulty) {
case '初级': return DifficultyLevel.Beginner;
case '中级': return DifficultyLevel.Intermediate;
case '高级': return DifficultyLevel.Advanced;
default: return DifficultyLevel.Intermediate;
}
}
function mapToLegacyDifficulty(level: string): ClassroomData['difficulty'] {
switch (level) {
case 'beginner': return '初级';
case 'advanced': return '高级';
case 'expert': return '高级';
default: return '中级';
}
}

View File

@@ -56,12 +56,19 @@ function initErrorStore(): void {
errors: [],
addError: (error: AppError) => {
// Dedup: skip if same title+message already exists and undismissed
const isDuplicate = errorStore.errors.some(
(e) => !e.dismissed && e.title === error.title && e.message === error.message
);
if (isDuplicate) return;
const storedError: StoredError = {
...error,
dismissed: false,
reported: false,
};
errorStore.errors = [storedError, ...errorStore.errors];
// Cap at 50 errors to prevent unbounded growth
errorStore.errors = [storedError, ...errorStore.errors].slice(0, 50);
// Notify listeners
notifyErrorListeners(error);
},

View File

@@ -103,6 +103,12 @@ export function installChatMethods(ClientClass: { prototype: KernelClient }): vo
callbacks.onDelta(streamEvent.delta);
break;
case 'thinkingDelta':
if (callbacks.onThinkingDelta) {
callbacks.onThinkingDelta(streamEvent.delta);
}
break;
case 'tool_start':
log.debug('Tool started:', streamEvent.name, streamEvent.input);
if (callbacks.onTool) {

View File

@@ -5,8 +5,20 @@
*/
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import { createLogger } from './logger';
import type { KernelClient } from './kernel-client';
const log = createLogger('KernelHands');
/** Payload emitted by the Rust backend on `hand-execution-complete` events. */
export interface HandExecutionCompletePayload {
approvalId: string;
handId: string;
success: boolean;
error?: string | null;
}
export function installHandMethods(ClientClass: { prototype: KernelClient }): void {
const proto = ClientClass.prototype as any;
@@ -92,7 +104,7 @@ export function installHandMethods(ClientClass: { prototype: KernelClient }): vo
*/
proto.getHandStatus = async function (this: KernelClient, name: string, runId: string): Promise<{ status: string; result?: unknown }> {
try {
return await invoke('hand_run_status', { handName: name, runId });
return await invoke('hand_run_status', { runId });
} catch (e) {
const { createLogger } = await import('./logger');
createLogger('KernelHands').debug('hand_run_status failed', { name, runId, error: e });
@@ -171,4 +183,26 @@ export function installHandMethods(ClientClass: { prototype: KernelClient }): vo
proto.respondToApproval = async function (this: KernelClient, approvalId: string, approved: boolean, reason?: string): Promise<void> {
return invoke('approval_respond', { id: approvalId, approved, reason });
};
// ─── Event Listeners ───
/**
* Listen for `hand-execution-complete` events emitted by the Rust backend
* after a hand finishes executing (both from direct trigger and approval flow).
*
* Returns an unlisten function for cleanup.
*/
proto.onHandExecutionComplete = async function (
this: KernelClient,
callback: (payload: HandExecutionCompletePayload) => void,
): Promise<UnlistenFn> {
const unlisten = await listen<HandExecutionCompletePayload>(
'hand-execution-complete',
(event) => {
log.debug('hand-execution-complete', event.payload);
callback(event.payload);
},
);
return unlisten;
};
}

View File

@@ -109,7 +109,11 @@ export function installSkillMethods(ClientClass: { prototype: KernelClient }): v
}> {
return invoke('skill_execute', {
id,
context: {},
context: {
agentId: '',
sessionId: '',
workingDir: '',
},
input: input || {},
});
};

View File

@@ -96,7 +96,12 @@ export function installTriggerMethods(ClientClass: { prototype: KernelClient }):
triggerType?: TriggerTypeSpec;
}): Promise<TriggerItem> {
try {
return await invoke<TriggerItem>('trigger_update', { id, updates });
return await invoke<TriggerItem>('trigger_update', {
id,
name: updates.name,
enabled: updates.enabled,
handId: updates.handId,
});
} catch (error) {
this.log('error', `[TriggersAPI] updateTrigger(${id}) failed: ${this.formatError(error)}`);
throw error;

View File

@@ -58,6 +58,7 @@ export interface EventCallback {
export interface StreamCallbacks {
onDelta: (delta: string) => void;
onThinkingDelta?: (delta: string) => void;
onTool?: (tool: string, input: string, output: string) => void;
onHand?: (name: string, status: string, result?: unknown) => void;
onComplete: (inputTokens?: number, outputTokens?: number) => void;
@@ -71,6 +72,11 @@ export interface StreamEventDelta {
delta: string;
}
export interface StreamEventThinkingDelta {
type: 'thinkingDelta';
delta: string;
}
export interface StreamEventToolStart {
type: 'tool_start';
name: string;
@@ -114,6 +120,7 @@ export interface StreamEventHandEnd {
export type StreamChatEvent =
| StreamEventDelta
| StreamEventThinkingDelta
| StreamEventToolStart
| StreamEventToolEnd
| StreamEventIterationStart

View File

@@ -1,233 +0,0 @@
/**
* SaaS Admin Methods — Mixin
*
* Installs admin panel API methods onto SaaSClient.prototype.
* Uses the same mixin pattern as gateway-api.ts.
*
* Reserved for future admin UI (Next.js admin dashboard).
* These methods are not called by the desktop app but are kept as thin API
* wrappers for when the admin panel is built.
*/
import type {
ProviderInfo,
CreateProviderRequest,
UpdateProviderRequest,
ModelInfo,
CreateModelRequest,
UpdateModelRequest,
AccountApiKeyInfo,
CreateApiKeyRequest,
AccountPublic,
UpdateAccountRequest,
PaginatedResponse,
TokenInfo,
CreateTokenRequest,
OperationLogInfo,
DashboardStats,
RoleInfo,
CreateRoleRequest,
UpdateRoleRequest,
PermissionTemplate,
CreateTemplateRequest,
} from './saas-types';
export function installAdminMethods(ClientClass: { prototype: any }): void {
const proto = ClientClass.prototype;
// --- Provider Management (Admin) ---
/** List all providers */
proto.listProviders = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }): Promise<ProviderInfo[]> {
return this.request<ProviderInfo[]>('GET', '/api/v1/providers');
};
/** Get provider by ID */
proto.getProvider = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<ProviderInfo> {
return this.request<ProviderInfo>('GET', `/api/v1/providers/${id}`);
};
/** Create a new provider (admin only) */
proto.createProvider = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, data: CreateProviderRequest): Promise<ProviderInfo> {
return this.request<ProviderInfo>('POST', '/api/v1/providers', data);
};
/** Update a provider (admin only) */
proto.updateProvider = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string, data: UpdateProviderRequest): Promise<ProviderInfo> {
return this.request<ProviderInfo>('PATCH', `/api/v1/providers/${id}`, data);
};
/** Delete a provider (admin only) */
proto.deleteProvider = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<void> {
await this.request<void>('DELETE', `/api/v1/providers/${id}`);
};
// --- Model Management (Admin) ---
/** List models, optionally filtered by provider */
proto.listModelsAdmin = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, providerId?: string): Promise<ModelInfo[]> {
const qs = providerId ? `?provider_id=${encodeURIComponent(providerId)}` : '';
return this.request<ModelInfo[]>('GET', `/api/v1/models${qs}`);
};
/** Get model by ID */
proto.getModel = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<ModelInfo> {
return this.request<ModelInfo>('GET', `/api/v1/models/${id}`);
};
/** Create a new model (admin only) */
proto.createModel = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, data: CreateModelRequest): Promise<ModelInfo> {
return this.request<ModelInfo>('POST', '/api/v1/models', data);
};
/** Update a model (admin only) */
proto.updateModel = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string, data: UpdateModelRequest): Promise<ModelInfo> {
return this.request<ModelInfo>('PATCH', `/api/v1/models/${id}`, data);
};
/** Delete a model (admin only) */
proto.deleteModel = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<void> {
await this.request<void>('DELETE', `/api/v1/models/${id}`);
};
// --- Account API Keys ---
/** List account's API keys */
proto.listApiKeys = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, providerId?: string): Promise<AccountApiKeyInfo[]> {
const qs = providerId ? `?provider_id=${encodeURIComponent(providerId)}` : '';
return this.request<AccountApiKeyInfo[]>('GET', `/api/v1/keys${qs}`);
};
/** Create a new API key */
proto.createApiKey = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, data: CreateApiKeyRequest): Promise<AccountApiKeyInfo> {
return this.request<AccountApiKeyInfo>('POST', '/api/v1/keys', data);
};
/** Rotate an API key */
proto.rotateApiKey = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string, newKeyValue: string): Promise<void> {
await this.request<void>('POST', `/api/v1/keys/${id}/rotate`, { new_key_value: newKeyValue });
};
/** Revoke an API key */
proto.revokeApiKey = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<void> {
await this.request<void>('DELETE', `/api/v1/keys/${id}`);
};
// --- Account Management (Admin) ---
/** List all accounts (admin only) */
proto.listAccounts = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, params?: { page?: number; page_size?: number; role?: string; status?: string; search?: string }): Promise<PaginatedResponse<AccountPublic>> {
const qs = new URLSearchParams();
if (params?.page) qs.set('page', String(params.page));
if (params?.page_size) qs.set('page_size', String(params.page_size));
if (params?.role) qs.set('role', params.role);
if (params?.status) qs.set('status', params.status);
if (params?.search) qs.set('search', params.search);
const query = qs.toString();
return this.request<PaginatedResponse<AccountPublic>>('GET', `/api/v1/accounts${query ? '?' + query : ''}`);
};
/** Get account by ID (admin or self) */
proto.getAccount = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<AccountPublic> {
return this.request<AccountPublic>('GET', `/api/v1/accounts/${id}`);
};
/** Update account (admin or self) */
proto.updateAccount = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string, data: UpdateAccountRequest): Promise<AccountPublic> {
return this.request<AccountPublic>('PATCH', `/api/v1/accounts/${id}`, data);
};
/** Update account status (admin only) */
proto.updateAccountStatus = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string, status: 'active' | 'disabled' | 'suspended'): Promise<void> {
await this.request<void>('PATCH', `/api/v1/accounts/${id}/status`, { status });
};
// --- API Token Management ---
/** List API tokens for current account */
proto.listTokens = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }): Promise<TokenInfo[]> {
return this.request<TokenInfo[]>('GET', '/api/v1/tokens');
};
/** Create a new API token */
proto.createToken = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, data: CreateTokenRequest): Promise<TokenInfo> {
return this.request<TokenInfo>('POST', '/api/v1/tokens', data);
};
/** Revoke an API token */
proto.revokeToken = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<void> {
await this.request<void>('DELETE', `/api/v1/tokens/${id}`);
};
// --- Operation Logs (Admin) ---
/** List operation logs (admin only) */
proto.listOperationLogs = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, params?: { page?: number; page_size?: number }): Promise<OperationLogInfo[]> {
const qs = new URLSearchParams();
if (params?.page) qs.set('page', String(params.page));
if (params?.page_size) qs.set('page_size', String(params.page_size));
const query = qs.toString();
return this.request<OperationLogInfo[]>('GET', `/api/v1/logs/operations${query ? '?' + query : ''}`);
};
// --- Dashboard Statistics (Admin) ---
/** Get dashboard statistics (admin only) */
proto.getDashboardStats = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }): Promise<DashboardStats> {
return this.request<DashboardStats>('GET', '/api/v1/stats/dashboard');
};
// --- Role Management (Admin) ---
/** List all roles */
proto.listRoles = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }): Promise<RoleInfo[]> {
return this.request<RoleInfo[]>('GET', '/api/v1/roles');
};
/** Get role by ID */
proto.getRole = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<RoleInfo> {
return this.request<RoleInfo>('GET', `/api/v1/roles/${id}`);
};
/** Create a new role (admin only) */
proto.createRole = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, data: CreateRoleRequest): Promise<RoleInfo> {
return this.request<RoleInfo>('POST', '/api/v1/roles', data);
};
/** Update a role (admin only) */
proto.updateRole = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string, data: UpdateRoleRequest): Promise<RoleInfo> {
return this.request<RoleInfo>('PUT', `/api/v1/roles/${id}`, data);
};
/** Delete a role (admin only) */
proto.deleteRole = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<void> {
await this.request<void>('DELETE', `/api/v1/roles/${id}`);
};
// --- Permission Templates ---
/** List permission templates */
proto.listPermissionTemplates = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }): Promise<PermissionTemplate[]> {
return this.request<PermissionTemplate[]>('GET', '/api/v1/permission-templates');
};
/** Get permission template by ID */
proto.getPermissionTemplate = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<PermissionTemplate> {
return this.request<PermissionTemplate>('GET', `/api/v1/permission-templates/${id}`);
};
/** Create a permission template (admin only) */
proto.createPermissionTemplate = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, data: CreateTemplateRequest): Promise<PermissionTemplate> {
return this.request<PermissionTemplate>('POST', '/api/v1/permission-templates', data);
};
/** Delete a permission template (admin only) */
proto.deletePermissionTemplate = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, id: string): Promise<void> {
await this.request<void>('DELETE', `/api/v1/permission-templates/${id}`);
};
/** Apply permission template to accounts (admin only) */
proto.applyPermissionTemplate = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, templateId: string, accountIds: string[]): Promise<{ ok: boolean; applied_count: number }> {
return this.request<{ ok: boolean; applied_count: number }>('POST', `/api/v1/permission-templates/${templateId}/apply`, { account_ids: accountIds });
};
}

View File

@@ -17,7 +17,6 @@
* - saas-errors.ts — SaaSApiError class
* - saas-session.ts — session persistence (load/save/clear)
* - saas-auth.ts — login/register/TOTP methods (mixin)
* - saas-admin.ts — admin panel API methods (mixin)
* - saas-relay.ts — relay tasks, chat completion, usage (mixin)
* - saas-prompt.ts — prompt OTA methods (mixin)
* - saas-telemetry.ts — telemetry reporting methods (mixin)
@@ -96,26 +95,6 @@ import type {
SaaSErrorResponse,
RelayTaskInfo,
UsageStats,
ProviderInfo,
CreateProviderRequest,
UpdateProviderRequest,
ModelInfo,
CreateModelRequest,
UpdateModelRequest,
AccountApiKeyInfo,
CreateApiKeyRequest,
AccountPublic,
UpdateAccountRequest,
PaginatedResponse,
TokenInfo,
CreateTokenRequest,
OperationLogInfo,
DashboardStats,
RoleInfo,
CreateRoleRequest,
UpdateRoleRequest,
PermissionTemplate,
CreateTemplateRequest,
PromptCheckResult,
PromptTemplateInfo,
PromptVersionInfo,
@@ -128,7 +107,7 @@ import { createLogger } from './logger';
const saasLog = createLogger('saas-client');
import { installAuthMethods } from './saas-auth';
import { installAdminMethods } from './saas-admin';
import { installRelayMethods } from './saas-relay';
import { installPromptMethods } from './saas-prompt';
import { installTelemetryMethods } from './saas-telemetry';
@@ -140,6 +119,25 @@ export class SaaSClient {
private baseUrl: string;
private token: string | null = null;
/**
* Refresh mutex: shared Promise to prevent concurrent token refresh.
* When multiple requests hit 401 simultaneously, they all await the same
* refresh Promise instead of triggering N parallel refresh calls.
*/
private _refreshPromise: Promise<string> | null = null;
/**
* Thread-safe token refresh — coalesces concurrent refresh attempts into one.
* First caller triggers the actual refresh; subsequent callers await the same Promise.
*/
async refreshMutex(): Promise<string> {
if (this._refreshPromise) return this._refreshPromise;
this._refreshPromise = this.refreshToken().finally(() => {
this._refreshPromise = null;
});
return this._refreshPromise;
}
constructor(baseUrl: string) {
this.baseUrl = baseUrl.replace(/\/+$/, '');
}
@@ -237,7 +235,7 @@ export class SaaSClient {
// 401: 尝试刷新 Token 后重试 (防止递归)
if (response.status === 401 && !this._isAuthEndpoint(path) && !_isRefreshRetry) {
try {
const newToken = await this.refreshToken();
const newToken = await this.refreshMutex();
if (newToken) {
return this.request<T>(method, path, body, timeoutMs, true);
}
@@ -394,7 +392,7 @@ export class SaaSClient {
* Used for template selection during onboarding.
*/
async fetchAvailableTemplates(): Promise<AgentTemplateAvailable[]> {
return this.request<AgentTemplateAvailable[]>('GET', '/agent-templates/available');
return this.request<AgentTemplateAvailable[]>('GET', '/api/v1/agent-templates/available');
}
/**
@@ -402,13 +400,12 @@ export class SaaSClient {
* Returns all fields needed to create an agent from template.
*/
async fetchTemplateFull(id: string): Promise<AgentTemplateFull> {
return this.request<AgentTemplateFull>('GET', `/agent-templates/${id}/full`);
return this.request<AgentTemplateFull>('GET', `/api/v1/agent-templates/${id}/full`);
}
}
// === Install mixin methods ===
installAuthMethods(SaaSClient);
installAdminMethods(SaaSClient);
installRelayMethods(SaaSClient);
installPromptMethods(SaaSClient);
installTelemetryMethods(SaaSClient);
@@ -429,57 +426,6 @@ export interface SaaSClient {
verifyTotp(code: string): Promise<TotpResultResponse>;
disableTotp(password: string): Promise<TotpResultResponse>;
// --- Admin: Providers (saas-admin.ts) ---
listProviders(): Promise<ProviderInfo[]>;
getProvider(id: string): Promise<ProviderInfo>;
createProvider(data: CreateProviderRequest): Promise<ProviderInfo>;
updateProvider(id: string, data: UpdateProviderRequest): Promise<ProviderInfo>;
deleteProvider(id: string): Promise<void>;
// --- Admin: Models (saas-admin.ts) ---
listModelsAdmin(providerId?: string): Promise<ModelInfo[]>;
getModel(id: string): Promise<ModelInfo>;
createModel(data: CreateModelRequest): Promise<ModelInfo>;
updateModel(id: string, data: UpdateModelRequest): Promise<ModelInfo>;
deleteModel(id: string): Promise<void>;
// --- Admin: API Keys (saas-admin.ts) ---
listApiKeys(providerId?: string): Promise<AccountApiKeyInfo[]>;
createApiKey(data: CreateApiKeyRequest): Promise<AccountApiKeyInfo>;
rotateApiKey(id: string, newKeyValue: string): Promise<void>;
revokeApiKey(id: string): Promise<void>;
// --- Admin: Accounts (saas-admin.ts) ---
listAccounts(params?: { page?: number; page_size?: number; role?: string; status?: string; search?: string }): Promise<PaginatedResponse<AccountPublic>>;
getAccount(id: string): Promise<AccountPublic>;
updateAccount(id: string, data: UpdateAccountRequest): Promise<AccountPublic>;
updateAccountStatus(id: string, status: 'active' | 'disabled' | 'suspended'): Promise<void>;
// --- Admin: Tokens (saas-admin.ts) ---
listTokens(): Promise<TokenInfo[]>;
createToken(data: CreateTokenRequest): Promise<TokenInfo>;
revokeToken(id: string): Promise<void>;
// --- Admin: Logs (saas-admin.ts) ---
listOperationLogs(params?: { page?: number; page_size?: number }): Promise<OperationLogInfo[]>;
// --- Admin: Dashboard (saas-admin.ts) ---
getDashboardStats(): Promise<DashboardStats>;
// --- Admin: Roles (saas-admin.ts) ---
listRoles(): Promise<RoleInfo[]>;
getRole(id: string): Promise<RoleInfo>;
createRole(data: CreateRoleRequest): Promise<RoleInfo>;
updateRole(id: string, data: UpdateRoleRequest): Promise<RoleInfo>;
deleteRole(id: string): Promise<void>;
// --- Admin: Permission Templates (saas-admin.ts) ---
listPermissionTemplates(): Promise<PermissionTemplate[]>;
getPermissionTemplate(id: string): Promise<PermissionTemplate>;
createPermissionTemplate(data: CreateTemplateRequest): Promise<PermissionTemplate>;
deletePermissionTemplate(id: string): Promise<void>;
applyPermissionTemplate(templateId: string, accountIds: string[]): Promise<{ ok: boolean; applied_count: number }>;
// --- Relay (saas-relay.ts) ---
listRelayTasks(query?: { status?: string; page?: number; page_size?: number }): Promise<RelayTaskInfo[]>;
getRelayTask(taskId: string): Promise<RelayTaskInfo>;

View File

@@ -55,6 +55,7 @@ export function installRelayMethods(ClientClass: { prototype: any }): void {
_serverReachable: boolean;
_isAuthEndpoint(path: string): boolean;
refreshToken(): Promise<string>;
refreshMutex(): Promise<string>;
},
body: unknown,
signal?: AbortSignal,
@@ -87,7 +88,7 @@ export function installRelayMethods(ClientClass: { prototype: any }): void {
// On 401, attempt token refresh once
if (response.status === 401 && attempt === 0 && !this._isAuthEndpoint('/api/v1/relay/chat/completions')) {
try {
const newToken = await this.refreshToken();
const newToken = await this.refreshMutex();
if (newToken) continue; // Retry with refreshed token
} catch (e) {
logger.debug('Token refresh failed', { error: e });

View File

@@ -299,36 +299,6 @@ function readLocalStorageBackup(key: string): string | null {
}
}
/**
* Synchronous versions for compatibility with existing code
* These use localStorage only and are provided for gradual migration
*/
export const secureStorageSync = {
/**
* Synchronously get a value from localStorage (for migration only)
* @deprecated Use async secureStorage.get() instead
*/
get(key: string): string | null {
return readLocalStorageBackup(key);
},
/**
* Synchronously set a value in localStorage (for migration only)
* @deprecated Use async secureStorage.set() instead
*/
set(key: string, value: string): void {
writeLocalStorageBackup(key, value);
},
/**
* Synchronously delete a value from localStorage (for migration only)
* @deprecated Use async secureStorage.delete() instead
*/
delete(key: string): void {
clearLocalStorageBackup(key);
},
};
// === Device Keys Secure Storage ===
/**

View File

@@ -47,7 +47,6 @@ export type { EncryptedData } from './crypto-utils';
// Re-export secure storage
export {
secureStorage,
secureStorageSync,
isSecureStorageAvailable,
storeDeviceKeys,
getDeviceKeys,