chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -146,6 +146,282 @@ export interface ConfigSyncResult {
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
/** Paginated response wrapper */
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
// === Prompt OTA Types ===
|
||||
|
||||
/** Prompt template info */
|
||||
export interface PromptTemplateInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
description: string | null;
|
||||
source: string;
|
||||
current_version: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Prompt version info */
|
||||
export interface PromptVersionInfo {
|
||||
id: string;
|
||||
template_id: string;
|
||||
version: number;
|
||||
system_prompt: string;
|
||||
user_prompt_template: string | null;
|
||||
variables: PromptVariable[];
|
||||
changelog: string | null;
|
||||
min_app_version: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Prompt variable definition */
|
||||
export interface PromptVariable {
|
||||
name: string;
|
||||
type: string;
|
||||
default_value?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
/** OTA update check result */
|
||||
export interface PromptCheckResult {
|
||||
updates: PromptUpdatePayload[];
|
||||
server_time: string;
|
||||
}
|
||||
|
||||
/** Single OTA update payload */
|
||||
export interface PromptUpdatePayload {
|
||||
name: string;
|
||||
version: number;
|
||||
system_prompt: string;
|
||||
user_prompt_template: string | null;
|
||||
variables: PromptVariable[];
|
||||
source: string;
|
||||
min_app_version: string | null;
|
||||
changelog: string | null;
|
||||
}
|
||||
|
||||
/** Provider info from GET /api/v1/providers */
|
||||
export interface ProviderInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
base_url: string;
|
||||
api_protocol: string;
|
||||
enabled: boolean;
|
||||
rate_limit_rpm: number | null;
|
||||
rate_limit_tpm: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Create provider request */
|
||||
export interface CreateProviderRequest {
|
||||
name: string;
|
||||
display_name: string;
|
||||
base_url: string;
|
||||
api_protocol?: string;
|
||||
api_key?: string;
|
||||
rate_limit_rpm?: number;
|
||||
rate_limit_tpm?: number;
|
||||
}
|
||||
|
||||
/** Update provider request */
|
||||
export interface UpdateProviderRequest {
|
||||
display_name?: string;
|
||||
base_url?: string;
|
||||
api_key?: string;
|
||||
rate_limit_rpm?: number;
|
||||
rate_limit_tpm?: number;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/** Model info from GET /api/v1/models */
|
||||
export interface ModelInfo {
|
||||
id: string;
|
||||
provider_id: string;
|
||||
model_id: string;
|
||||
alias: string;
|
||||
context_window: number;
|
||||
max_output_tokens: number;
|
||||
supports_streaming: boolean;
|
||||
supports_vision: boolean;
|
||||
enabled: boolean;
|
||||
pricing_input: number;
|
||||
pricing_output: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Create model request */
|
||||
export interface CreateModelRequest {
|
||||
provider_id: string;
|
||||
model_id: string;
|
||||
alias: string;
|
||||
context_window?: number;
|
||||
max_output_tokens?: number;
|
||||
supports_streaming?: boolean;
|
||||
supports_vision?: boolean;
|
||||
pricing_input?: number;
|
||||
pricing_output?: number;
|
||||
}
|
||||
|
||||
/** Update model request */
|
||||
export interface UpdateModelRequest {
|
||||
alias?: string;
|
||||
context_window?: number;
|
||||
max_output_tokens?: number;
|
||||
supports_streaming?: boolean;
|
||||
supports_vision?: boolean;
|
||||
enabled?: boolean;
|
||||
pricing_input?: number;
|
||||
pricing_output?: number;
|
||||
}
|
||||
|
||||
/** Account API key info */
|
||||
export interface AccountApiKeyInfo {
|
||||
id: string;
|
||||
provider_id: string;
|
||||
key_label: string | null;
|
||||
permissions: string[];
|
||||
enabled: boolean;
|
||||
last_used_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Create API key request */
|
||||
export interface CreateApiKeyRequest {
|
||||
provider_id: string;
|
||||
key_value: string;
|
||||
key_label?: string;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
/** Usage statistics */
|
||||
export interface UsageStats {
|
||||
total_input_tokens: number;
|
||||
total_output_tokens: number;
|
||||
total_requests: number;
|
||||
by_provider: Record<string, { input_tokens: number; output_tokens: number; requests: number }>;
|
||||
by_model: Record<string, { input_tokens: number; output_tokens: number; requests: number }>;
|
||||
daily: Array<{ date: string; input_tokens: number; output_tokens: number; requests: number }>;
|
||||
}
|
||||
|
||||
/** Account public info (extended) */
|
||||
export interface AccountPublic {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
display_name: string;
|
||||
role: string;
|
||||
status: string;
|
||||
totp_enabled: boolean;
|
||||
last_login_at: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Update account request */
|
||||
export interface UpdateAccountRequest {
|
||||
display_name?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
/** Token info */
|
||||
export interface TokenInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
token_prefix: string;
|
||||
permissions: string[];
|
||||
last_used_at: string | null;
|
||||
expires_at: string | null;
|
||||
created_at: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
/** Create token request */
|
||||
export interface CreateTokenRequest {
|
||||
name: string;
|
||||
permissions: string[];
|
||||
expires_days?: number;
|
||||
}
|
||||
|
||||
/** Operation log info */
|
||||
export interface OperationLogInfo {
|
||||
id: number;
|
||||
account_id: string | null;
|
||||
action: string;
|
||||
target_type: string | null;
|
||||
target_id: string | null;
|
||||
details: Record<string, unknown> | null;
|
||||
ip_address: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** Dashboard statistics */
|
||||
export interface DashboardStats {
|
||||
total_accounts: number;
|
||||
active_accounts: number;
|
||||
tasks_today: number;
|
||||
active_providers: number;
|
||||
active_models: number;
|
||||
tokens_today_input: number;
|
||||
tokens_today_output: number;
|
||||
}
|
||||
|
||||
/** Role info */
|
||||
export interface RoleInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
permissions: string[];
|
||||
is_system: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Create role request */
|
||||
export interface CreateRoleRequest {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
/** Update role request */
|
||||
export interface UpdateRoleRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
/** Permission template */
|
||||
export interface PermissionTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
permissions: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Create template request */
|
||||
export interface CreateTemplateRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
// === Error Class ===
|
||||
|
||||
export class SaaSApiError extends Error {
|
||||
@@ -258,6 +534,11 @@ export class SaaSClient {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
/** Check if a path is an auth endpoint (avoid infinite refresh loop) */
|
||||
private _isAuthEndpoint(path: string): boolean {
|
||||
return path.includes('/auth/login') || path.includes('/auth/register') || path.includes('/auth/refresh');
|
||||
}
|
||||
|
||||
// --- Core HTTP ---
|
||||
|
||||
/** Track whether the server appears reachable */
|
||||
@@ -278,6 +559,7 @@ export class SaaSClient {
|
||||
path: string,
|
||||
body?: unknown,
|
||||
timeoutMs = 15000,
|
||||
_isRefreshRetry = false,
|
||||
): Promise<T> {
|
||||
const maxRetries = 2;
|
||||
const baseDelay = 1000;
|
||||
@@ -300,8 +582,31 @@ export class SaaSClient {
|
||||
|
||||
this._serverReachable = true;
|
||||
|
||||
// Handle 401 specially - caller may want to trigger re-auth
|
||||
if (response.status === 401) {
|
||||
// 401: 尝试刷新 Token 后重试 (防止递归)
|
||||
if (response.status === 401 && !this._isAuthEndpoint(path) && !_isRefreshRetry) {
|
||||
try {
|
||||
const newToken = await this.refreshToken();
|
||||
if (newToken) {
|
||||
// Persist refreshed token to localStorage
|
||||
try {
|
||||
const raw = localStorage.getItem('zclaw-saas-session');
|
||||
if (raw) {
|
||||
const session = JSON.parse(raw);
|
||||
session.token = newToken;
|
||||
localStorage.setItem('zclaw-saas-session', JSON.stringify(session));
|
||||
}
|
||||
} catch { /* non-blocking */ }
|
||||
return this.request<T>(method, path, body, timeoutMs, true);
|
||||
}
|
||||
} catch (refreshErr) {
|
||||
// Token refresh failed — clear session and trigger logout
|
||||
try {
|
||||
const { clearSaaSSession } = require('./saas-client');
|
||||
clearSaaSSession();
|
||||
localStorage.removeItem('zclaw-connection-mode');
|
||||
} catch { /* non-blocking */ }
|
||||
throw new SaaSApiError(401, 'SESSION_EXPIRED', '会话已过期,请重新登录');
|
||||
}
|
||||
throw new SaaSApiError(401, 'UNAUTHORIZED', '认证已过期,请重新登录');
|
||||
}
|
||||
|
||||
@@ -364,6 +669,8 @@ export class SaaSClient {
|
||||
async login(username: string, password: string, totpCode?: string): Promise<SaaSLoginResponse> {
|
||||
const body: Record<string, string> = { username, password };
|
||||
if (totpCode) body.totp_code = totpCode;
|
||||
// Clear stale token before login — avoid sending expired token on auth endpoint
|
||||
this.token = null;
|
||||
const data = await this.request<SaaSLoginResponse>(
|
||||
'POST', '/api/v1/auth/login', body,
|
||||
);
|
||||
@@ -381,6 +688,8 @@ export class SaaSClient {
|
||||
password: string;
|
||||
display_name?: string;
|
||||
}): Promise<SaaSLoginResponse> {
|
||||
// Clear stale token before register
|
||||
this.token = null;
|
||||
const result = await this.request<SaaSLoginResponse>(
|
||||
'POST', '/api/v1/auth/register', data,
|
||||
);
|
||||
@@ -449,10 +758,14 @@ export class SaaSClient {
|
||||
|
||||
/**
|
||||
* Send a heartbeat to indicate the device is still active.
|
||||
* Also sends platform and app_version so the backend can detect client upgrades.
|
||||
*/
|
||||
async deviceHeartbeat(deviceId: string): Promise<void> {
|
||||
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : 'unknown';
|
||||
await this.request<unknown>('POST', '/api/v1/devices/heartbeat', {
|
||||
device_id: deviceId,
|
||||
platform: typeof navigator !== 'undefined' ? navigator.platform : undefined,
|
||||
app_version: appVersion,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -460,7 +773,8 @@ export class SaaSClient {
|
||||
* List devices registered for the current account.
|
||||
*/
|
||||
async listDevices(): Promise<DeviceInfo[]> {
|
||||
return this.request<DeviceInfo[]>('GET', '/api/v1/devices');
|
||||
const res = await this.request<{ items: DeviceInfo[] }>('GET', '/api/v1/devices');
|
||||
return res.items;
|
||||
}
|
||||
|
||||
// --- Model Endpoints ---
|
||||
@@ -501,6 +815,7 @@ export class SaaSClient {
|
||||
* Send a chat completion request via the SaaS relay.
|
||||
* Returns the raw Response object to support both streaming and non-streaming.
|
||||
*
|
||||
* Includes one retry on 401 (auto token refresh) and on network errors.
|
||||
* The caller is responsible for:
|
||||
* - Reading the response body (JSON or SSE stream)
|
||||
* - Handling errors from the response
|
||||
@@ -509,27 +824,59 @@ export class SaaSClient {
|
||||
body: unknown,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Response> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
const maxAttempts = 2; // 1 initial + 1 retry
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (this.token) {
|
||||
headers['Authorization'] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
// Use caller's AbortSignal if provided, otherwise default 5min timeout
|
||||
const effectiveSignal = signal ?? AbortSignal.timeout(300_000);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/v1/relay/chat/completions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: effectiveSignal,
|
||||
},
|
||||
);
|
||||
|
||||
// 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();
|
||||
if (newToken) continue; // Retry with refreshed token
|
||||
} catch {
|
||||
// Refresh failed, return the 401 response
|
||||
}
|
||||
}
|
||||
|
||||
this._serverReachable = true;
|
||||
return response;
|
||||
} catch (err: unknown) {
|
||||
this._serverReachable = false;
|
||||
const isNetworkError = err instanceof TypeError
|
||||
&& (err.message.includes('Failed to fetch') || err.message.includes('NetworkError'));
|
||||
|
||||
if (isNetworkError && attempt < maxAttempts - 1) {
|
||||
// Brief backoff before retry
|
||||
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Use caller's AbortSignal if provided, otherwise default 5min timeout
|
||||
const effectiveSignal = signal ?? AbortSignal.timeout(300_000);
|
||||
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/v1/relay/chat/completions`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: effectiveSignal,
|
||||
},
|
||||
);
|
||||
|
||||
return response;
|
||||
// Unreachable but TypeScript needs it
|
||||
throw new Error('chatCompletion: all attempts exhausted');
|
||||
}
|
||||
|
||||
// --- Config Endpoints ---
|
||||
@@ -539,7 +886,8 @@ export class SaaSClient {
|
||||
*/
|
||||
async listConfig(category?: string): Promise<SaaSConfigItem[]> {
|
||||
const qs = category ? `?category=${encodeURIComponent(category)}` : '';
|
||||
return this.request<SaaSConfigItem[]>('GET', `/api/v1/config/items${qs}`);
|
||||
const res = await this.request<{ items: SaaSConfigItem[] }>('GET', `/api/v1/config/items${qs}`);
|
||||
return res.items;
|
||||
}
|
||||
|
||||
/** Compute config diff between client and SaaS (read-only) */
|
||||
@@ -551,6 +899,302 @@ export class SaaSClient {
|
||||
async syncConfig(request: SyncConfigRequest): Promise<ConfigSyncResult> {
|
||||
return this.request<ConfigSyncResult>('POST', '/api/v1/config/sync', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull all config items from SaaS (for startup auto-sync).
|
||||
* Returns configs updated since the given timestamp, or all if since is omitted.
|
||||
*/
|
||||
async pullConfig(since?: string): Promise<{
|
||||
configs: Array<{
|
||||
key: string
|
||||
category: string
|
||||
value: string | null
|
||||
value_type: string
|
||||
default: string | null
|
||||
updated_at: string
|
||||
}>
|
||||
pulled_at: string
|
||||
}> {
|
||||
const qs = since ? `?since=${encodeURIComponent(since)}` : '';
|
||||
return this.request('GET', '/api/v1/config/pull' + qs);
|
||||
}
|
||||
|
||||
// --- Provider Management (Admin) ---
|
||||
|
||||
/** List all providers */
|
||||
async listProviders(): Promise<ProviderInfo[]> {
|
||||
return this.request<ProviderInfo[]>('GET', '/api/v1/providers');
|
||||
}
|
||||
|
||||
/** Get provider by ID */
|
||||
async getProvider(id: string): Promise<ProviderInfo> {
|
||||
return this.request<ProviderInfo>('GET', `/api/v1/providers/${id}`);
|
||||
}
|
||||
|
||||
/** Create a new provider (admin only) */
|
||||
async createProvider(data: CreateProviderRequest): Promise<ProviderInfo> {
|
||||
return this.request<ProviderInfo>('POST', '/api/v1/providers', data);
|
||||
}
|
||||
|
||||
/** Update a provider (admin only) */
|
||||
async updateProvider(id: string, data: UpdateProviderRequest): Promise<ProviderInfo> {
|
||||
return this.request<ProviderInfo>('PATCH', `/api/v1/providers/${id}`, data);
|
||||
}
|
||||
|
||||
/** Delete a provider (admin only) */
|
||||
async deleteProvider(id: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `/api/v1/providers/${id}`);
|
||||
}
|
||||
|
||||
// --- Model Management (Admin) ---
|
||||
|
||||
/** List models, optionally filtered by provider */
|
||||
async listModelsAdmin(providerId?: string): Promise<ModelInfo[]> {
|
||||
const qs = providerId ? `?provider_id=${encodeURIComponent(providerId)}` : '';
|
||||
return this.request<ModelInfo[]>('GET', `/api/v1/models${qs}`);
|
||||
}
|
||||
|
||||
/** Get model by ID */
|
||||
async getModel(id: string): Promise<ModelInfo> {
|
||||
return this.request<ModelInfo>('GET', `/api/v1/models/${id}`);
|
||||
}
|
||||
|
||||
/** Create a new model (admin only) */
|
||||
async createModel(data: CreateModelRequest): Promise<ModelInfo> {
|
||||
return this.request<ModelInfo>('POST', '/api/v1/models', data);
|
||||
}
|
||||
|
||||
/** Update a model (admin only) */
|
||||
async updateModel(id: string, data: UpdateModelRequest): Promise<ModelInfo> {
|
||||
return this.request<ModelInfo>('PATCH', `/api/v1/models/${id}`, data);
|
||||
}
|
||||
|
||||
/** Delete a model (admin only) */
|
||||
async deleteModel(id: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `/api/v1/models/${id}`);
|
||||
}
|
||||
|
||||
// --- Account API Keys ---
|
||||
|
||||
/** List account's API keys */
|
||||
async listApiKeys(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 */
|
||||
async createApiKey(data: CreateApiKeyRequest): Promise<AccountApiKeyInfo> {
|
||||
return this.request<AccountApiKeyInfo>('POST', '/api/v1/keys', data);
|
||||
}
|
||||
|
||||
/** Rotate an API key */
|
||||
async rotateApiKey(id: string, newKeyValue: string): Promise<void> {
|
||||
await this.request<void>('POST', `/api/v1/keys/${id}/rotate`, { new_key_value: newKeyValue });
|
||||
}
|
||||
|
||||
/** Revoke an API key */
|
||||
async revokeApiKey(id: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `/api/v1/keys/${id}`);
|
||||
}
|
||||
|
||||
// --- Usage Statistics ---
|
||||
|
||||
/** Get usage statistics for current account */
|
||||
async getUsage(params?: { from?: string; to?: string; provider_id?: string; model_id?: string }): Promise<UsageStats> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.from) qs.set('from', params.from);
|
||||
if (params?.to) qs.set('to', params.to);
|
||||
if (params?.provider_id) qs.set('provider_id', params.provider_id);
|
||||
if (params?.model_id) qs.set('model_id', params.model_id);
|
||||
const query = qs.toString();
|
||||
return this.request<UsageStats>('GET', `/api/v1/usage${query ? '?' + query : ''}`);
|
||||
}
|
||||
|
||||
// --- Account Management (Admin) ---
|
||||
|
||||
/** List all accounts (admin only) */
|
||||
async listAccounts(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) */
|
||||
async getAccount(id: string): Promise<AccountPublic> {
|
||||
return this.request<AccountPublic>('GET', `/api/v1/accounts/${id}`);
|
||||
}
|
||||
|
||||
/** Update account (admin or self) */
|
||||
async updateAccount(id: string, data: UpdateAccountRequest): Promise<AccountPublic> {
|
||||
return this.request<AccountPublic>('PATCH', `/api/v1/accounts/${id}`, data);
|
||||
}
|
||||
|
||||
/** Update account status (admin only) */
|
||||
async updateAccountStatus(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 */
|
||||
async listTokens(): Promise<TokenInfo[]> {
|
||||
return this.request<TokenInfo[]>('GET', '/api/v1/tokens');
|
||||
}
|
||||
|
||||
/** Create a new API token */
|
||||
async createToken(data: CreateTokenRequest): Promise<TokenInfo> {
|
||||
return this.request<TokenInfo>('POST', '/api/v1/tokens', data);
|
||||
}
|
||||
|
||||
/** Revoke an API token */
|
||||
async revokeToken(id: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `/api/v1/tokens/${id}`);
|
||||
}
|
||||
|
||||
// --- Operation Logs (Admin) ---
|
||||
|
||||
/** List operation logs (admin only) */
|
||||
async listOperationLogs(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) */
|
||||
async getDashboardStats(): Promise<DashboardStats> {
|
||||
return this.request<DashboardStats>('GET', '/api/v1/stats/dashboard');
|
||||
}
|
||||
|
||||
// --- Role Management (Admin) ---
|
||||
|
||||
/** List all roles */
|
||||
async listRoles(): Promise<RoleInfo[]> {
|
||||
return this.request<RoleInfo[]>('GET', '/api/v1/roles');
|
||||
}
|
||||
|
||||
/** Get role by ID */
|
||||
async getRole(id: string): Promise<RoleInfo> {
|
||||
return this.request<RoleInfo>('GET', `/api/v1/roles/${id}`);
|
||||
}
|
||||
|
||||
/** Create a new role (admin only) */
|
||||
async createRole(data: CreateRoleRequest): Promise<RoleInfo> {
|
||||
return this.request<RoleInfo>('POST', '/api/v1/roles', data);
|
||||
}
|
||||
|
||||
/** Update a role (admin only) */
|
||||
async updateRole(id: string, data: UpdateRoleRequest): Promise<RoleInfo> {
|
||||
return this.request<RoleInfo>('PUT', `/api/v1/roles/${id}`, data);
|
||||
}
|
||||
|
||||
/** Delete a role (admin only) */
|
||||
async deleteRole(id: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `/api/v1/roles/${id}`);
|
||||
}
|
||||
|
||||
// --- Permission Templates ---
|
||||
|
||||
/** List permission templates */
|
||||
async listPermissionTemplates(): Promise<PermissionTemplate[]> {
|
||||
return this.request<PermissionTemplate[]>('GET', '/api/v1/permission-templates');
|
||||
}
|
||||
|
||||
/** Get permission template by ID */
|
||||
async getPermissionTemplate(id: string): Promise<PermissionTemplate> {
|
||||
return this.request<PermissionTemplate>('GET', `/api/v1/permission-templates/${id}`);
|
||||
}
|
||||
|
||||
/** Create a permission template (admin only) */
|
||||
async createPermissionTemplate(data: CreateTemplateRequest): Promise<PermissionTemplate> {
|
||||
return this.request<PermissionTemplate>('POST', '/api/v1/permission-templates', data);
|
||||
}
|
||||
|
||||
/** Delete a permission template (admin only) */
|
||||
async deletePermissionTemplate(id: string): Promise<void> {
|
||||
await this.request<void>('DELETE', `/api/v1/permission-templates/${id}`);
|
||||
}
|
||||
|
||||
/** Apply permission template to accounts (admin only) */
|
||||
async applyPermissionTemplate(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 });
|
||||
}
|
||||
|
||||
// === Prompt OTA ===
|
||||
|
||||
/** Check for prompt updates (OTA) */
|
||||
async checkPromptUpdates(deviceId: string, currentVersions: Record<string, number>): Promise<PromptCheckResult> {
|
||||
return this.request<PromptCheckResult>('POST', '/api/v1/prompts/check', {
|
||||
device_id: deviceId,
|
||||
versions: currentVersions,
|
||||
});
|
||||
}
|
||||
|
||||
/** List all prompt templates */
|
||||
async listPrompts(params?: { category?: string; source?: string; status?: string; page?: number; page_size?: number }): Promise<PaginatedResponse<PromptTemplateInfo>> {
|
||||
const qs = params ? '?' + new URLSearchParams(params as Record<string, string>).toString() : '';
|
||||
return this.request<PaginatedResponse<PromptTemplateInfo>>('GET', `/api/v1/prompts${qs}`);
|
||||
}
|
||||
|
||||
/** Get prompt template by name */
|
||||
async getPrompt(name: string): Promise<PromptTemplateInfo> {
|
||||
return this.request<PromptTemplateInfo>('GET', `/api/v1/prompts/${encodeURIComponent(name)}`);
|
||||
}
|
||||
|
||||
/** List prompt versions */
|
||||
async listPromptVersions(name: string): Promise<PromptVersionInfo[]> {
|
||||
return this.request<PromptVersionInfo[]>('GET', `/api/v1/prompts/${encodeURIComponent(name)}/versions`);
|
||||
}
|
||||
|
||||
/** Get specific prompt version */
|
||||
async getPromptVersion(name: string, version: number): Promise<PromptVersionInfo> {
|
||||
return this.request<PromptVersionInfo>('GET', `/api/v1/prompts/${encodeURIComponent(name)}/versions/${version}`);
|
||||
}
|
||||
|
||||
// === Telemetry ===
|
||||
|
||||
/** Report anonymous usage telemetry (token counts only, no content) */
|
||||
async reportTelemetry(data: {
|
||||
device_id: string;
|
||||
app_version: string;
|
||||
entries: Array<{
|
||||
model_id: string;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
latency_ms?: number;
|
||||
success: boolean;
|
||||
error_type?: string;
|
||||
timestamp: string;
|
||||
connection_mode: string;
|
||||
}>;
|
||||
}): Promise<{ accepted: number; rejected: number }> {
|
||||
return this.request<{ accepted: number; rejected: number }>(
|
||||
'POST', '/api/v1/telemetry/report', data,
|
||||
);
|
||||
}
|
||||
|
||||
/** Report audit log summary (action types and counts only, no content) */
|
||||
async reportAuditSummary(data: {
|
||||
device_id: string;
|
||||
entries: Array<{
|
||||
action: string;
|
||||
target: string;
|
||||
result: string;
|
||||
timestamp: string;
|
||||
}>;
|
||||
}): Promise<{ accepted: number; total: number }> {
|
||||
return this.request<{ accepted: number; total: number }>(
|
||||
'POST', '/api/v1/telemetry/audit', data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// === Singleton ===
|
||||
|
||||
Reference in New Issue
Block a user