mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-18 22:57:31 +08:00
241 lines
6.8 KiB
JavaScript
241 lines
6.8 KiB
JavaScript
import crypto from 'node:crypto';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { safeStorage } from 'electron';
|
|
|
|
function encryptSecret(secret) {
|
|
if (!safeStorage.isEncryptionAvailable()) {
|
|
return { encrypted: false, value: secret };
|
|
}
|
|
|
|
return {
|
|
encrypted: true,
|
|
value: safeStorage.encryptString(secret).toString('base64'),
|
|
};
|
|
}
|
|
|
|
function decryptSecret(record) {
|
|
if (!record?.value) return null;
|
|
if (!record.encrypted) return record.value;
|
|
return safeStorage.decryptString(Buffer.from(record.value, 'base64'));
|
|
}
|
|
|
|
export class CloudController {
|
|
constructor({ storePath, controlPlaneUrl, callbackUrl, onChange }) {
|
|
this.storePath = storePath;
|
|
this.controlPlaneUrl = controlPlaneUrl;
|
|
this.callbackUrl = callbackUrl;
|
|
this.onChange = onChange;
|
|
this.cloudAccount = null;
|
|
this.cloudEnvironments = [];
|
|
this.authState = 'logged_out';
|
|
}
|
|
|
|
getAccount() {
|
|
return this.cloudAccount;
|
|
}
|
|
|
|
getAuthState() {
|
|
return this.authState;
|
|
}
|
|
|
|
getEnvironments() {
|
|
return this.cloudEnvironments;
|
|
}
|
|
|
|
getEnvironmentUrl(environment) {
|
|
return environment.access_url || `https://${environment.subdomain}.cloudcli.ai`;
|
|
}
|
|
|
|
async getEnvironmentLaunchUrl(environment) {
|
|
if (!environment?.id) {
|
|
return this.getEnvironmentUrl(environment);
|
|
}
|
|
|
|
const data = await this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/launch`, {
|
|
method: 'POST',
|
|
});
|
|
|
|
return data.launch_url || data.environment_url || this.getEnvironmentUrl(environment);
|
|
}
|
|
|
|
findEnvironment(environmentId) {
|
|
return this.cloudEnvironments.find((item) => item.id === environmentId) || null;
|
|
}
|
|
|
|
async loadCloudAccount() {
|
|
try {
|
|
const raw = await fs.readFile(this.storePath, 'utf8');
|
|
const stored = JSON.parse(raw);
|
|
const apiKey = decryptSecret(stored.apiKey);
|
|
this.cloudAccount = {
|
|
deviceId: stored.deviceId || crypto.randomUUID(),
|
|
email: stored.email || null,
|
|
apiKey: apiKey || null,
|
|
};
|
|
this.authState = apiKey ? 'connected' : (stored.email ? 'expired' : 'logged_out');
|
|
return this.cloudAccount;
|
|
} catch {
|
|
this.cloudAccount = {
|
|
deviceId: crypto.randomUUID(),
|
|
email: null,
|
|
apiKey: null,
|
|
};
|
|
this.authState = 'logged_out';
|
|
return this.cloudAccount;
|
|
}
|
|
}
|
|
|
|
async saveCloudAccount(account) {
|
|
const payload = {
|
|
deviceId: account.deviceId || crypto.randomUUID(),
|
|
email: account.email || null,
|
|
apiKey: account.apiKey ? encryptSecret(account.apiKey) : null,
|
|
};
|
|
|
|
await fs.mkdir(path.dirname(this.storePath), { recursive: true });
|
|
await fs.writeFile(this.storePath, JSON.stringify(payload, null, 2), 'utf8');
|
|
this.cloudAccount = {
|
|
deviceId: payload.deviceId,
|
|
email: payload.email,
|
|
apiKey: account.apiKey || null,
|
|
};
|
|
this.authState = account.apiKey ? 'connected' : 'logged_out';
|
|
this.onChange?.();
|
|
return this.cloudAccount;
|
|
}
|
|
|
|
async clearCloudAccount() {
|
|
this.cloudAccount = {
|
|
deviceId: crypto.randomUUID(),
|
|
email: null,
|
|
apiKey: null,
|
|
};
|
|
this.cloudEnvironments = [];
|
|
this.authState = 'logged_out';
|
|
await fs.rm(this.storePath, { force: true });
|
|
this.onChange?.();
|
|
}
|
|
|
|
async invalidateCloudAccount() {
|
|
this.cloudEnvironments = [];
|
|
if (!this.cloudAccount) {
|
|
this.cloudAccount = {
|
|
deviceId: crypto.randomUUID(),
|
|
email: null,
|
|
apiKey: null,
|
|
};
|
|
} else {
|
|
this.cloudAccount = {
|
|
...this.cloudAccount,
|
|
apiKey: null,
|
|
};
|
|
}
|
|
this.authState = this.cloudAccount.email ? 'expired' : 'logged_out';
|
|
const payload = {
|
|
deviceId: this.cloudAccount.deviceId,
|
|
email: this.cloudAccount.email || null,
|
|
apiKey: null,
|
|
};
|
|
await fs.mkdir(path.dirname(this.storePath), { recursive: true });
|
|
await fs.writeFile(this.storePath, JSON.stringify(payload, null, 2), 'utf8');
|
|
this.onChange?.();
|
|
}
|
|
|
|
async cloudApi(pathname, options = {}) {
|
|
if (!this.cloudAccount?.apiKey) {
|
|
throw new Error('Connect your CloudCLI account first.');
|
|
}
|
|
|
|
const response = await fetch(`${this.controlPlaneUrl}${pathname}`, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': this.cloudAccount.apiKey,
|
|
...(options.headers || {}),
|
|
},
|
|
});
|
|
|
|
const body = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
if (response.status === 401 || response.status === 403) {
|
|
await this.invalidateCloudAccount();
|
|
}
|
|
throw new Error(body.error || `CloudCLI API request failed: ${response.status}`);
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
async refreshCloudEnvironments() {
|
|
if (!this.cloudAccount?.apiKey) {
|
|
this.cloudEnvironments = [];
|
|
this.onChange?.();
|
|
return [];
|
|
}
|
|
|
|
const data = await this.cloudApi('/api/v1/environments');
|
|
this.cloudEnvironments = data.environments || [];
|
|
this.onChange?.();
|
|
return this.cloudEnvironments;
|
|
}
|
|
|
|
async startEnvironment(environment) {
|
|
await this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/start`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async stopEnvironment(environment) {
|
|
await this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/stop`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async getEnvironmentCredentials(environment) {
|
|
return this.cloudApi(`/api/v1/environments/${encodeURIComponent(environment.id)}/credentials`);
|
|
}
|
|
|
|
async startEnvironmentAndWait(environment, timeoutMs) {
|
|
await this.startEnvironment(environment);
|
|
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
const environments = await this.refreshCloudEnvironments();
|
|
const current = environments.find((env) => env.id === environment.id);
|
|
if (current?.status === 'running') {
|
|
return current;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
}
|
|
|
|
throw new Error(`${environment.name} did not become ready in time.`);
|
|
}
|
|
|
|
buildConnectUrl() {
|
|
if (!this.cloudAccount?.deviceId) {
|
|
this.cloudAccount = {
|
|
deviceId: crypto.randomUUID(),
|
|
email: null,
|
|
apiKey: null,
|
|
};
|
|
}
|
|
|
|
const connectUrl = new URL('/auth/app-connect', this.controlPlaneUrl);
|
|
connectUrl.searchParams.set('device_id', this.cloudAccount.deviceId);
|
|
connectUrl.searchParams.set('callback_url', this.callbackUrl);
|
|
connectUrl.searchParams.set('app_surface', 'cloudcli_desktop');
|
|
connectUrl.searchParams.set('client_platform', 'desktop');
|
|
return connectUrl.toString();
|
|
}
|
|
|
|
async saveFromCallback({ apiKey, email }) {
|
|
await this.saveCloudAccount({
|
|
deviceId: this.cloudAccount?.deviceId || crypto.randomUUID(),
|
|
email,
|
|
apiKey,
|
|
});
|
|
return this.cloudAccount;
|
|
}
|
|
}
|