portos-ai-toolkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +76 -0
- package/src/client/api.js +102 -0
- package/src/client/components/ProviderDropdown.jsx +39 -0
- package/src/client/components/index.js +5 -0
- package/src/client/hooks/index.js +6 -0
- package/src/client/hooks/useProviders.js +96 -0
- package/src/client/hooks/useRuns.js +94 -0
- package/src/client/index.js +11 -0
- package/src/client/pages/AIProviders.jsx +665 -0
- package/src/index.js +8 -0
- package/src/server/index.d.ts +87 -0
- package/src/server/index.js +95 -0
- package/src/server/prompts.js +234 -0
- package/src/server/providers.js +253 -0
- package/src/server/providers.test.js +120 -0
- package/src/server/routes/prompts.js +96 -0
- package/src/server/routes/providers.js +105 -0
- package/src/server/routes/runs.js +157 -0
- package/src/server/runner.js +475 -0
- package/src/server/validation.js +51 -0
- package/src/shared/constants.js +26 -0
- package/src/shared/index.js +5 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdir, rm, writeFile } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { createProviderService } from './providers.js';
|
|
6
|
+
|
|
7
|
+
const TEST_DATA_DIR = join(process.cwd(), 'test-data');
|
|
8
|
+
|
|
9
|
+
describe('Provider Service', () => {
|
|
10
|
+
let providerService;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
// Create test data directory
|
|
14
|
+
if (!existsSync(TEST_DATA_DIR)) {
|
|
15
|
+
await mkdir(TEST_DATA_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
providerService = createProviderService({
|
|
19
|
+
dataDir: TEST_DATA_DIR,
|
|
20
|
+
providersFile: 'providers.json'
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
// Clean up test data
|
|
26
|
+
if (existsSync(TEST_DATA_DIR)) {
|
|
27
|
+
await rm(TEST_DATA_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should create a provider', async () => {
|
|
32
|
+
const provider = await providerService.createProvider({
|
|
33
|
+
name: 'Test Provider',
|
|
34
|
+
type: 'cli',
|
|
35
|
+
command: 'test',
|
|
36
|
+
args: ['--version']
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(provider).toBeDefined();
|
|
40
|
+
expect(provider.id).toBe('test-provider');
|
|
41
|
+
expect(provider.name).toBe('Test Provider');
|
|
42
|
+
expect(provider.type).toBe('cli');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should get all providers', async () => {
|
|
46
|
+
await providerService.createProvider({
|
|
47
|
+
name: 'Test Provider 1',
|
|
48
|
+
type: 'cli',
|
|
49
|
+
command: 'test1'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await providerService.createProvider({
|
|
53
|
+
name: 'Test Provider 2',
|
|
54
|
+
type: 'api',
|
|
55
|
+
endpoint: 'https://api.example.com'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const { providers } = await providerService.getAllProviders();
|
|
59
|
+
expect(providers).toHaveLength(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should set active provider', async () => {
|
|
63
|
+
const newProvider = await providerService.createProvider({
|
|
64
|
+
name: 'Test Provider',
|
|
65
|
+
type: 'cli',
|
|
66
|
+
command: 'test'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const active = await providerService.setActiveProvider(newProvider.id);
|
|
70
|
+
expect(active).toBeDefined();
|
|
71
|
+
expect(active.id).toBe(newProvider.id);
|
|
72
|
+
|
|
73
|
+
const activeProvider = await providerService.getActiveProvider();
|
|
74
|
+
expect(activeProvider.id).toBe(newProvider.id);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should update a provider', async () => {
|
|
78
|
+
const newProvider = await providerService.createProvider({
|
|
79
|
+
name: 'Test Provider',
|
|
80
|
+
type: 'cli',
|
|
81
|
+
command: 'test'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const updated = await providerService.updateProvider(newProvider.id, {
|
|
85
|
+
command: 'updated-test'
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(updated.command).toBe('updated-test');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should delete a provider', async () => {
|
|
92
|
+
const newProvider = await providerService.createProvider({
|
|
93
|
+
name: 'Test Provider',
|
|
94
|
+
type: 'cli',
|
|
95
|
+
command: 'test'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const deleted = await providerService.deleteProvider(newProvider.id);
|
|
99
|
+
expect(deleted).toBe(true);
|
|
100
|
+
|
|
101
|
+
const retrieved = await providerService.getProviderById(newProvider.id);
|
|
102
|
+
expect(retrieved).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw error for duplicate provider', async () => {
|
|
106
|
+
await providerService.createProvider({
|
|
107
|
+
name: 'Test Provider',
|
|
108
|
+
type: 'cli',
|
|
109
|
+
command: 'test'
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await expect(
|
|
113
|
+
providerService.createProvider({
|
|
114
|
+
name: 'Test Provider',
|
|
115
|
+
type: 'cli',
|
|
116
|
+
command: 'test'
|
|
117
|
+
})
|
|
118
|
+
).rejects.toThrow('Provider with this ID already exists');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create prompts routes
|
|
5
|
+
*/
|
|
6
|
+
export function createPromptsRoutes(promptsService, options = {}) {
|
|
7
|
+
const router = Router();
|
|
8
|
+
const { asyncHandler = (fn) => fn } = options;
|
|
9
|
+
|
|
10
|
+
// GET /prompts/stages - Get all stages
|
|
11
|
+
router.get('/stages', asyncHandler(async (req, res) => {
|
|
12
|
+
const stages = promptsService.getStages();
|
|
13
|
+
res.json(stages);
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// GET /prompts/stages/:name - Get specific stage
|
|
17
|
+
router.get('/stages/:name', asyncHandler(async (req, res) => {
|
|
18
|
+
const stage = promptsService.getStage(req.params.name);
|
|
19
|
+
|
|
20
|
+
if (!stage) {
|
|
21
|
+
return res.status(404).json({ error: 'Stage not found' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const template = await promptsService.getStageTemplate(req.params.name);
|
|
25
|
+
res.json({ ...stage, template });
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// PUT /prompts/stages/:name - Update stage
|
|
29
|
+
router.put('/stages/:name', asyncHandler(async (req, res) => {
|
|
30
|
+
const { config, template } = req.body;
|
|
31
|
+
|
|
32
|
+
if (config) {
|
|
33
|
+
await promptsService.updateStageConfig(req.params.name, config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (template) {
|
|
37
|
+
await promptsService.updateStageTemplate(req.params.name, template);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const updated = promptsService.getStage(req.params.name);
|
|
41
|
+
const updatedTemplate = await promptsService.getStageTemplate(req.params.name);
|
|
42
|
+
|
|
43
|
+
res.json({ ...updated, template: updatedTemplate });
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// POST /prompts/stages/:name/preview - Preview stage with test data
|
|
47
|
+
router.post('/stages/:name/preview', asyncHandler(async (req, res) => {
|
|
48
|
+
const preview = await promptsService.previewPrompt(req.params.name, req.body);
|
|
49
|
+
res.json({ preview });
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// GET /prompts/variables - Get all variables
|
|
53
|
+
router.get('/variables', asyncHandler(async (req, res) => {
|
|
54
|
+
const variables = promptsService.getVariables();
|
|
55
|
+
res.json(variables);
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// GET /prompts/variables/:key - Get specific variable
|
|
59
|
+
router.get('/variables/:key', asyncHandler(async (req, res) => {
|
|
60
|
+
const variable = promptsService.getVariable(req.params.key);
|
|
61
|
+
|
|
62
|
+
if (!variable) {
|
|
63
|
+
return res.status(404).json({ error: 'Variable not found' });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
res.json(variable);
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
// POST /prompts/variables - Create variable
|
|
70
|
+
router.post('/variables', asyncHandler(async (req, res) => {
|
|
71
|
+
const { key, ...data } = req.body;
|
|
72
|
+
|
|
73
|
+
if (!key) {
|
|
74
|
+
return res.status(400).json({ error: 'Variable key is required' });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await promptsService.createVariable(key, data);
|
|
78
|
+
const created = promptsService.getVariable(key);
|
|
79
|
+
res.status(201).json(created);
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
// PUT /prompts/variables/:key - Update variable
|
|
83
|
+
router.put('/variables/:key', asyncHandler(async (req, res) => {
|
|
84
|
+
await promptsService.updateVariable(req.params.key, req.body);
|
|
85
|
+
const updated = promptsService.getVariable(req.params.key);
|
|
86
|
+
res.json(updated);
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// DELETE /prompts/variables/:key - Delete variable
|
|
90
|
+
router.delete('/variables/:key', asyncHandler(async (req, res) => {
|
|
91
|
+
await promptsService.deleteVariable(req.params.key);
|
|
92
|
+
res.status(204).send();
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
return router;
|
|
96
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create providers routes
|
|
5
|
+
*/
|
|
6
|
+
export function createProvidersRoutes(providerService, options = {}) {
|
|
7
|
+
const router = Router();
|
|
8
|
+
const { asyncHandler = (fn) => fn } = options;
|
|
9
|
+
|
|
10
|
+
// GET /providers - List all providers
|
|
11
|
+
router.get('/', asyncHandler(async (req, res) => {
|
|
12
|
+
const data = await providerService.getAllProviders();
|
|
13
|
+
res.json(data);
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// GET /providers/active - Get active provider
|
|
17
|
+
router.get('/active', asyncHandler(async (req, res) => {
|
|
18
|
+
const provider = await providerService.getActiveProvider();
|
|
19
|
+
res.json(provider);
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// PUT /providers/active - Set active provider
|
|
23
|
+
router.put('/active', asyncHandler(async (req, res) => {
|
|
24
|
+
const { id } = req.body;
|
|
25
|
+
if (!id) {
|
|
26
|
+
return res.status(400).json({ error: 'Provider ID required' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const provider = await providerService.setActiveProvider(id);
|
|
30
|
+
|
|
31
|
+
if (!provider) {
|
|
32
|
+
return res.status(404).json({ error: 'Provider not found' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.json(provider);
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// GET /providers/:id - Get provider by ID
|
|
39
|
+
router.get('/:id', asyncHandler(async (req, res) => {
|
|
40
|
+
const provider = await providerService.getProviderById(req.params.id);
|
|
41
|
+
|
|
42
|
+
if (!provider) {
|
|
43
|
+
return res.status(404).json({ error: 'Provider not found' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
res.json(provider);
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// POST /providers - Create new provider
|
|
50
|
+
router.post('/', asyncHandler(async (req, res) => {
|
|
51
|
+
const { name, type } = req.body;
|
|
52
|
+
|
|
53
|
+
if (!name) {
|
|
54
|
+
return res.status(400).json({ error: 'Name is required' });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!type || !['cli', 'api'].includes(type)) {
|
|
58
|
+
return res.status(400).json({ error: 'Type must be "cli" or "api"' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const provider = await providerService.createProvider(req.body);
|
|
62
|
+
res.status(201).json(provider);
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// PUT /providers/:id - Update provider
|
|
66
|
+
router.put('/:id', asyncHandler(async (req, res) => {
|
|
67
|
+
const provider = await providerService.updateProvider(req.params.id, req.body);
|
|
68
|
+
|
|
69
|
+
if (!provider) {
|
|
70
|
+
return res.status(404).json({ error: 'Provider not found' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
res.json(provider);
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// DELETE /providers/:id - Delete provider
|
|
77
|
+
router.delete('/:id', asyncHandler(async (req, res) => {
|
|
78
|
+
const deleted = await providerService.deleteProvider(req.params.id);
|
|
79
|
+
|
|
80
|
+
if (!deleted) {
|
|
81
|
+
return res.status(404).json({ error: 'Provider not found' });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
res.status(204).send();
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// POST /providers/:id/test - Test provider connectivity
|
|
88
|
+
router.post('/:id/test', asyncHandler(async (req, res) => {
|
|
89
|
+
const result = await providerService.testProvider(req.params.id);
|
|
90
|
+
res.json(result);
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// POST /providers/:id/refresh-models - Refresh models for API provider
|
|
94
|
+
router.post('/:id/refresh-models', asyncHandler(async (req, res) => {
|
|
95
|
+
const provider = await providerService.refreshProviderModels(req.params.id);
|
|
96
|
+
|
|
97
|
+
if (!provider) {
|
|
98
|
+
return res.status(404).json({ error: 'Provider not found or not an API type' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res.json(provider);
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
return router;
|
|
105
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create runs routes
|
|
5
|
+
*/
|
|
6
|
+
export function createRunsRoutes(runnerService, options = {}) {
|
|
7
|
+
const router = Router();
|
|
8
|
+
const { asyncHandler = (fn) => fn, io = null } = options;
|
|
9
|
+
|
|
10
|
+
// GET /runs - List runs
|
|
11
|
+
router.get('/', asyncHandler(async (req, res) => {
|
|
12
|
+
const limit = parseInt(req.query.limit) || 50;
|
|
13
|
+
const offset = parseInt(req.query.offset) || 0;
|
|
14
|
+
const source = req.query.source || 'all';
|
|
15
|
+
|
|
16
|
+
const result = await runnerService.listRuns(limit, offset, source);
|
|
17
|
+
res.json(result);
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// POST /runs - Create and execute a new run
|
|
21
|
+
router.post('/', asyncHandler(async (req, res) => {
|
|
22
|
+
const { providerId, model, prompt, workspacePath, workspaceName, timeout, screenshots } = req.body;
|
|
23
|
+
console.log(`🚀 POST /runs - provider: ${providerId}, model: ${model}, workspace: ${workspaceName}`);
|
|
24
|
+
|
|
25
|
+
if (!providerId) {
|
|
26
|
+
return res.status(400).json({ error: 'providerId is required' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!prompt) {
|
|
30
|
+
return res.status(400).json({ error: 'prompt is required' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const runData = await runnerService.createRun({
|
|
34
|
+
providerId,
|
|
35
|
+
model,
|
|
36
|
+
prompt,
|
|
37
|
+
workspacePath,
|
|
38
|
+
workspaceName,
|
|
39
|
+
timeout,
|
|
40
|
+
screenshots
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { runId, provider, metadata, timeout: effectiveTimeout } = runData;
|
|
44
|
+
console.log(`🚀 Run created: ${runId}, provider type: ${provider.type}`);
|
|
45
|
+
|
|
46
|
+
// Execute based on provider type
|
|
47
|
+
if (provider.type === 'cli') {
|
|
48
|
+
runnerService.executeCliRun(
|
|
49
|
+
runId,
|
|
50
|
+
provider,
|
|
51
|
+
prompt,
|
|
52
|
+
workspacePath,
|
|
53
|
+
(data) => {
|
|
54
|
+
io?.emit(`run:${runId}:data`, data);
|
|
55
|
+
},
|
|
56
|
+
(finalMetadata) => {
|
|
57
|
+
console.log(`✅ Run complete: ${runId}, success: ${finalMetadata.success}`);
|
|
58
|
+
io?.emit(`run:${runId}:complete`, finalMetadata);
|
|
59
|
+
},
|
|
60
|
+
effectiveTimeout
|
|
61
|
+
);
|
|
62
|
+
} else if (provider.type === 'api') {
|
|
63
|
+
runnerService.executeApiRun(
|
|
64
|
+
runId,
|
|
65
|
+
provider,
|
|
66
|
+
model,
|
|
67
|
+
prompt,
|
|
68
|
+
workspacePath,
|
|
69
|
+
screenshots,
|
|
70
|
+
(data) => {
|
|
71
|
+
io?.emit(`run:${runId}:data`, data);
|
|
72
|
+
},
|
|
73
|
+
(finalMetadata) => {
|
|
74
|
+
io?.emit(`run:${runId}:complete`, finalMetadata);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Return immediately with run ID
|
|
80
|
+
res.status(202).json({
|
|
81
|
+
runId,
|
|
82
|
+
status: 'started',
|
|
83
|
+
metadata
|
|
84
|
+
});
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// GET /runs/:id - Get run metadata
|
|
88
|
+
router.get('/:id', asyncHandler(async (req, res) => {
|
|
89
|
+
const metadata = await runnerService.getRun(req.params.id);
|
|
90
|
+
|
|
91
|
+
if (!metadata) {
|
|
92
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isActive = await runnerService.isRunActive(req.params.id);
|
|
96
|
+
res.json({
|
|
97
|
+
...metadata,
|
|
98
|
+
isActive
|
|
99
|
+
});
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// GET /runs/:id/output - Get run output
|
|
103
|
+
router.get('/:id/output', asyncHandler(async (req, res) => {
|
|
104
|
+
const output = await runnerService.getRunOutput(req.params.id);
|
|
105
|
+
|
|
106
|
+
if (output === null) {
|
|
107
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.type('text/plain').send(output);
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
// GET /runs/:id/prompt - Get run prompt
|
|
114
|
+
router.get('/:id/prompt', asyncHandler(async (req, res) => {
|
|
115
|
+
const prompt = await runnerService.getRunPrompt(req.params.id);
|
|
116
|
+
|
|
117
|
+
if (prompt === null) {
|
|
118
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
res.type('text/plain').send(prompt);
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
// POST /runs/:id/stop - Stop a running execution
|
|
125
|
+
router.post('/:id/stop', asyncHandler(async (req, res) => {
|
|
126
|
+
const stopped = await runnerService.stopRun(req.params.id);
|
|
127
|
+
|
|
128
|
+
if (!stopped) {
|
|
129
|
+
return res.status(404).json({ error: 'Run not found or not active' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
res.json({ success: true });
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
// DELETE /runs/:id - Delete a run
|
|
136
|
+
router.delete('/:id', asyncHandler(async (req, res) => {
|
|
137
|
+
const deleted = await runnerService.deleteRun(req.params.id);
|
|
138
|
+
|
|
139
|
+
if (!deleted) {
|
|
140
|
+
return res.status(404).json({ error: 'Run not found' });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
res.status(204).send();
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
// DELETE /runs/failed - Delete all failed runs
|
|
147
|
+
router.delete('/', asyncHandler(async (req, res) => {
|
|
148
|
+
if (req.query.filter !== 'failed') {
|
|
149
|
+
return res.status(400).json({ error: 'Only filter=failed is supported for bulk delete' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const deletedCount = await runnerService.deleteFailedRuns();
|
|
153
|
+
res.json({ deletedCount });
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
return router;
|
|
157
|
+
}
|