feat :
- Added tunnel - Removed cloud feature
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { validateApiKey, getModelAliases, setModelAlias, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { validateApiKey, getModelAliases, setModelAlias } from "@/models";
|
||||
|
||||
// PUT /api/cloud/models/alias - Set model alias (for cloud/CLI)
|
||||
export async function PUT(request) {
|
||||
@@ -37,9 +35,6 @@ export async function PUT(request) {
|
||||
// Update alias
|
||||
await setModelAlias(alias, model);
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
model,
|
||||
@@ -52,21 +47,6 @@ export async function PUT(request) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing aliases to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/cloud/models/alias - Get all aliases
|
||||
export async function GET(request) {
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getComboById, updateCombo, deleteCombo, getComboByName, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { getComboById, updateCombo, deleteCombo, getComboByName } from "@/lib/localDb";
|
||||
|
||||
// Validate combo name: only a-z, A-Z, 0-9, -, _
|
||||
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
@@ -48,9 +46,6 @@ export async function PUT(request, { params }) {
|
||||
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json(combo);
|
||||
} catch (error) {
|
||||
console.log("Error updating combo:", error);
|
||||
@@ -67,9 +62,6 @@ export async function DELETE(request, { params }) {
|
||||
if (!success) {
|
||||
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
@@ -77,18 +69,3 @@ export async function DELETE(request, { params }) {
|
||||
return NextResponse.json({ error: "Failed to delete combo" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCombos, createCombo, getComboByName, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { getCombos, createCombo, getComboByName } from "@/lib/localDb";
|
||||
|
||||
// Validate combo name: only a-z, A-Z, 0-9, -, _
|
||||
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
@@ -40,27 +38,9 @@ export async function POST(request) {
|
||||
|
||||
const combo = await createCombo({ name, models: models || [] });
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json(combo, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error creating combo:", error);
|
||||
return NextResponse.json({ error: "Failed to create combo" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { deleteApiKey, getApiKeyById, updateApiKey, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { deleteApiKey, getApiKeyById, updateApiKey } from "@/lib/localDb";
|
||||
|
||||
// GET /api/keys/[id] - Get single key
|
||||
export async function GET(request, { params }) {
|
||||
@@ -34,7 +32,6 @@ export async function PUT(request, { params }) {
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
|
||||
const updated = await updateApiKey(id, updateData);
|
||||
await syncKeysToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ key: updated });
|
||||
} catch (error) {
|
||||
@@ -53,27 +50,9 @@ export async function DELETE(request, { params }) {
|
||||
return NextResponse.json({ error: "Key not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncKeysToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ message: "Key deleted successfully" });
|
||||
} catch (error) {
|
||||
console.log("Error deleting key:", error);
|
||||
return NextResponse.json({ error: "Failed to delete key" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync API keys to Cloud if enabled
|
||||
*/
|
||||
async function syncKeysToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing keys to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getApiKeys, createApiKey, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getApiKeys, createApiKey } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// GET /api/keys - List API keys
|
||||
export async function GET() {
|
||||
@@ -28,9 +27,6 @@ export async function POST(request) {
|
||||
const machineId = await getConsistentMachineId();
|
||||
const apiKey = await createApiKey(name, machineId);
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncKeysToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
key: apiKey.key,
|
||||
name: apiKey.name,
|
||||
@@ -42,18 +38,3 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: "Failed to create key" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync API keys to Cloud if enabled
|
||||
*/
|
||||
async function syncKeysToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing keys to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getModelAliases, setModelAlias, deleteModelAlias, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { getModelAliases, setModelAlias, deleteModelAlias } from "@/models";
|
||||
|
||||
// GET /api/models/alias - Get all aliases
|
||||
export async function GET() {
|
||||
@@ -25,7 +23,6 @@ export async function PUT(request) {
|
||||
}
|
||||
|
||||
await setModelAlias(alias, model);
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ success: true, model, alias });
|
||||
} catch (error) {
|
||||
@@ -45,7 +42,6 @@ export async function DELETE(request) {
|
||||
}
|
||||
|
||||
await deleteModelAlias(alias);
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
@@ -53,15 +49,3 @@ export async function DELETE(request) {
|
||||
return NextResponse.json({ error: "Failed to delete alias" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing aliases to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@ import {
|
||||
requestDeviceCode,
|
||||
pollForToken
|
||||
} from "@/lib/oauth/providers";
|
||||
import { createProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { createProviderConnection } from "@/models";
|
||||
|
||||
/**
|
||||
* Dynamic OAuth API Route
|
||||
@@ -89,9 +87,6 @@ export async function POST(request, { params }) {
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connection: {
|
||||
@@ -138,9 +133,6 @@ export async function POST(request, { params }) {
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connection: {
|
||||
@@ -167,18 +159,3 @@ export async function POST(request, { params }) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud after OAuth:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { CursorService } from "@/lib/oauth/services/cursor";
|
||||
import { createProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { createProviderConnection } from "@/models";
|
||||
|
||||
/**
|
||||
* POST /api/oauth/cursor/import
|
||||
@@ -58,9 +56,6 @@ export async function POST(request) {
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connection: {
|
||||
@@ -103,18 +98,3 @@ export async function GET() {
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud after Cursor import:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { KiroService } from "@/lib/oauth/services/kiro";
|
||||
import { createProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { createProviderConnection } from "@/models";
|
||||
|
||||
/**
|
||||
* POST /api/oauth/kiro/import
|
||||
@@ -43,9 +41,6 @@ export async function POST(request) {
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connection: {
|
||||
@@ -59,18 +54,3 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud after Kiro import:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { KiroService } from "@/lib/oauth/services/kiro";
|
||||
import { createProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { createProviderConnection } from "@/models";
|
||||
|
||||
/**
|
||||
* POST /api/oauth/kiro/social-exchange
|
||||
@@ -54,9 +52,6 @@ export async function POST(request) {
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
connection: {
|
||||
@@ -70,18 +65,3 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing to cloud after Kiro OAuth:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnectionById, updateProviderConnection, deleteProviderConnection, isCloudEnabled } from "@/models";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { getProviderConnectionById, updateProviderConnection, deleteProviderConnection } from "@/models";
|
||||
|
||||
// GET /api/providers/[id] - Get single connection
|
||||
export async function GET(request, { params }) {
|
||||
@@ -59,9 +57,6 @@ export async function PUT(request, { params }) {
|
||||
delete result.refreshToken;
|
||||
delete result.idToken;
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ connection: result });
|
||||
} catch (error) {
|
||||
console.log("Error updating connection:", error);
|
||||
@@ -79,27 +74,9 @@ export async function DELETE(request, { params }) {
|
||||
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ message: "Connection deleted successfully" });
|
||||
} catch (error) {
|
||||
console.log("Error deleting connection:", error);
|
||||
return NextResponse.json({ error: "Failed to delete connection" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing providers to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { getProviderConnectionById, updateProviderConnection, isCloudEnabled } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import {
|
||||
GEMINI_CONFIG,
|
||||
@@ -325,17 +323,5 @@ export async function testSingleConnection(id) {
|
||||
|
||||
await updateProviderConnection(id, updateData);
|
||||
|
||||
if (result.refreshed) {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (cloudEnabled) {
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Error syncing to cloud after token refresh:", err);
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: result.valid, error: result.error, latencyMs, testedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnections, createProviderConnection, getProviderNodeById, isCloudEnabled } from "@/models";
|
||||
import { getProviderConnections, createProviderConnection, getProviderNodeById } from "@/models";
|
||||
import { APIKEY_PROVIDERS } from "@/shared/constants/config";
|
||||
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
// GET /api/providers - List all connections
|
||||
export async function GET() {
|
||||
@@ -101,27 +99,9 @@ export async function POST(request) {
|
||||
const result = { ...newConnection };
|
||||
delete result.apiKey;
|
||||
|
||||
// Auto sync to Cloud if enabled
|
||||
await syncToCloudIfEnabled();
|
||||
|
||||
return NextResponse.json({ connection: result }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.log("Error creating provider:", error);
|
||||
return NextResponse.json({ error: "Failed to create provider" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to Cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const cloudEnabled = await isCloudEnabled();
|
||||
if (!cloudEnabled) return;
|
||||
|
||||
const machineId = await getConsistentMachineId();
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.log("Error syncing providers to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,318 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProviderConnections, getModelAliases, getCombos, getApiKeys, createApiKey, updateProviderConnection, updateSettings, getCloudUrl } from "@/lib/localDb";
|
||||
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
|
||||
const CLOUD_SYNC_TIMEOUT_MS = Number(process.env.CLOUD_SYNC_TIMEOUT_MS || 12000);
|
||||
|
||||
async function getResolvedCloudUrl() {
|
||||
return await getCloudUrl();
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = CLOUD_SYNC_TIMEOUT_MS) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
return await fetch(url, { ...options, signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
case "check":
|
||||
return handleCheck();
|
||||
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) {
|
||||
const cloudUrl = await getResolvedCloudUrl();
|
||||
if (!cloudUrl) {
|
||||
return { error: "Cloud URL is not configured" };
|
||||
}
|
||||
|
||||
// Get current data from db
|
||||
const providers = await getProviderConnections();
|
||||
const modelAliases = await getModelAliases();
|
||||
const combos = await getCombos();
|
||||
const apiKeys = await getApiKeys();
|
||||
|
||||
let response;
|
||||
try {
|
||||
// Send to Cloud
|
||||
response = await fetchWithTimeout(`${cloudUrl}/sync/${machineId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
providers,
|
||||
modelAliases,
|
||||
combos,
|
||||
apiKeys
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeout = error?.name === "AbortError";
|
||||
return { error: isTimeout ? "Cloud sync timeout" : "Cloud sync request failed" };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.log("Cloud sync failed:", errorText);
|
||||
return { error: "Cloud sync failed" };
|
||||
}
|
||||
|
||||
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 cloudUrl = await getResolvedCloudUrl();
|
||||
const pingResponse = await fetchWithTimeout(`${cloudUrl}/${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 cloudUrl = await getResolvedCloudUrl();
|
||||
if (!cloudUrl) {
|
||||
return NextResponse.json({ error: "Cloud URL is not configured" }, { status: 500 });
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetchWithTimeout(`${cloudUrl}/sync/${machineId}`, {
|
||||
method: "DELETE"
|
||||
});
|
||||
} catch (error) {
|
||||
const isTimeout = error?.name === "AbortError";
|
||||
return NextResponse.json(
|
||||
{ error: isTimeout ? "Cloud disable timeout" : "Failed to reach cloud service" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
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:20128";
|
||||
await updateClaudeSettingsToLocal(machineId, host, cloudUrl);
|
||||
|
||||
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, cloudUrl) {
|
||||
try {
|
||||
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
||||
const cloudEndpoint = `${cloudUrl}/${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 !== cloudEndpoint) {
|
||||
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: ${cloudEndpoint} → ${localUrl}`);
|
||||
} catch (error) {
|
||||
console.log("Failed to update Claude CLI settings:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cloud worker is reachable
|
||||
*/
|
||||
async function handleCheck() {
|
||||
const cloudUrl = await getResolvedCloudUrl();
|
||||
if (!cloudUrl) {
|
||||
return NextResponse.json({ error: "Cloud URL is not configured" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetchWithTimeout(`${cloudUrl}/health`, { method: "GET" }, 5000);
|
||||
if (res.ok) {
|
||||
return NextResponse.json({ success: true, message: "Worker is running" });
|
||||
}
|
||||
return NextResponse.json({ error: `Worker responded with ${res.status}` }, { status: 502 });
|
||||
} catch (error) {
|
||||
const isTimeout = error?.name === "AbortError";
|
||||
return NextResponse.json(
|
||||
{ error: isTimeout ? "Worker request timeout" : "Cannot reach worker" },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import initializeCloudSync from "@/shared/services/initializeCloudSync";
|
||||
|
||||
let syncInitialized = false;
|
||||
|
||||
// POST /api/sync/initialize - Initialize cloud sync scheduler
|
||||
export async function POST(request) {
|
||||
try {
|
||||
if (syncInitialized) {
|
||||
return NextResponse.json({
|
||||
message: "Cloud sync already initialized"
|
||||
});
|
||||
}
|
||||
|
||||
await initializeCloudSync();
|
||||
syncInitialized = true;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Cloud sync initialized successfully"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error initializing cloud sync:", error);
|
||||
return NextResponse.json({
|
||||
error: "Failed to initialize cloud sync"
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/sync/status - Check sync initialization status
|
||||
export async function GET(request) {
|
||||
return NextResponse.json({
|
||||
initialized: syncInitialized,
|
||||
message: syncInitialized ? "Cloud sync is running" : "Cloud sync not initialized"
|
||||
});
|
||||
}
|
||||
12
src/app/api/tunnel/disable/route.js
Normal file
12
src/app/api/tunnel/disable/route.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { disableTunnel } from "@/lib/tunnel/tunnelManager";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const result = await disableTunnel();
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error("Tunnel disable error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
12
src/app/api/tunnel/enable/route.js
Normal file
12
src/app/api/tunnel/enable/route.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { enableTunnel } from "@/lib/tunnel/tunnelManager";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const result = await enableTunnel();
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error("Tunnel enable error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
12
src/app/api/tunnel/status/route.js
Normal file
12
src/app/api/tunnel/status/route.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTunnelStatus } from "@/lib/tunnel/tunnelManager";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const status = await getTunnelStatus();
|
||||
return NextResponse.json(status);
|
||||
} catch (error) {
|
||||
console.error("Tunnel status error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,6 @@
|
||||
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
|
||||
import { getMachineId } from "@/shared/utils/machine";
|
||||
import { getUsageForProvider } from "open-sse/services/usage.js";
|
||||
import { getExecutor } from "open-sse/executors/index.js";
|
||||
import { syncToCloud } from "@/app/api/sync/cloud/route";
|
||||
|
||||
/**
|
||||
* Sync to cloud if enabled
|
||||
*/
|
||||
async function syncToCloudIfEnabled() {
|
||||
try {
|
||||
const machineId = await getMachineId();
|
||||
if (!machineId) return;
|
||||
await syncToCloud(machineId);
|
||||
} catch (error) {
|
||||
console.error("[Usage API] Error syncing to cloud:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh credentials using executor and update database
|
||||
* @returns {{ connection, refreshed: boolean }}
|
||||
@@ -119,16 +103,9 @@ export async function GET(request, { params }) {
|
||||
}
|
||||
|
||||
// Refresh credentials if needed using executor
|
||||
let refreshed = false;
|
||||
try {
|
||||
const result = await refreshAndUpdateCredentials(connection);
|
||||
connection = result.connection;
|
||||
refreshed = result.refreshed;
|
||||
|
||||
// Sync to cloud only if token was refreshed
|
||||
if (refreshed) {
|
||||
await syncToCloudIfEnabled();
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error("[Usage API] Credential refresh failed:", refreshError);
|
||||
return Response.json({
|
||||
|
||||
@@ -1,33 +1,53 @@
|
||||
import { getUsageStats, statsEmitter } from "@/lib/usageDb";
|
||||
import { getUsageStats, statsEmitter, getActiveRequests } from "@/lib/usageDb";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const encoder = new TextEncoder();
|
||||
const state = { closed: false, keepalive: null, send: null };
|
||||
const state = { closed: false, keepalive: null, send: null, sendPending: null, cachedStats: null };
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
// Full stats refresh (heavy) + immediate lightweight push
|
||||
state.send = async () => {
|
||||
if (state.closed) return;
|
||||
try {
|
||||
const stats = await getUsageStats();
|
||||
if (stats.activeRequests?.length > 0) {
|
||||
console.log(`[SSE] Push | active=${stats.activeRequests.length} | ${stats.activeRequests.map(r => r.provider).join(",")}`);
|
||||
// Push lightweight update immediately so UI reflects changes fast
|
||||
if (state.cachedStats) {
|
||||
const { activeRequests, recentRequests, errorProvider } = await getActiveRequests();
|
||||
const quickStats = { ...state.cachedStats, activeRequests, recentRequests, errorProvider };
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(quickStats)}\n\n`));
|
||||
}
|
||||
// Then do full recalc and update cache
|
||||
const stats = await getUsageStats();
|
||||
state.cachedStats = stats;
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(stats)}\n\n`));
|
||||
} catch {
|
||||
// Controller closed → self-cleanup
|
||||
state.closed = true;
|
||||
statsEmitter.off("update", state.send);
|
||||
statsEmitter.off("pending", state.sendPending);
|
||||
clearInterval(state.keepalive);
|
||||
}
|
||||
};
|
||||
|
||||
// Lightweight push: only refresh activeRequests + recentRequests on pending changes
|
||||
state.sendPending = async () => {
|
||||
if (state.closed || !state.cachedStats) return;
|
||||
try {
|
||||
const { activeRequests, recentRequests, errorProvider } = await getActiveRequests();
|
||||
const stats = { ...state.cachedStats, activeRequests, recentRequests, errorProvider };
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(stats)}\n\n`));
|
||||
} catch {
|
||||
state.closed = true;
|
||||
statsEmitter.off("pending", state.sendPending);
|
||||
}
|
||||
};
|
||||
|
||||
await state.send();
|
||||
console.log(`[SSE] Client connected | listeners=${statsEmitter.listenerCount("update") + 1}`);
|
||||
|
||||
statsEmitter.on("update", state.send);
|
||||
statsEmitter.on("pending", state.sendPending);
|
||||
|
||||
state.keepalive = setInterval(() => {
|
||||
if (state.closed) { clearInterval(state.keepalive); return; }
|
||||
@@ -43,6 +63,7 @@ export async function GET() {
|
||||
cancel() {
|
||||
state.closed = true;
|
||||
statsEmitter.off("update", state.send);
|
||||
statsEmitter.off("pending", state.sendPending);
|
||||
clearInterval(state.keepalive);
|
||||
console.log("[SSE] Client disconnected");
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user