feat(saas): 桌面端 P2 客户端补齐 — TOTP 2FA、Relay 任务、Config 同步
- saas-client: 添加 TOTP/Relay/Config 类型和 typed 方法,login 支持 totp_code - saasStore: TOTP 感知登录 (检测 TOTP_ERROR → 两步登录),TOTP 管理动作 - SaaSLogin: TOTP 验证码输入步骤 (6 位数字,Enter 提交) - TOTPSettings (新): 启用流程 (QR 码 + secret + 验证码),禁用 (密码确认) - RelayTasksPanel (新): 状态过滤、任务列表、Admin 重试按钮 - SaaSSettings: 集成 TOTP 和 Relay 面板到设置页
This commit is contained in:
@@ -72,6 +72,20 @@ interface SaaSRefreshResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
/** TOTP setup response from POST /api/v1/auth/totp/setup */
|
||||
export interface TotpSetupResponse {
|
||||
otpauth_uri: string;
|
||||
secret: string;
|
||||
issuer: string;
|
||||
}
|
||||
|
||||
/** TOTP verify/disable response */
|
||||
export interface TotpResultResponse {
|
||||
ok: boolean;
|
||||
totp_enabled: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** Device info stored on the SaaS backend */
|
||||
export interface DeviceInfo {
|
||||
id: string;
|
||||
@@ -83,6 +97,55 @@ export interface DeviceInfo {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Relay task info from GET /api/v1/relay/tasks */
|
||||
export interface RelayTaskInfo {
|
||||
id: string;
|
||||
account_id: string;
|
||||
provider_id: string;
|
||||
model_id: string;
|
||||
status: string;
|
||||
priority: number;
|
||||
attempt_count: number;
|
||||
max_attempts: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
error_message: string | null;
|
||||
queued_at: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Config diff request for POST /api/v1/config/diff and /sync */
|
||||
export interface SyncConfigRequest {
|
||||
client_fingerprint: string;
|
||||
action: 'push' | 'merge';
|
||||
config_keys: string[];
|
||||
client_values: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** A single config diff entry */
|
||||
export interface ConfigDiffItem {
|
||||
key_path: string;
|
||||
client_value: string | null;
|
||||
saas_value: string | null;
|
||||
conflict: boolean;
|
||||
}
|
||||
|
||||
/** Config diff response */
|
||||
export interface ConfigDiffResponse {
|
||||
items: ConfigDiffItem[];
|
||||
total_keys: number;
|
||||
conflicts: number;
|
||||
}
|
||||
|
||||
/** Config sync result */
|
||||
export interface ConfigSyncResult {
|
||||
updated: number;
|
||||
created: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
// === Error Class ===
|
||||
|
||||
export class SaaSApiError extends Error {
|
||||
@@ -210,7 +273,7 @@ export class SaaSClient {
|
||||
* Retries up to 2 times with exponential backoff (1s, 2s).
|
||||
* Throws SaaSApiError on non-ok responses.
|
||||
*/
|
||||
private async request<T>(
|
||||
public async request<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
@@ -298,9 +361,11 @@ export class SaaSClient {
|
||||
* Login with username and password.
|
||||
* Auto-sets the client token on success.
|
||||
*/
|
||||
async login(username: string, password: string): Promise<SaaSLoginResponse> {
|
||||
async login(username: string, password: string, totpCode?: string): Promise<SaaSLoginResponse> {
|
||||
const body: Record<string, string> = { username, password };
|
||||
if (totpCode) body.totp_code = totpCode;
|
||||
const data = await this.request<SaaSLoginResponse>(
|
||||
'POST', '/api/v1/auth/login', { username, password },
|
||||
'POST', '/api/v1/auth/login', body,
|
||||
);
|
||||
this.token = data.token;
|
||||
return data;
|
||||
@@ -350,6 +415,23 @@ export class SaaSClient {
|
||||
});
|
||||
}
|
||||
|
||||
// --- TOTP Endpoints ---
|
||||
|
||||
/** Generate a TOTP secret and otpauth URI */
|
||||
async setupTotp(): Promise<TotpSetupResponse> {
|
||||
return this.request<TotpSetupResponse>('POST', '/api/v1/auth/totp/setup');
|
||||
}
|
||||
|
||||
/** Verify a TOTP code and enable 2FA */
|
||||
async verifyTotp(code: string): Promise<TotpResultResponse> {
|
||||
return this.request<TotpResultResponse>('POST', '/api/v1/auth/totp/verify', { code });
|
||||
}
|
||||
|
||||
/** Disable 2FA (requires password confirmation) */
|
||||
async disableTotp(password: string): Promise<TotpResultResponse> {
|
||||
return this.request<TotpResultResponse>('POST', '/api/v1/auth/totp/disable', { password });
|
||||
}
|
||||
|
||||
// --- Device Endpoints ---
|
||||
|
||||
/**
|
||||
@@ -391,6 +473,28 @@ export class SaaSClient {
|
||||
return this.request<SaaSModelInfo[]>('GET', '/api/v1/relay/models');
|
||||
}
|
||||
|
||||
// --- Relay Task Management ---
|
||||
|
||||
/** List relay tasks for the current user */
|
||||
async listRelayTasks(query?: { status?: string; page?: number; page_size?: number }): Promise<RelayTaskInfo[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (query?.status) params.set('status', query.status);
|
||||
if (query?.page) params.set('page', String(query.page));
|
||||
if (query?.page_size) params.set('page_size', String(query.page_size));
|
||||
const qs = params.toString();
|
||||
return this.request<RelayTaskInfo[]>('GET', `/api/v1/relay/tasks${qs ? '?' + qs : ''}`);
|
||||
}
|
||||
|
||||
/** Get a single relay task */
|
||||
async getRelayTask(taskId: string): Promise<RelayTaskInfo> {
|
||||
return this.request<RelayTaskInfo>('GET', `/api/v1/relay/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
/** Retry a failed relay task (admin only) */
|
||||
async retryRelayTask(taskId: string): Promise<{ ok: boolean; task_id: string }> {
|
||||
return this.request<{ ok: boolean; task_id: string }>('POST', `/api/v1/relay/tasks/${taskId}/retry`);
|
||||
}
|
||||
|
||||
// --- Chat Relay ---
|
||||
|
||||
/**
|
||||
@@ -437,6 +541,16 @@ export class SaaSClient {
|
||||
const qs = category ? `?category=${encodeURIComponent(category)}` : '';
|
||||
return this.request<SaaSConfigItem[]>('GET', `/api/v1/config/items${qs}`);
|
||||
}
|
||||
|
||||
/** Compute config diff between client and SaaS (read-only) */
|
||||
async computeConfigDiff(request: SyncConfigRequest): Promise<ConfigDiffResponse> {
|
||||
return this.request<ConfigDiffResponse>('POST', '/api/v1/config/diff', request);
|
||||
}
|
||||
|
||||
/** Sync config from client to SaaS (push) or merge */
|
||||
async syncConfig(request: SyncConfigRequest): Promise<ConfigSyncResult> {
|
||||
return this.request<ConfigSyncResult>('POST', '/api/v1/config/sync', request);
|
||||
}
|
||||
}
|
||||
|
||||
// === Singleton ===
|
||||
|
||||
Reference in New Issue
Block a user