mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 10:19:37 +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 { WebSocketServer } from 'ws';
|
||||||
import { setupWebSocket } from './websocket';
|
import { setupWebSocket } from './websocket';
|
||||||
import { createSessionMiddleware, authMiddleware } from './middleware/auth-middleware';
|
import { createSessionMiddleware, authMiddleware } from './middleware/auth-middleware';
|
||||||
|
import { startAutoSyncWatcher, stopAutoSyncWatcher } from '../cliproxy/sync';
|
||||||
|
|
||||||
export interface ServerOptions {
|
export interface ServerOptions {
|
||||||
port: number;
|
port: number;
|
||||||
@@ -93,7 +94,16 @@ export async function startServer(options: ServerOptions): Promise<ServerInstanc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket connection handler + file watcher
|
// 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
|
// Start listening
|
||||||
return new Promise<ServerInstance>((resolve) => {
|
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 websearchRoutes from './websearch-routes';
|
||||||
import cliproxyAuthRoutes from './cliproxy-auth-routes';
|
import cliproxyAuthRoutes from './cliproxy-auth-routes';
|
||||||
import cliproxyStatsRoutes from './cliproxy-stats-routes';
|
import cliproxyStatsRoutes from './cliproxy-stats-routes';
|
||||||
|
import cliproxySyncRoutes from './cliproxy-sync-routes';
|
||||||
import copilotRoutes from './copilot-routes';
|
import copilotRoutes from './copilot-routes';
|
||||||
import miscRoutes from './misc-routes';
|
import miscRoutes from './misc-routes';
|
||||||
import cliproxyServerRoutes from './proxy-routes';
|
import cliproxyServerRoutes from './proxy-routes';
|
||||||
@@ -51,6 +52,7 @@ apiRoutes.use('/persist', persistRoutes);
|
|||||||
apiRoutes.use('/cliproxy', variantRoutes);
|
apiRoutes.use('/cliproxy', variantRoutes);
|
||||||
apiRoutes.use('/cliproxy/auth', cliproxyAuthRoutes);
|
apiRoutes.use('/cliproxy/auth', cliproxyAuthRoutes);
|
||||||
apiRoutes.use('/cliproxy', cliproxyStatsRoutes);
|
apiRoutes.use('/cliproxy', cliproxyStatsRoutes);
|
||||||
|
apiRoutes.use('/cliproxy/sync', cliproxySyncRoutes);
|
||||||
apiRoutes.use('/cliproxy/openai-compat', providerRoutes);
|
apiRoutes.use('/cliproxy/openai-compat', providerRoutes);
|
||||||
|
|
||||||
// ==================== WebSearch ====================
|
// ==================== WebSearch ====================
|
||||||
|
|||||||
Reference in New Issue
Block a user