mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-17 10:14:08 +00:00
fix(api): complete error handling and add missing endpoints
- Add try-catch to all route handlers in settings, variant, config routes - Implement 5 missing endpoints in cliproxy-stats-routes: - GET/PUT /config.yaml for YAML config management - GET /auth-files to list auth directory files - GET /auth-files/download for file download - PUT /models/:provider to update model settings - All handlers now return proper error messages on failure
This commit is contained in:
1 parent
85b0f17110
commit
3ed961fce9
4 files changed
+208
-1
No files matched your search
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {
|
import {
|
||||||
fetchCliproxyStats,
|
fetchCliproxyStats,
|
||||||
@@ -11,7 +12,11 @@ import {
|
|||||||
fetchCliproxyErrorLogs,
|
fetchCliproxyErrorLogs,
|
||||||
fetchCliproxyErrorLogContent,
|
fetchCliproxyErrorLogContent,
|
||||||
} from '../../cliproxy/stats-fetcher';
|
} from '../../cliproxy/stats-fetcher';
|
||||||
import { getCliproxyWritablePath } from '../../cliproxy/config-generator';
|
import {
|
||||||
|
getCliproxyWritablePath,
|
||||||
|
getConfigPath,
|
||||||
|
getAuthDir,
|
||||||
|
} from '../../cliproxy/config-generator';
|
||||||
import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker';
|
import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker';
|
||||||
import { ensureCliproxyService } from '../../cliproxy/service-manager';
|
import { ensureCliproxyService } from '../../cliproxy/service-manager';
|
||||||
import { checkCliproxyUpdate } from '../../cliproxy/binary-manager';
|
import { checkCliproxyUpdate } from '../../cliproxy/binary-manager';
|
||||||
@@ -259,4 +264,170 @@ router.get('/error-logs/:name', async (req: Request, res: Response): Promise<voi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== Config File ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/cliproxy/config.yaml - Get CLIProxy YAML config content
|
||||||
|
* Returns: plain text YAML content
|
||||||
|
*/
|
||||||
|
router.get('/config.yaml', async (_req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
res.status(404).json({ error: 'Config file not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(configPath, 'utf8');
|
||||||
|
res.type('text/yaml').send(content);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/cliproxy/config.yaml - Save CLIProxy YAML config content
|
||||||
|
* Body: { content: string }
|
||||||
|
* Returns: { success: true, path: string }
|
||||||
|
*/
|
||||||
|
router.put('/config.yaml', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { content } = req.body;
|
||||||
|
|
||||||
|
if (typeof content !== 'string') {
|
||||||
|
res.status(400).json({ error: 'Missing required field: content' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configPath = getConfigPath();
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
const configDir = path.dirname(configPath);
|
||||||
|
if (!fs.existsSync(configDir)) {
|
||||||
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write atomically
|
||||||
|
const tempPath = configPath + '.tmp';
|
||||||
|
fs.writeFileSync(tempPath, content);
|
||||||
|
fs.renameSync(tempPath, configPath);
|
||||||
|
|
||||||
|
res.json({ success: true, path: configPath });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== Auth Files ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/cliproxy/auth-files - List auth files in auth directory
|
||||||
|
* Returns: { files: Array<{ name, size, mtime }> }
|
||||||
|
*/
|
||||||
|
router.get('/auth-files', async (_req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const authDir = getAuthDir();
|
||||||
|
|
||||||
|
if (!fs.existsSync(authDir)) {
|
||||||
|
res.json({ files: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(authDir, { withFileTypes: true });
|
||||||
|
const files = entries
|
||||||
|
.filter((entry) => entry.isFile())
|
||||||
|
.map((entry) => {
|
||||||
|
const filePath = path.join(authDir, entry.name);
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
return {
|
||||||
|
name: entry.name,
|
||||||
|
size: stat.size,
|
||||||
|
mtime: stat.mtime.getTime(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ files, directory: authDir });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/cliproxy/auth-files/download - Download auth file content
|
||||||
|
* Query: ?name=filename
|
||||||
|
* Returns: file content as octet-stream
|
||||||
|
*/
|
||||||
|
router.get('/auth-files/download', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { name } = req.query;
|
||||||
|
|
||||||
|
if (!name || typeof name !== 'string') {
|
||||||
|
res.status(400).json({ error: 'Missing required query parameter: name' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate filename - prevent path traversal
|
||||||
|
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
||||||
|
res.status(400).json({ error: 'Invalid filename' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authDir = getAuthDir();
|
||||||
|
const filePath = path.join(authDir, name);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
res.status(404).json({ error: 'Auth file not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(filePath);
|
||||||
|
res.setHeader('Content-Disposition', `attachment; filename="${name}"`);
|
||||||
|
res.type('application/octet-stream').send(content);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== Model Updates ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/cliproxy/models/:provider - Update model for a provider
|
||||||
|
* Body: { model: string }
|
||||||
|
* Returns: { success: true, provider, model }
|
||||||
|
*/
|
||||||
|
router.put('/models/:provider', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { provider } = req.params;
|
||||||
|
const { model } = req.body;
|
||||||
|
|
||||||
|
if (!model || typeof model !== 'string') {
|
||||||
|
res.status(400).json({ error: 'Missing required field: model' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the settings file for this provider
|
||||||
|
const ccsDir = getCliproxyWritablePath();
|
||||||
|
const settingsPath = path.join(ccsDir, `${provider}.settings.json`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(settingsPath)) {
|
||||||
|
res.status(404).json({ error: `Settings file not found for provider: ${provider}` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and update settings
|
||||||
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||||
|
settings.env = settings.env || {};
|
||||||
|
settings.env.ANTHROPIC_MODEL = model;
|
||||||
|
|
||||||
|
// Write atomically
|
||||||
|
const tempPath = settingsPath + '.tmp';
|
||||||
|
fs.writeFileSync(tempPath, JSON.stringify(settings, null, 2) + '\n');
|
||||||
|
fs.renameSync(tempPath, settingsPath);
|
||||||
|
|
||||||
|
res.json({ success: true, provider, model });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -107,6 +107,7 @@ router.post('/migrate', async (req: Request, res: Response): Promise<void> => {
|
|||||||
* POST /api/config/rollback - Rollback migration to JSON format
|
* POST /api/config/rollback - Rollback migration to JSON format
|
||||||
*/
|
*/
|
||||||
router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
|
router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
const { backupPath } = req.body;
|
const { backupPath } = req.body;
|
||||||
|
|
||||||
if (!backupPath || typeof backupPath !== 'string') {
|
if (!backupPath || typeof backupPath !== 'string') {
|
||||||
@@ -116,6 +117,9 @@ router.post('/rollback', async (req: Request, res: Response): Promise<void> => {
|
|||||||
|
|
||||||
const success = await rollback(backupPath);
|
const success = await rollback(backupPath);
|
||||||
res.json({ success });
|
res.json({ success });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -32,6 +32,7 @@ function maskApiKeys(settings: Settings): Settings {
|
|||||||
* GET /api/settings/:profile - Get settings with masked API keys
|
* GET /api/settings/:profile - Get settings with masked API keys
|
||||||
*/
|
*/
|
||||||
router.get('/:profile', (req: Request, res: Response): void => {
|
router.get('/:profile', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { profile } = req.params;
|
const { profile } = req.params;
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||||
@@ -51,12 +52,16 @@ router.get('/:profile', (req: Request, res: Response): void => {
|
|||||||
mtime: stat.mtime.getTime(),
|
mtime: stat.mtime.getTime(),
|
||||||
path: settingsPath,
|
path: settingsPath,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/settings/:profile/raw - Get full settings (for editing)
|
* GET /api/settings/:profile/raw - Get full settings (for editing)
|
||||||
*/
|
*/
|
||||||
router.get('/:profile/raw', (req: Request, res: Response): void => {
|
router.get('/:profile/raw', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { profile } = req.params;
|
const { profile } = req.params;
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||||
@@ -75,12 +80,16 @@ router.get('/:profile/raw', (req: Request, res: Response): void => {
|
|||||||
mtime: stat.mtime.getTime(),
|
mtime: stat.mtime.getTime(),
|
||||||
path: settingsPath,
|
path: settingsPath,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/settings/:profile - Update settings with conflict detection and backup
|
* PUT /api/settings/:profile - Update settings with conflict detection and backup
|
||||||
*/
|
*/
|
||||||
router.put('/:profile', (req: Request, res: Response): void => {
|
router.put('/:profile', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { profile } = req.params;
|
const { profile } = req.params;
|
||||||
const { settings, expectedMtime } = req.body;
|
const { settings, expectedMtime } = req.body;
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
@@ -129,6 +138,9 @@ router.put('/:profile', (req: Request, res: Response): void => {
|
|||||||
backupPath,
|
backupPath,
|
||||||
created: !fileExists,
|
created: !fileExists,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== Presets ====================
|
// ==================== Presets ====================
|
||||||
@@ -137,6 +149,7 @@ router.put('/:profile', (req: Request, res: Response): void => {
|
|||||||
* GET /api/settings/:profile/presets - Get saved presets for a provider
|
* GET /api/settings/:profile/presets - Get saved presets for a provider
|
||||||
*/
|
*/
|
||||||
router.get('/:profile/presets', (req: Request, res: Response): void => {
|
router.get('/:profile/presets', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { profile } = req.params;
|
const { profile } = req.params;
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||||
@@ -148,12 +161,16 @@ router.get('/:profile/presets', (req: Request, res: Response): void => {
|
|||||||
|
|
||||||
const settings = loadSettings(settingsPath);
|
const settings = loadSettings(settingsPath);
|
||||||
res.json({ presets: settings.presets || [] });
|
res.json({ presets: settings.presets || [] });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/settings/:profile/presets - Create a new preset
|
* POST /api/settings/:profile/presets - Create a new preset
|
||||||
*/
|
*/
|
||||||
router.post('/:profile/presets', (req: Request, res: Response): void => {
|
router.post('/:profile/presets', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { profile } = req.params;
|
const { profile } = req.params;
|
||||||
const { name, default: defaultModel, opus, sonnet, haiku } = req.body;
|
const { name, default: defaultModel, opus, sonnet, haiku } = req.body;
|
||||||
|
|
||||||
@@ -191,12 +208,16 @@ router.post('/:profile/presets', (req: Request, res: Response): void => {
|
|||||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||||
|
|
||||||
res.status(201).json({ preset });
|
res.status(201).json({ preset });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/settings/:profile/presets/:name - Delete a preset
|
* DELETE /api/settings/:profile/presets/:name - Delete a preset
|
||||||
*/
|
*/
|
||||||
router.delete('/:profile/presets/:name', (req: Request, res: Response): void => {
|
router.delete('/:profile/presets/:name', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { profile, name } = req.params;
|
const { profile, name } = req.params;
|
||||||
const ccsDir = getCcsDir();
|
const ccsDir = getCcsDir();
|
||||||
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
|
||||||
@@ -216,6 +237,9 @@ router.delete('/:profile/presets/:name', (req: Request, res: Response): void =>
|
|||||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -78,6 +78,7 @@ router.post('/', (req: Request, res: Response): void => {
|
|||||||
* PUT /api/cliproxy/:name - Update cliproxy variant
|
* PUT /api/cliproxy/:name - Update cliproxy variant
|
||||||
*/
|
*/
|
||||||
router.put('/:name', (req: Request, res: Response): void => {
|
router.put('/:name', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
const { provider, account, model } = req.body;
|
const { provider, account, model } = req.body;
|
||||||
|
|
||||||
@@ -126,12 +127,16 @@ router.put('/:name', (req: Request, res: Response): void => {
|
|||||||
settings: variant.settings,
|
settings: variant.settings,
|
||||||
updated: true,
|
updated: true,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/cliproxy/:name - Delete cliproxy variant
|
* DELETE /api/cliproxy/:name - Delete cliproxy variant
|
||||||
*/
|
*/
|
||||||
router.delete('/:name', (req: Request, res: Response): void => {
|
router.delete('/:name', (req: Request, res: Response): void => {
|
||||||
|
try {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
|
|
||||||
const config = readConfigSafe();
|
const config = readConfigSafe();
|
||||||
@@ -154,6 +159,9 @@ router.delete('/:name', (req: Request, res: Response): void => {
|
|||||||
writeConfig(config);
|
writeConfig(config);
|
||||||
|
|
||||||
res.json({ name, deleted: true });
|
res.json({ name, deleted: true });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: (error as Error).message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
Reference in new issue
Block a user