refactor(desktop): split kernel_commands/pipeline_commands into modules, add SaaS client libs and gateway modules
Split monolithic kernel_commands.rs (2185 lines) and pipeline_commands.rs (1391 lines) into focused sub-modules under kernel_commands/ and pipeline_commands/ directories. Add gateway module (commands, config, io, runtime), health_check, and 15 new TypeScript client libraries for SaaS relay, auth, admin, telemetry, and kernel sub-systems (a2a, agent, chat, hands, skills, triggers). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
131
desktop/src/lib/saas-relay.ts
Normal file
131
desktop/src/lib/saas-relay.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* SaaS Relay Methods — Mixin
|
||||
*
|
||||
* Installs relay-related methods (tasks, chat completion, usage) onto
|
||||
* SaaSClient.prototype. Uses the same mixin pattern as gateway-api.ts.
|
||||
*/
|
||||
|
||||
import type {
|
||||
RelayTaskInfo,
|
||||
UsageStats,
|
||||
} from './saas-types';
|
||||
import { createLogger } from './logger';
|
||||
const logger = createLogger('SaaSRelay');
|
||||
|
||||
export function installRelayMethods(ClientClass: { prototype: any }): void {
|
||||
const proto = ClientClass.prototype;
|
||||
|
||||
// --- Relay Task Management ---
|
||||
|
||||
/** List relay tasks for the current user */
|
||||
proto.listRelayTasks = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, 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 */
|
||||
proto.getRelayTask = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, taskId: string): Promise<RelayTaskInfo> {
|
||||
return this.request<RelayTaskInfo>('GET', `/api/v1/relay/tasks/${taskId}`);
|
||||
};
|
||||
|
||||
/** Retry a failed relay task (admin only) */
|
||||
proto.retryRelayTask = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, 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 ---
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
proto.chatCompletion = async function (
|
||||
this: {
|
||||
baseUrl: string;
|
||||
token: string | null;
|
||||
_serverReachable: boolean;
|
||||
_isAuthEndpoint(path: string): boolean;
|
||||
refreshToken(): Promise<string>;
|
||||
},
|
||||
body: unknown,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Response> {
|
||||
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,
|
||||
credentials: 'include', // Send HttpOnly cookies
|
||||
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 (e) {
|
||||
logger.debug('Token refresh failed', { error: e });
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Unreachable but TypeScript needs it
|
||||
throw new Error('chatCompletion: all attempts exhausted');
|
||||
};
|
||||
|
||||
// --- Usage Statistics ---
|
||||
|
||||
/** Get usage statistics for current account */
|
||||
proto.getUsage = async function (this: { request<T>(method: string, path: string, body?: unknown): Promise<T> }, 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 : ''}`);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user