mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
feat(cliproxy): add sync API routes
- add /api/cliproxy/sync endpoints for local sync - add /api/cliproxy/sync/aliases for model alias management - add /api/cliproxy/sync/auto-sync for watcher toggle - separate try-catch for config save vs watcher restart
This commit is contained in:
+11
-1
@@ -12,6 +12,7 @@ import path from 'path';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { setupWebSocket } from './websocket';
|
||||
import { createSessionMiddleware, authMiddleware } from './middleware/auth-middleware';
|
||||
import { startAutoSyncWatcher, stopAutoSyncWatcher } from '../cliproxy/sync';
|
||||
|
||||
export interface ServerOptions {
|
||||
port: number;
|
||||
@@ -93,7 +94,16 @@ export async function startServer(options: ServerOptions): Promise<ServerInstanc
|
||||
}
|
||||
|
||||
// WebSocket connection handler + file watcher
|
||||
const { cleanup } = setupWebSocket(wss);
|
||||
const { cleanup: wsCleanup } = setupWebSocket(wss);
|
||||
|
||||
// Start auto-sync watcher (if enabled in config)
|
||||
startAutoSyncWatcher();
|
||||
|
||||
// Combined cleanup function
|
||||
const cleanup = () => {
|
||||
wsCleanup();
|
||||
stopAutoSyncWatcher().catch(() => {});
|
||||
};
|
||||
|
||||
// Start listening
|
||||
return new Promise<ServerInstance>((resolve) => {
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* CLIProxy Sync Routes - Local sync management for CLIProxy profiles
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
||||
import {
|
||||
generateSyncPayload,
|
||||
generateSyncPreview,
|
||||
listAllAliases,
|
||||
addProfileAlias,
|
||||
removeProfileAlias,
|
||||
getAutoSyncStatus,
|
||||
restartAutoSyncWatcher,
|
||||
syncToLocalConfig,
|
||||
getLocalSyncStatus,
|
||||
} from '../../cliproxy/sync';
|
||||
import { saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/sync/status - Get local sync status
|
||||
* Returns: { configExists, configPath, currentKeyCount, syncableProfileCount }
|
||||
*/
|
||||
router.get('/status', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const status = getLocalSyncStatus();
|
||||
res.json({
|
||||
configured: status.configExists,
|
||||
configPath: status.configPath,
|
||||
currentKeyCount: status.currentKeyCount,
|
||||
syncableProfileCount: status.syncableProfileCount,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/sync/preview - Get sync preview (dry run)
|
||||
* Returns: { profiles: SyncPreviewItem[], payload: ClaudeKey[] }
|
||||
*/
|
||||
router.get('/preview', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const preview = generateSyncPreview();
|
||||
const payload = generateSyncPayload();
|
||||
|
||||
// Mask API keys in payload for preview
|
||||
const maskedPayload = payload.map((key) => ({
|
||||
...key,
|
||||
'api-key': maskApiKey(key['api-key']),
|
||||
}));
|
||||
|
||||
res.json({
|
||||
profiles: preview,
|
||||
payload: maskedPayload,
|
||||
count: payload.length,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/cliproxy/sync - Execute sync to local CLIProxy config
|
||||
* Returns: { success, syncedCount, configPath, error? }
|
||||
*/
|
||||
router.post('/', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const result = syncToLocalConfig();
|
||||
|
||||
if (!result.success) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: result.error,
|
||||
configPath: result.configPath,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.syncedCount === 0) {
|
||||
res.json({
|
||||
success: true,
|
||||
syncedCount: 0,
|
||||
message: 'No profiles to sync',
|
||||
configPath: result.configPath,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = generateSyncPreview();
|
||||
res.json({
|
||||
success: true,
|
||||
syncedCount: result.syncedCount,
|
||||
configPath: result.configPath,
|
||||
profiles: preview.map((p) => p.name),
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Model Aliases ====================
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/sync/aliases - List all model aliases
|
||||
* Returns: { aliases: Record<string, ModelAlias[]> }
|
||||
*/
|
||||
router.get('/aliases', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const aliases = listAllAliases();
|
||||
res.json({ aliases });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/cliproxy/sync/aliases - Add a model alias
|
||||
* Body: { profile: string, from: string, to: string }
|
||||
* Returns: { success: true }
|
||||
*/
|
||||
router.post('/aliases', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { profile, from, to } = req.body;
|
||||
|
||||
if (!profile || !from || !to) {
|
||||
res.status(400).json({ error: 'Missing required fields: profile, from, to' });
|
||||
return;
|
||||
}
|
||||
|
||||
addProfileAlias(profile, from, to);
|
||||
res.json({ success: true, profile, from, to });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/cliproxy/sync/aliases - Remove a model alias
|
||||
* Body: { profile: string, from: string }
|
||||
* Returns: { success: boolean }
|
||||
*/
|
||||
router.delete('/aliases', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { profile, from } = req.body;
|
||||
|
||||
if (!profile || !from) {
|
||||
res.status(400).json({ error: 'Missing required fields: profile, from' });
|
||||
return;
|
||||
}
|
||||
|
||||
const removed = removeProfileAlias(profile, from);
|
||||
res.json({ success: removed, profile, from });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Auto-Sync ====================
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/sync/auto-sync - Get auto-sync status
|
||||
* Returns: { enabled, watching, syncing }
|
||||
*/
|
||||
router.get('/auto-sync', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const status = getAutoSyncStatus();
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/cliproxy/sync/auto-sync - Toggle auto-sync setting
|
||||
* Body: { enabled: boolean }
|
||||
* Returns: { success: true, enabled }
|
||||
*/
|
||||
router.put('/auto-sync', async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { enabled } = req.body;
|
||||
|
||||
if (typeof enabled !== 'boolean') {
|
||||
res.status(400).json({ error: 'Invalid field: enabled must be a boolean' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Update config
|
||||
const config = loadOrCreateUnifiedConfig();
|
||||
if (!config.cliproxy) {
|
||||
// Should not happen as loadOrCreate initializes it, but handle gracefully
|
||||
res.status(500).json({ error: 'CLIProxy config not initialized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Save config
|
||||
try {
|
||||
config.cliproxy.auto_sync = enabled;
|
||||
saveUnifiedConfig(config);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: `Failed to save config: ${(error as Error).message}` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Restart watcher (separate operation)
|
||||
try {
|
||||
await restartAutoSyncWatcher();
|
||||
} catch (watcherError) {
|
||||
// Log but don't fail - config was saved successfully
|
||||
console.warn('Watcher restart failed:', (watcherError as Error).message);
|
||||
}
|
||||
|
||||
res.json({ success: true, enabled });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mask API key for display (show first 4 and last 4 chars).
|
||||
*/
|
||||
function maskApiKey(key: string): string {
|
||||
if (key.length <= 12) {
|
||||
return '***';
|
||||
}
|
||||
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -18,6 +18,7 @@ import settingsRoutes from './settings-routes';
|
||||
import websearchRoutes from './websearch-routes';
|
||||
import cliproxyAuthRoutes from './cliproxy-auth-routes';
|
||||
import cliproxyStatsRoutes from './cliproxy-stats-routes';
|
||||
import cliproxySyncRoutes from './cliproxy-sync-routes';
|
||||
import copilotRoutes from './copilot-routes';
|
||||
import miscRoutes from './misc-routes';
|
||||
import cliproxyServerRoutes from './proxy-routes';
|
||||
@@ -51,6 +52,7 @@ apiRoutes.use('/persist', persistRoutes);
|
||||
apiRoutes.use('/cliproxy', variantRoutes);
|
||||
apiRoutes.use('/cliproxy/auth', cliproxyAuthRoutes);
|
||||
apiRoutes.use('/cliproxy', cliproxyStatsRoutes);
|
||||
apiRoutes.use('/cliproxy/sync', cliproxySyncRoutes);
|
||||
apiRoutes.use('/cliproxy/openai-compat', providerRoutes);
|
||||
|
||||
// ==================== WebSearch ====================
|
||||
|
||||
Reference in New Issue
Block a user