chore: add buildOutput RTK filter, drop legacy cloud sync, internal cleanup

- feat(rtk): buildOutput filter + autodetect for npm/yarn/cargo logs
- chore: remove unused cloud sync module and related routes
- ui: hide deprecated providers (qwen, iflow, antigravity)
- chore: minor tray/cli/internal adjustments
This commit is contained in:
decolua 2026-05-16 10:54:41 +07:00
parent 21ea744c72
commit 3cca2252a6
26 changed files with 871 additions and 535 deletions

View file

@ -273,19 +273,21 @@ export default function ProvidersPage() {
}))
.filter((p) => matchSearch(p.name));
const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter(([, info]) =>
matchSearch(info.name),
const oauthEntries = Object.entries(OAUTH_PROVIDERS).filter(
([, info]) => !info.hidden && matchSearch(info.name),
);
const freeEntries = Object.entries(FREE_PROVIDERS).filter(([, info]) =>
matchSearch(info.name),
const freeEntries = Object.entries(FREE_PROVIDERS).filter(
([, info]) => !info.hidden && matchSearch(info.name),
);
const freeTierEntries = Object.entries(FREE_TIER_PROVIDERS).filter(
([, info]) => matchSearch(info.name),
([, info]) => !info.hidden && matchSearch(info.name),
);
const apikeyEntries = sortByPriority(
Object.entries(APIKEY_PROVIDERS).filter(
([, info]) =>
(info.serviceKinds ?? ["llm"]).includes("llm") && matchSearch(info.name),
!info.hidden &&
(info.serviceKinds ?? ["llm"]).includes("llm") &&
matchSearch(info.name),
),
"apikey",
);

View file

@ -1,50 +0,0 @@
import { NextResponse } from "next/server";
import { validateApiKey, getProviderConnections, getModelAliases } from "@/models";
// Verify API key and return provider credentials
export async function POST(request) {
try {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const apiKey = authHeader.slice(7);
// Validate API key
const isValid = await validateApiKey(apiKey);
if (!isValid) {
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
}
// Get active provider connections
const connections = await getProviderConnections({ isActive: true });
// Map connections
const mappedConnections = connections.map(conn => ({
provider: conn.provider,
authType: conn.authType,
apiKey: conn.apiKey || null,
accessToken: conn.accessToken || null,
refreshToken: conn.refreshToken || null,
projectId: conn.projectId || null,
expiresAt: conn.expiresAt,
priority: conn.priority,
globalPriority: conn.globalPriority,
defaultModel: conn.defaultModel,
isActive: conn.isActive
}));
// Get model aliases
const modelAliases = await getModelAliases();
return NextResponse.json({
connections: mappedConnections,
modelAliases
});
} catch (error) {
console.log("Cloud auth error:", error);
return NextResponse.json({ error: "Internal error" }, { status: 500 });
}
}

View file

@ -1,57 +0,0 @@
import { NextResponse } from "next/server";
import { validateApiKey, getProviderConnections, updateProviderConnection } from "@/models";
// Update provider credentials (for cloud token refresh)
export async function PUT(request) {
try {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const apiKey = authHeader.slice(7);
const body = await request.json();
const { provider, credentials } = body;
if (!provider || !credentials) {
return NextResponse.json({ error: "Provider and credentials required" }, { status: 400 });
}
// Validate API key
const isValid = await validateApiKey(apiKey);
if (!isValid) {
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
}
// Find active connection for provider
const connections = await getProviderConnections({ provider, isActive: true });
const connection = connections[0];
if (!connection) {
return NextResponse.json({ error: `No active connection found for provider: ${provider}` }, { status: 404 });
}
// Update credentials
const updateData = {};
if (credentials.accessToken) {
updateData.accessToken = credentials.accessToken;
}
if (credentials.refreshToken) {
updateData.refreshToken = credentials.refreshToken;
}
if (credentials.expiresIn) {
updateData.expiresAt = new Date(Date.now() + credentials.expiresIn * 1000).toISOString();
}
await updateProviderConnection(connection.id, updateData);
return NextResponse.json({
success: true,
message: `Credentials updated for provider: ${provider}`
});
} catch (error) {
console.log("Update credentials error:", error);
return NextResponse.json({ error: "Failed to update credentials" }, { status: 500 });
}
}

View file

@ -1,50 +0,0 @@
import { NextResponse } from "next/server";
import { validateApiKey, getModelAliases } from "@/models";
// Resolve model alias to provider/model
export async function POST(request) {
try {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const apiKey = authHeader.slice(7);
const body = await request.json();
const { alias } = body;
if (!alias) {
return NextResponse.json({ error: "Missing alias" }, { status: 400 });
}
// Validate API key
const isValid = await validateApiKey(apiKey);
if (!isValid) {
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
}
// Get model aliases
const modelAliases = await getModelAliases();
const resolved = modelAliases[alias];
if (resolved) {
// Parse provider/model
const firstSlash = resolved.indexOf("/");
if (firstSlash > 0) {
return NextResponse.json({
alias,
provider: resolved.slice(0, firstSlash),
model: resolved.slice(firstSlash + 1)
});
}
}
// Not found
return NextResponse.json({ error: "Alias not found" }, { status: 404 });
} catch (error) {
console.log("Model resolve error:", error);
return NextResponse.json({ error: "Internal error" }, { status: 500 });
}
}

View file

@ -1,72 +0,0 @@
import { NextResponse } from "next/server";
import { validateApiKey, getModelAliases, setModelAlias } from "@/models";
// PUT /api/cloud/models/alias - Set model alias (for cloud/CLI)
export async function PUT(request) {
try {
const authHeader = request.headers.get("authorization");
const apiKey = authHeader?.replace("Bearer ", "");
if (!apiKey) {
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const isValid = await validateApiKey(apiKey);
if (!isValid) {
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
}
const body = await request.json();
const { model, alias } = body;
if (!model || !alias) {
return NextResponse.json({ error: "Model and alias required" }, { status: 400 });
}
// Check if alias already exists for different model
const aliases = await getModelAliases();
const existingModel = aliases[alias];
if (existingModel && existingModel !== model) {
return NextResponse.json({
error: `Alias '${alias}' already in use for model '${existingModel}'`
}, { status: 400 });
}
// Update alias
await setModelAlias(alias, model);
return NextResponse.json({
success: true,
model,
alias,
message: `Alias '${alias}' set for model '${model}'`
});
} catch (error) {
console.log("Error updating alias:", error);
return NextResponse.json({ error: "Failed to update alias" }, { status: 500 });
}
}
// GET /api/cloud/models/alias - Get all aliases
export async function GET(request) {
try {
const authHeader = request.headers.get("authorization");
const apiKey = authHeader?.replace("Bearer ", "");
if (!apiKey) {
return NextResponse.json({ error: "Missing API key" }, { status: 401 });
}
const isValid = await validateApiKey(apiKey);
if (!isValid) {
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
}
const aliases = await getModelAliases();
return NextResponse.json({ aliases });
} catch (error) {
console.log("Error fetching aliases:", error);
return NextResponse.json({ error: "Failed to fetch aliases" }, { status: 500 });
}
}

View file

@ -1,7 +1,4 @@
// Auto-initialize cloud sync when server starts
import "@/lib/initCloudSync";
// This API route is called automatically to initialize sync
// This API route is called automatically to initialize app
export async function GET() {
return new Response("Initialized", { status: 200 });
}

View file

@ -1,6 +1,9 @@
import { NextResponse } from "next/server";
import { getApiKeys } from "@/lib/localDb";
import { UPDATER_CONFIG } from "@/shared/constants/config";
import { getConsistentMachineId } from "@/shared/utils/machineId";
const CLI_TOKEN_SALT = "9r-cli-auth";
// POST /api/models/test - Ping a single model via internal completions or embeddings
export async function POST(request) {
@ -19,6 +22,8 @@ export async function POST(request) {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
// Bypass dashboardGuard for internal self-call via CLI token (machineId-based)
headers["x-9r-cli-token"] = await getConsistentMachineId(CLI_TOKEN_SALT);
const start = Date.now();

View file

@ -3,6 +3,9 @@ import { getProviderConnectionById, getApiKeys } from "@/lib/localDb";
import { getProviderModels, PROVIDER_ID_TO_ALIAS } from "open-sse/config/providerModels.js";
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import { UPDATER_CONFIG } from "@/shared/constants/config";
import { getConsistentMachineId } from "@/shared/utils/machineId";
const CLI_TOKEN_SALT = "9r-cli-auth";
/**
* Get an active API key to pass through auth when requireApiKey is enabled.
@ -16,11 +19,12 @@ async function getInternalApiKey() {
* Ping a single model via internal completions endpoint (OpenAI format).
* open-sse handles all provider translation automatically.
*/
async function pingModel(modelId, baseUrl, apiKey) {
async function pingModel(modelId, baseUrl, apiKey, cliToken) {
const start = Date.now();
try {
const headers = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
if (cliToken) headers["x-9r-cli-token"] = cliToken;
const res = await fetch(`${baseUrl}/api/v1/chat/completions`, {
method: "POST",
headers,
@ -83,17 +87,19 @@ export async function POST(request, { params }) {
}
const apiKey = await getInternalApiKey();
// Bypass dashboardGuard for internal self-call via CLI token (machineId-based)
const cliToken = await getConsistentMachineId(CLI_TOKEN_SALT);
// Warm up with first model to trigger token refresh (if needed) before parallel calls.
// This prevents race condition where multiple requests concurrently refresh the same token.
const [first, ...rest] = models;
const firstResult = await pingModel(`${alias}/${first.id}`, baseUrl, apiKey);
const firstResult = await pingModel(`${alias}/${first.id}`, baseUrl, apiKey, cliToken);
const results = [{ modelId: first.id, name: first.name || first.id, ...firstResult }];
if (rest.length > 0) {
const restResults = await Promise.all(
rest.map(async (model) => {
const result = await pingModel(`${alias}/${model.id}`, baseUrl, apiKey);
const result = await pingModel(`${alias}/${model.id}`, baseUrl, apiKey, cliToken);
return { modelId: model.id, name: model.name || model.id, ...result };
})
);

View file

@ -2,19 +2,51 @@ import { NextResponse } from "next/server";
import { getProviderConnections } from "@/lib/localDb";
import { backfillCodexEmails } from "@/lib/oauth/providers";
// GET /api/providers/client - List all connections for client (includes sensitive fields for sync)
// Whitelist: only safe metadata fields exposed to UI
const SAFE_FIELDS = [
"id", "provider", "authType", "name", "email", "displayName",
"priority", "globalPriority", "isActive", "defaultModel",
"testStatus", "lastError", "lastErrorAt", "errorCode",
"expiresAt", "lastUsedAt", "consecutiveUseCount",
"createdAt", "updatedAt",
];
// providerSpecificData fields safe to expose (non-secret config only)
const SAFE_PSD_FIELDS = [
"baseUrl", "azureEndpoint", "deployment", "apiVersion", "accountId",
"region", "projectId", "resourceUrl", "proxyPoolId",
"connectionProxyEnabled", "connectionProxyUrl", "connectionNoProxy",
"githubLogin", "githubName", "githubEmail", "githubUserId",
"username", "firstName", "lastName", "authMethod", "authKind",
];
function maskName(name) {
if (typeof name !== "string" || name.length <= 16) return name;
// Names like "hahask-uDUOg90..." may embed API keys — mask if looks like key
if (/[a-zA-Z0-9_-]{32,}/.test(name)) return `${name.slice(0, 8)}***`;
return name;
}
function sanitize(c) {
const safe = {};
for (const f of SAFE_FIELDS) if (c[f] !== undefined) safe[f] = c[f];
if (safe.name) safe.name = maskName(safe.name);
if (c.providerSpecificData) {
const psd = {};
for (const f of SAFE_PSD_FIELDS) {
if (c.providerSpecificData[f] !== undefined) psd[f] = c.providerSpecificData[f];
}
safe.providerSpecificData = psd;
}
return safe;
}
// GET /api/providers/client - List connections for dashboard UI (whitelist only)
export async function GET() {
try {
await backfillCodexEmails();
const connections = await getProviderConnections();
// Include sensitive fields for sync to cloud (only accessible from same origin)
const clientConnections = connections.map(c => ({
...c,
// Don't hide sensitive fields here since this is for internal sync
}));
return NextResponse.json({ connections: clientConnections });
return NextResponse.json({ connections: connections.map(sanitize) });
} catch (error) {
console.log("Error fetching providers for client:", error);
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });

View file

@ -1,4 +1,3 @@
import { callCloudWithMachineId } from "@/shared/utils/cloud.js";
import { handleChat } from "@/sse/handlers/chat.js";
import { initTranslators } from "open-sse/translator/index.js";

View file

@ -2,7 +2,6 @@ import { Inter } from "next/font/google";
import "material-symbols/outlined.css";
import "./globals.css";
import { ThemeProvider } from "@/shared/components/ThemeProvider";
import "@/lib/initCloudSync"; // Auto-initialize cloud sync
import "@/lib/network/initOutboundProxy"; // Auto-initialize outbound proxy env
import { initConsoleLogCapture } from "@/lib/consoleLogBuffer";
import { RuntimeI18nProvider } from "@/i18n/RuntimeI18nProvider";

View file

@ -1,5 +1,3 @@
// Auto-initialize cloud sync when server starts
import "@/lib/initCloudSync";
import { redirect } from "next/navigation";
export default function InitPage() {

View file

@ -68,8 +68,16 @@ const PROTECTED_API_PATHS = [
// Routes that spawn child processes or read host secrets — restrict to localhost.
const LOCAL_ONLY_PATHS = [
"/api/cli-tools/cowork-settings",
"/api/cli-tools/antigravity-mitm",
"/api/mcp/",
"/api/tunnel/tailscale-install",
"/api/tunnel/tailscale-enable",
"/api/tunnel/tailscale-disable",
"/api/tunnel/tailscale-login",
"/api/tunnel/tailscale-start-daemon",
"/api/tunnel/tailscale-check",
"/api/tunnel/enable",
"/api/tunnel/disable",
"/api/oauth/cursor/auto-import",
"/api/oauth/kiro/auto-import",
];

View file

@ -1,31 +0,0 @@
import initializeApp from "@/shared/services/initializeApp";
// Survive Next.js HMR — module-level flag resets on reload, globalThis persists
const g = globalThis.__cloudSyncInit ??= { initialized: false, inProgress: null };
export async function ensureAppInitialized() {
if (g.initialized) return true;
if (g.inProgress) return g.inProgress;
g.inProgress = (async () => {
try {
await initializeApp();
g.initialized = true;
} catch (error) {
console.error("[ServerInit] Error initializing app:", error);
} finally {
g.inProgress = null;
}
return g.initialized;
})();
return g.inProgress;
}
// Auto-initialize at runtime only, not during next build.
// Defer to next tick so HTTP server can accept connections before heavy init runs.
if (process.env.NEXT_PHASE !== "phase-production-build") {
setImmediate(() => {
ensureAppInitialized().catch(console.log);
});
}
export default ensureAppInitialized;

View file

@ -1,14 +1,16 @@
// Provider definitions
const RISK_NOTICE = "⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.";
// Free Providers (kiro first, iflow last)
export const FREE_PROVIDERS = {
kiro: { id: "kiro", alias: "kr", name: "Kiro AI", icon: "psychology_alt", color: "#FF6B35", website: "https://kiro.dev", notice: { signupUrl: "https://kiro.dev" } },
qwen: { id: "qwen", alias: "qw", name: "Qwen Code", icon: "psychology", color: "#10B981", mediaPriority: 999, deprecated: true, deprecationNotice: "Qwen OAuth free tier was discontinued by Alibaba on 2026-04-15. New connections will not work.", website: "https://chat.qwen.ai", notice: { signupUrl: "https://chat.qwen.ai" }, serviceKinds: ["llm", "tts"], ttsConfig: { baseUrl: "http://localhost:8000/v1/audio/speech", authType: "none", authHeader: "none", format: "openai", models: [{ id: "qwen3-tts", name: "Qwen3 TTS" }] } },
"gemini-cli": { id: "gemini-cli", alias: "gc", name: "Gemini CLI", icon: "terminal", color: "#4285F4", deprecated: true, deprecationNotice: "Gemini CLI is designed exclusively for Gemini CLI. Using it with other tools (OpenClaw, Claude, Codex...) may result in account restrictions or bans.", website: "https://github.com/google-gemini/gemini-cli", notice: { signupUrl: "https://github.com/google-gemini/gemini-cli" } },
qwen: { id: "qwen", alias: "qw", name: "Qwen Code", icon: "psychology", color: "#10B981", mediaPriority: 999, hidden: true, deprecated: true, deprecationNotice: "Qwen OAuth free tier was discontinued by Alibaba on 2026-04-15. New connections will not work.", website: "https://chat.qwen.ai", notice: { signupUrl: "https://chat.qwen.ai" }, serviceKinds: ["llm", "tts"], ttsConfig: { baseUrl: "http://localhost:8000/v1/audio/speech", authType: "none", authHeader: "none", format: "openai", models: [{ id: "qwen3-tts", name: "Qwen3 TTS" }] } },
"gemini-cli": { id: "gemini-cli", alias: "gc", name: "Gemini CLI", icon: "terminal", color: "#4285F4", deprecated: true, deprecationNotice: RISK_NOTICE, website: "https://github.com/google-gemini/gemini-cli", notice: { signupUrl: "https://github.com/google-gemini/gemini-cli" } },
// gitlab: { id: "gitlab", alias: "gl", name: "GitLab Duo", icon: "code", color: "#FC6D26" },
// codebuddy: { id: "codebuddy", alias: "cb", name: "CodeBuddy", icon: "smart_toy", color: "#006EFF" },
// qoder: { id: "qoder", alias: "qd", name: "Qoder AI", icon: "water_drop", color: "#EC4899" },
iflow: { id: "iflow", alias: "if", name: "iFlow AI", icon: "water_drop", color: "#6366F1", website: "https://iflow.cn", notice: { signupUrl: "https://iflow.cn" } },
iflow: { id: "iflow", alias: "if", name: "iFlow AI", icon: "water_drop", color: "#6366F1", hidden: true, website: "https://iflow.cn", notice: { signupUrl: "https://iflow.cn" } },
opencode: { id: "opencode", alias: "oc", name: "OpenCode Free", icon: "terminal", color: "#E87040", textIcon: "OC", noAuth: true, passthroughModels: true, modelsFetcher: { url: "https://opencode.ai/zen/v1/models", type: "opencode-free" } },
};
@ -53,10 +55,10 @@ const MINIMAX_TTS_MODELS = [
// OAuth Providers
export const OAUTH_PROVIDERS = {
claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757", website: "https://claude.ai", notice: { signupUrl: "https://claude.ai" } },
antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B", deprecated: true, deprecationNotice: "AG is designed exclusively for Antigravity IDE. Using it with other tools (OpenClaw, Claude, Codex...) may result in account restrictions or bans.", website: "https://antigravity.google", notice: { signupUrl: "https://antigravity.google" } },
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6", thinkingConfig: THINKING_CONFIG.effort, serviceKinds: ["llm", "image"], kindNotice: { image: "Requires a ChatGPT Plus (or higher) account. Free accounts are not supported for image generation." }, website: "https://chatgpt.com/codex", notice: { signupUrl: "https://chatgpt.com/codex" } },
github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333", serviceKinds: ["llm", "embedding"], embeddingConfig: { baseUrl: "https://models.github.ai/inference/embeddings", authType: "apikey", authHeader: "bearer", models: [{ id: "text-embedding-3-small", name: "Text Embedding 3 Small (GitHub)", dimensions: 1536 }, { id: "text-embedding-3-large", name: "Text Embedding 3 Large (GitHub)", dimensions: 3072 }] }, website: "https://github.com/features/copilot", notice: { signupUrl: "https://github.com/features/copilot" } },
claude: { id: "claude", alias: "cc", name: "Claude Code", icon: "smart_toy", color: "#D97757", deprecated: true, deprecationNotice: RISK_NOTICE, website: "https://claude.ai", notice: { signupUrl: "https://claude.ai" } },
antigravity: { id: "antigravity", alias: "ag", name: "Antigravity", icon: "rocket_launch", color: "#F59E0B", hidden: true, deprecated: true, deprecationNotice: "AG is designed exclusively for Antigravity IDE. Using it with other tools (OpenClaw, Claude, Codex...) may result in account restrictions or bans.", website: "https://antigravity.google", notice: { signupUrl: "https://antigravity.google" } },
codex: { id: "codex", alias: "cx", name: "OpenAI Codex", icon: "code", color: "#3B82F6", deprecated: true, deprecationNotice: RISK_NOTICE, thinkingConfig: THINKING_CONFIG.effort, serviceKinds: ["llm", "image"], kindNotice: { image: "Requires a ChatGPT Plus (or higher) account. Free accounts are not supported for image generation." }, website: "https://chatgpt.com/codex", notice: { signupUrl: "https://chatgpt.com/codex" } },
github: { id: "github", alias: "gh", name: "GitHub Copilot", icon: "code", color: "#333333", deprecated: true, deprecationNotice: RISK_NOTICE, serviceKinds: ["llm", "embedding"], embeddingConfig: { baseUrl: "https://models.github.ai/inference/embeddings", authType: "apikey", authHeader: "bearer", models: [{ id: "text-embedding-3-small", name: "Text Embedding 3 Small (GitHub)", dimensions: 1536 }, { id: "text-embedding-3-large", name: "Text Embedding 3 Large (GitHub)", dimensions: 3072 }] }, website: "https://github.com/features/copilot", notice: { signupUrl: "https://github.com/features/copilot" } },
cursor: { id: "cursor", alias: "cu", name: "Cursor IDE", icon: "edit_note", color: "#00D4AA", website: "https://cursor.com", notice: { signupUrl: "https://cursor.com" } },
// "kimi-coding": { id: "kimi-coding", alias: "kmc", name: "Kimi Coding", icon: "psychology", color: "#1E40AF", textIcon: "KC" },
kilocode: { id: "kilocode", alias: "kc", name: "Kilo Code", icon: "code", color: "#FF6B35", textIcon: "KC", website: "https://kilocode.ai", notice: { signupUrl: "https://kilocode.ai" } },

View file

@ -1,122 +0,0 @@
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { isCloudEnabled } from "@/lib/localDb";
const INTERNAL_BASE_URL =
process.env.BASE_URL ||
process.env.NEXT_PUBLIC_BASE_URL ||
"http://localhost:20128";
/**
* Cloud sync scheduler
*/
export class CloudSyncScheduler {
constructor(machineId = null, intervalMinutes = 15) {
this.machineId = machineId;
this.intervalMinutes = intervalMinutes;
this.intervalId = null;
}
/**
* Initialize machine ID if not provided
*/
async initializeMachineId() {
if (!this.machineId) {
this.machineId = await getConsistentMachineId();
}
}
/**
* Start periodic sync (delays first sync to allow server to be ready)
*/
async start() {
if (this.intervalId) {
return;
}
await this.initializeMachineId();
// Delay first sync by 30 seconds to ensure server is ready
setTimeout(() => {
this.syncWithRetry().catch(() => {});
}, 30000);
// Then sync periodically
this.intervalId = setInterval(() => {
this.syncWithRetry().catch(() => {});
}, this.intervalMinutes * 60 * 1000);
}
/**
* Stop periodic sync
*/
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
/**
* Sync with retry logic (exponential backoff)
*/
async syncWithRetry(maxRetries = 1) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await this.sync();
return result;
} catch (error) {
if (attempt === maxRetries) {
return null;
}
const delay = Math.min(1000 * Math.pow(2, attempt), 10000); // Max 10s
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
/**
* Perform sync via internal API route (handles token update to db.json)
*/
async sync() {
// Check if cloud is enabled
const enabled = await isCloudEnabled();
if (!enabled) {
return null;
}
await this.initializeMachineId();
// Call internal API route which handles both sync and token update
const response = await fetch(`${INTERNAL_BASE_URL}/api/sync/cloud`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ machineId: this.machineId, action: "sync" })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || "Sync failed");
}
const result = await response.json();
return result;
}
/**
* Check if scheduler is running
*/
isRunning() {
return this.intervalId !== null;
}
}
// Export a singleton instance if needed
let cloudSyncScheduler = null;
export async function getCloudSyncScheduler(machineId = null, intervalMinutes = 15) {
if (!cloudSyncScheduler) {
cloudSyncScheduler = new CloudSyncScheduler(machineId, intervalMinutes);
}
return cloudSyncScheduler;
}

View file

@ -1,37 +0,0 @@
/* ========== CLOUD SYNC COMMENTED OUT (replaced by Tunnel) ==========
import { getCloudSyncScheduler } from "@/shared/services/cloudSyncScheduler";
========== END CLOUD SYNC ========== */
import { cleanupProviderConnections } from "@/lib/localDb";
/**
* Initialize cloud sync scheduler
* This should be called when the application starts
*/
export async function initializeCloudSync() {
try {
// Cleanup null fields from existing data
await cleanupProviderConnections();
/* ========== CLOUD SYNC COMMENTED OUT (replaced by Tunnel) ==========
// Create scheduler instance with default 15-minute interval
const scheduler = await getCloudSyncScheduler(null, 15);
// Start the scheduler
await scheduler.start();
return scheduler;
========== END CLOUD SYNC ========== */
return null;
} catch (error) {
console.error("[CloudSync] Error initializing scheduler:", error);
throw error;
}
}
// For development/testing purposes
if (typeof require !== "undefined" && require.main === module) {
initializeCloudSync().catch(console.log);
}
export default initializeCloudSync;

View file

@ -1,40 +0,0 @@
import { getMachineId } from "@/shared/utils/machine";
// Function to get cloud URL with machine ID
export function getCloudUrl(machineId) {
// Get from environment or default to localhost:8787
const cloudUrl = process.env.NEXT_PUBLIC_CLOUD_URL || "http://localhost:8787";
return `${cloudUrl}/${machineId}/v1/chat/completions`;
}
// Function to call cloud with machine ID
export async function callCloudWithMachineId(request) {
const machineId = await getMachineId();
if (!machineId) {
throw new Error("Could not get machine ID");
}
const cloudUrl = getCloudUrl(machineId);
// Get the original request body and headers
const body = await request.json();
const headers = new Headers(request.headers);
// Remove authorization header since cloud won't need it (uses machineId instead)
headers.delete("authorization");
// Call the cloud with machine ID
const response = await fetch(cloudUrl, {
method: "POST",
headers: headers,
body: JSON.stringify(body)
});
return response;
}
// Function to periodically sync provider data to cloud (now a no-op)
export function startProviderSync(cloudUrl, intervalMs = 900000) { // Default 15 minutes
console.log("Frontend sync is disabled. Use backend sync instead.");
return null;
}

View file

@ -1,18 +1,6 @@
import { getConsistentMachineId } from './machineId';
import { getConsistentMachineId } from "./machineId";
// Get machine ID using node-machine-id with salt
export async function getMachineId() {
return await getConsistentMachineId();
}
// Keep sync functions for backward compatibility but make them no-ops
// (Frontend sync is disabled - use backend sync instead)
export async function syncProviderDataToCloud(cloudUrl) {
console.log("Frontend sync is disabled. Use backend sync instead.");
return Promise.resolve(true);
}
export async function getProvidersNeedingRefresh() {
console.log("Frontend sync is disabled. Use backend sync instead.");
return Promise.resolve([]);
}