mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
256 lines
7.6 KiB
JavaScript
256 lines
7.6 KiB
JavaScript
import { NextResponse } from "next/server";
|
|
import { getProviderConnections, getModelAliases, getCombos, getApiKeys, createApiKey, updateProviderConnection, updateSettings } from "@/lib/localDb";
|
|
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import os from "os";
|
|
|
|
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
|
|
|
/**
|
|
* POST /api/sync/cloud
|
|
* Sync data with Cloud
|
|
*/
|
|
export async function POST(request) {
|
|
try {
|
|
const body = await request.json();
|
|
const { action } = body;
|
|
|
|
// Always get machineId from server, don't trust client
|
|
const machineId = await getConsistentMachineId();
|
|
|
|
switch (action) {
|
|
case "enable":
|
|
await updateSettings({ cloudEnabled: true });
|
|
// Auto create key if none exists
|
|
const keys = await getApiKeys();
|
|
let createdKey = null;
|
|
if (keys.length === 0) {
|
|
createdKey = await createApiKey("Default Key", machineId);
|
|
}
|
|
return syncAndVerify(machineId, createdKey?.key, keys);
|
|
case "sync": {
|
|
const syncResult = await syncToCloud(machineId);
|
|
if (syncResult.error) {
|
|
return NextResponse.json(syncResult, { status: 502 });
|
|
}
|
|
return NextResponse.json(syncResult);
|
|
}
|
|
case "disable":
|
|
await updateSettings({ cloudEnabled: false });
|
|
return handleDisable(machineId, request);
|
|
default:
|
|
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
|
|
}
|
|
} catch (error) {
|
|
console.log("Cloud sync error:", error);
|
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync data to Cloud (exported for reuse)
|
|
* @param {string} machineId
|
|
* @param {string|null} createdKey - Key created during enable
|
|
*/
|
|
export async function syncToCloud(machineId, createdKey = null) {
|
|
// Get current data from db
|
|
const providers = await getProviderConnections();
|
|
const modelAliases = await getModelAliases();
|
|
const combos = await getCombos();
|
|
const apiKeys = await getApiKeys();
|
|
|
|
// Send to Cloud
|
|
const response = await fetch(`${CLOUD_URL}/sync/${machineId}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
providers,
|
|
modelAliases,
|
|
combos,
|
|
apiKeys
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.log("Cloud sync failed:", errorText);
|
|
return NextResponse.json({ error: "Cloud sync failed" }, { status: 502 });
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Update local db with tokens from Cloud (providers stored by ID)
|
|
if (result.data && result.data.providers) {
|
|
await updateLocalTokens(result.data.providers);
|
|
}
|
|
|
|
const responseData = {
|
|
success: true,
|
|
message: "Synced successfully",
|
|
changes: result.changes
|
|
};
|
|
|
|
if (createdKey) {
|
|
responseData.createdKey = createdKey;
|
|
}
|
|
|
|
return responseData;
|
|
}
|
|
|
|
/**
|
|
* Sync and verify connection with ping
|
|
*/
|
|
async function syncAndVerify(machineId, createdKey, existingKeys) {
|
|
// Step 1: Sync data to cloud
|
|
const syncResult = await syncToCloud(machineId, createdKey);
|
|
if (syncResult.error) {
|
|
return NextResponse.json(syncResult, { status: 502 });
|
|
}
|
|
|
|
// Step 2: Verify connection by pinging the cloud
|
|
const apiKey = createdKey || existingKeys[0]?.key;
|
|
if (!apiKey) {
|
|
return NextResponse.json({
|
|
...syncResult,
|
|
verified: false,
|
|
verifyError: "No API key available"
|
|
});
|
|
}
|
|
|
|
try {
|
|
const pingResponse = await fetch(`${CLOUD_URL}/${machineId}/v1/verify`, {
|
|
method: "GET",
|
|
headers: {
|
|
"Authorization": `Bearer ${apiKey}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
});
|
|
|
|
if (pingResponse.ok) {
|
|
return NextResponse.json({
|
|
...syncResult,
|
|
verified: true
|
|
});
|
|
} else {
|
|
return NextResponse.json({
|
|
...syncResult,
|
|
verified: false,
|
|
verifyError: `Ping failed: ${pingResponse.status}`
|
|
});
|
|
}
|
|
} catch (error) {
|
|
return NextResponse.json({
|
|
...syncResult,
|
|
verified: false,
|
|
verifyError: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable Cloud - delete cache and update Claude CLI settings
|
|
*/
|
|
async function handleDisable(machineId, request) {
|
|
const response = await fetch(`${CLOUD_URL}/sync/${machineId}`, {
|
|
method: "DELETE"
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.log("Cloud disable failed:", errorText);
|
|
return NextResponse.json({ error: "Failed to disable cloud" }, { status: 502 });
|
|
}
|
|
|
|
// Update Claude CLI settings to use local endpoint
|
|
const host = request.headers.get("host") || "localhost:3000";
|
|
await updateClaudeSettingsToLocal(machineId, host);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: "Cloud disabled"
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update Claude CLI settings to use local endpoint (only if currently using cloud)
|
|
*/
|
|
async function updateClaudeSettingsToLocal(machineId, host) {
|
|
try {
|
|
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
const cloudUrl = `${CLOUD_URL}/${machineId}`;
|
|
const localUrl = `http://${host}`;
|
|
|
|
// Read current settings
|
|
let settings;
|
|
try {
|
|
const content = await fs.readFile(settingsPath, "utf-8");
|
|
settings = JSON.parse(content);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") {
|
|
return; // No settings file, nothing to update
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
// Check if ANTHROPIC_BASE_URL matches cloud URL
|
|
const currentUrl = settings.env?.ANTHROPIC_BASE_URL;
|
|
if (!currentUrl || currentUrl !== cloudUrl) {
|
|
return; // Not using cloud URL, don't modify
|
|
}
|
|
|
|
// Update to local URL
|
|
settings.env.ANTHROPIC_BASE_URL = localUrl;
|
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
console.log(`Updated Claude CLI settings: ${cloudUrl} → ${localUrl}`);
|
|
} catch (error) {
|
|
console.log("Failed to update Claude CLI settings:", error.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update local db with data from Cloud
|
|
* Simple logic: if Cloud is newer, sync entire provider
|
|
* cloudProviders is object keyed by provider ID
|
|
*/
|
|
async function updateLocalTokens(cloudProviders) {
|
|
const localProviders = await getProviderConnections();
|
|
|
|
for (const localProvider of localProviders) {
|
|
const cloudProvider = cloudProviders[localProvider.id];
|
|
if (!cloudProvider) continue;
|
|
|
|
const cloudUpdatedAt = new Date(cloudProvider.updatedAt || 0).getTime();
|
|
const localUpdatedAt = new Date(localProvider.updatedAt || 0).getTime();
|
|
|
|
// Simple logic: if Cloud is newer, sync entire provider
|
|
if (cloudUpdatedAt > localUpdatedAt) {
|
|
const updates = {
|
|
// Tokens
|
|
accessToken: cloudProvider.accessToken,
|
|
refreshToken: cloudProvider.refreshToken,
|
|
expiresAt: cloudProvider.expiresAt,
|
|
expiresIn: cloudProvider.expiresIn,
|
|
|
|
// Provider specific data
|
|
providerSpecificData: cloudProvider.providerSpecificData || localProvider.providerSpecificData,
|
|
|
|
// Status fields
|
|
testStatus: cloudProvider.status || "active",
|
|
lastError: cloudProvider.lastError,
|
|
lastErrorAt: cloudProvider.lastErrorAt,
|
|
errorCode: cloudProvider.errorCode,
|
|
rateLimitedUntil: cloudProvider.rateLimitedUntil,
|
|
|
|
// Metadata
|
|
updatedAt: cloudProvider.updatedAt
|
|
};
|
|
|
|
await updateProviderConnection(localProvider.id, updates);
|
|
console.log(`Updated ${localProvider.provider} (${localProvider.id}) from Cloud (newer: ${new Date(cloudUpdatedAt).toISOString()})`);
|
|
} else {
|
|
console.log(`Skipped ${localProvider.provider} (${localProvider.id}) - Local is newer or equal`);
|
|
}
|
|
}
|
|
}
|