ai-agent-router 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.
Files changed (166) hide show
  1. package/.claude/commands/openspec/apply.md +23 -0
  2. package/.claude/commands/openspec/archive.md +27 -0
  3. package/.claude/commands/openspec/proposal.md +28 -0
  4. package/.claude/settings.local.json +12 -0
  5. package/.claude/skills/ui-ux-pro-max/SKILL.md +228 -0
  6. package/.claude/skills/ui-ux-pro-max/data/charts.csv +26 -0
  7. package/.claude/skills/ui-ux-pro-max/data/colors.csv +97 -0
  8. package/.claude/skills/ui-ux-pro-max/data/landing.csv +31 -0
  9. package/.claude/skills/ui-ux-pro-max/data/products.csv +97 -0
  10. package/.claude/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  11. package/.claude/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  12. package/.claude/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  13. package/.claude/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  14. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  15. package/.claude/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  16. package/.claude/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  17. package/.claude/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  18. package/.claude/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  19. package/.claude/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  20. package/.claude/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  21. package/.claude/skills/ui-ux-pro-max/data/styles.csv +59 -0
  22. package/.claude/skills/ui-ux-pro-max/data/typography.csv +58 -0
  23. package/.claude/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  24. package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-311.pyc +0 -0
  25. package/.claude/skills/ui-ux-pro-max/scripts/core.py +238 -0
  26. package/.claude/skills/ui-ux-pro-max/scripts/search.py +61 -0
  27. package/.cursor/commands/openspec-apply.md +23 -0
  28. package/.cursor/commands/openspec-archive.md +27 -0
  29. package/.cursor/commands/openspec-proposal.md +28 -0
  30. package/.cursor/commands/ui-ux-pro-max.md +226 -0
  31. package/.eslintrc.json +3 -0
  32. package/.shared/ui-ux-pro-max/data/charts.csv +26 -0
  33. package/.shared/ui-ux-pro-max/data/colors.csv +97 -0
  34. package/.shared/ui-ux-pro-max/data/landing.csv +31 -0
  35. package/.shared/ui-ux-pro-max/data/products.csv +97 -0
  36. package/.shared/ui-ux-pro-max/data/prompts.csv +24 -0
  37. package/.shared/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  38. package/.shared/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  39. package/.shared/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  40. package/.shared/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  41. package/.shared/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  42. package/.shared/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  43. package/.shared/ui-ux-pro-max/data/stacks/react.csv +54 -0
  44. package/.shared/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  45. package/.shared/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  46. package/.shared/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  47. package/.shared/ui-ux-pro-max/data/styles.csv +59 -0
  48. package/.shared/ui-ux-pro-max/data/typography.csv +58 -0
  49. package/.shared/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  50. package/.shared/ui-ux-pro-max/scripts/core.py +238 -0
  51. package/.shared/ui-ux-pro-max/scripts/search.py +61 -0
  52. package/AGENTS.md +18 -0
  53. package/CLAUDE.md +18 -0
  54. package/IMPLEMENTATION.md +157 -0
  55. package/LICENSE +21 -0
  56. package/README.md +165 -0
  57. package/dist/.next/types/app/api/config/route.js +52 -0
  58. package/dist/.next/types/app/api/gateway/[...path]/route.js +52 -0
  59. package/dist/.next/types/app/api/gateway/route.js +52 -0
  60. package/dist/.next/types/app/api/logs/route.js +52 -0
  61. package/dist/.next/types/app/api/models/route.js +52 -0
  62. package/dist/.next/types/app/api/providers/route.js +52 -0
  63. package/dist/.next/types/app/api/providers/test/route.js +52 -0
  64. package/dist/.next/types/app/api/service/start/route.js +52 -0
  65. package/dist/.next/types/app/api/service/status/route.js +52 -0
  66. package/dist/.next/types/app/api/service/stop/route.js +52 -0
  67. package/dist/.next/types/app/layout.js +22 -0
  68. package/dist/.next/types/app/logs/page.js +22 -0
  69. package/dist/.next/types/app/models/page.js +22 -0
  70. package/dist/.next/types/app/page.js +22 -0
  71. package/dist/.next/types/app/providers/page.js +22 -0
  72. package/dist/src/app/api/config/route.js +43 -0
  73. package/dist/src/app/api/gateway/[...path]/route.js +83 -0
  74. package/dist/src/app/api/gateway/route.js +63 -0
  75. package/dist/src/app/api/logs/route.js +34 -0
  76. package/dist/src/app/api/models/route.js +152 -0
  77. package/dist/src/app/api/providers/route.js +118 -0
  78. package/dist/src/app/api/providers/test/route.js +154 -0
  79. package/dist/src/app/api/service/start/route.js +55 -0
  80. package/dist/src/app/api/service/status/route.js +17 -0
  81. package/dist/src/app/api/service/stop/route.js +20 -0
  82. package/dist/src/app/components/ConfirmDialog.jsx +31 -0
  83. package/dist/src/app/components/Nav.jsx +45 -0
  84. package/dist/src/app/components/Toast.jsx +37 -0
  85. package/dist/src/app/components/ToastProvider.jsx +21 -0
  86. package/dist/src/app/layout.jsx +13 -0
  87. package/dist/src/app/logs/page.jsx +210 -0
  88. package/dist/src/app/models/page.jsx +291 -0
  89. package/dist/src/app/page.jsx +236 -0
  90. package/dist/src/app/providers/page.jsx +402 -0
  91. package/dist/src/cli/index.js +90 -0
  92. package/dist/src/db/database.js +69 -0
  93. package/dist/src/db/queries.js +261 -0
  94. package/dist/src/db/schema.js +67 -0
  95. package/dist/src/server/crypto.js +22 -0
  96. package/dist/src/server/gateway-server.js +200 -0
  97. package/dist/src/server/gateway.js +76 -0
  98. package/dist/src/server/logger.js +72 -0
  99. package/dist/src/server/providers/anthropic.js +52 -0
  100. package/dist/src/server/providers/gemini.js +64 -0
  101. package/dist/src/server/providers/index.js +16 -0
  102. package/dist/src/server/providers/openai.js +86 -0
  103. package/dist/src/server/providers/types.js +1 -0
  104. package/dist/src/server/service-manager.js +286 -0
  105. package/docs/TODO.md +19 -0
  106. package/next.config.js +7 -0
  107. package/openspec/AGENTS.md +456 -0
  108. package/openspec/changes/add-logging/proposal.md +18 -0
  109. package/openspec/changes/add-logging/specs/core/spec.md +21 -0
  110. package/openspec/changes/add-logging/tasks.md +16 -0
  111. package/openspec/changes/add-provider-test-connection/proposal.md +22 -0
  112. package/openspec/changes/add-provider-test-connection/specs/model-provider/spec.md +68 -0
  113. package/openspec/changes/add-provider-test-connection/tasks.md +31 -0
  114. package/openspec/changes/improve-gateway-startup/design.md +137 -0
  115. package/openspec/changes/improve-gateway-startup/proposal.md +33 -0
  116. package/openspec/changes/improve-gateway-startup/specs/api-gateway/spec.md +94 -0
  117. package/openspec/changes/improve-gateway-startup/specs/web-ui/spec.md +67 -0
  118. package/openspec/changes/improve-gateway-startup/tasks.md +47 -0
  119. package/openspec/changes/init-api-gateway/design.md +185 -0
  120. package/openspec/changes/init-api-gateway/proposal.md +30 -0
  121. package/openspec/changes/init-api-gateway/specs/api-gateway/spec.md +42 -0
  122. package/openspec/changes/init-api-gateway/specs/cli-tool/spec.md +40 -0
  123. package/openspec/changes/init-api-gateway/specs/model-management/spec.md +47 -0
  124. package/openspec/changes/init-api-gateway/specs/model-provider/spec.md +33 -0
  125. package/openspec/changes/init-api-gateway/specs/request-logging/spec.md +54 -0
  126. package/openspec/changes/init-api-gateway/specs/web-ui/spec.md +49 -0
  127. package/openspec/changes/init-api-gateway/tasks.md +84 -0
  128. package/openspec/project.md +58 -0
  129. package/package.json +51 -0
  130. package/postcss.config.js +6 -0
  131. package/src/app/api/config/route.ts +62 -0
  132. package/src/app/api/gateway/[...path]/route.ts +118 -0
  133. package/src/app/api/gateway/route.ts +77 -0
  134. package/src/app/api/logs/route.ts +48 -0
  135. package/src/app/api/models/route.ts +210 -0
  136. package/src/app/api/providers/route.ts +162 -0
  137. package/src/app/api/providers/test/route.ts +182 -0
  138. package/src/app/api/service/start/route.ts +73 -0
  139. package/src/app/api/service/status/route.ts +22 -0
  140. package/src/app/api/service/stop/route.ts +27 -0
  141. package/src/app/components/ConfirmDialog.tsx +63 -0
  142. package/src/app/components/Nav.tsx +66 -0
  143. package/src/app/components/Toast.tsx +61 -0
  144. package/src/app/components/ToastProvider.tsx +43 -0
  145. package/src/app/globals.css +71 -0
  146. package/src/app/layout.tsx +22 -0
  147. package/src/app/logs/page.tsx +261 -0
  148. package/src/app/models/page.tsx +500 -0
  149. package/src/app/page.tsx +742 -0
  150. package/src/app/providers/page.tsx +558 -0
  151. package/src/cli/index.ts +95 -0
  152. package/src/db/database.ts +125 -0
  153. package/src/db/queries.ts +339 -0
  154. package/src/db/schema.ts +117 -0
  155. package/src/server/crypto.ts +48 -0
  156. package/src/server/gateway-server.ts +306 -0
  157. package/src/server/gateway.ts +163 -0
  158. package/src/server/logger.ts +96 -0
  159. package/src/server/providers/anthropic.ts +121 -0
  160. package/src/server/providers/gemini.ts +112 -0
  161. package/src/server/providers/index.ts +20 -0
  162. package/src/server/providers/openai.ts +235 -0
  163. package/src/server/providers/types.ts +20 -0
  164. package/src/server/service-manager.ts +321 -0
  165. package/tailwind.config.js +16 -0
  166. package/tsconfig.json +29 -0
@@ -0,0 +1,52 @@
1
+ import { decryptApiKey } from '../crypto.js';
2
+ export class AnthropicAdapter {
3
+ async forwardRequest(model, request) {
4
+ const apiKey = decryptApiKey(model.provider.api_key);
5
+ const baseUrl = model.provider.base_url || 'https://api.anthropic.com/v1';
6
+ // Build the target URL
7
+ let targetPath = request.path;
8
+ if (targetPath.startsWith('/v1/')) {
9
+ targetPath = targetPath.substring(4);
10
+ }
11
+ const url = `${baseUrl}/${targetPath}`;
12
+ // Prepare headers
13
+ const headers = {
14
+ 'x-api-key': apiKey,
15
+ 'anthropic-version': '2023-06-01',
16
+ 'Content-Type': 'application/json',
17
+ ...request.headers,
18
+ };
19
+ delete headers['host'];
20
+ delete headers['connection'];
21
+ delete headers['authorization'];
22
+ // Make the request
23
+ const response = await fetch(url, {
24
+ method: request.method,
25
+ headers,
26
+ body: request.method !== 'GET' && request.method !== 'HEAD' ? JSON.stringify(request.body) : undefined,
27
+ });
28
+ const responseBody = await response.text();
29
+ let parsedBody;
30
+ try {
31
+ parsedBody = JSON.parse(responseBody);
32
+ }
33
+ catch {
34
+ parsedBody = responseBody;
35
+ }
36
+ return {
37
+ status: response.status,
38
+ headers: Object.fromEntries(response.headers.entries()),
39
+ body: parsedBody,
40
+ };
41
+ }
42
+ async listModels(provider) {
43
+ // Anthropic doesn't have a public models endpoint
44
+ // Return common models
45
+ return [
46
+ { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet' },
47
+ { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus' },
48
+ { id: 'claude-3-sonnet-20240229', name: 'Claude 3 Sonnet' },
49
+ { id: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku' },
50
+ ];
51
+ }
52
+ }
@@ -0,0 +1,64 @@
1
+ import { decryptApiKey } from '../crypto.js';
2
+ export class GeminiAdapter {
3
+ async forwardRequest(model, request) {
4
+ const apiKey = decryptApiKey(model.provider.api_key);
5
+ const baseUrl = model.provider.base_url || 'https://generativelanguage.googleapis.com/v1';
6
+ // Build the target URL
7
+ let targetPath = request.path;
8
+ if (targetPath.startsWith('/v1/')) {
9
+ targetPath = targetPath.substring(4);
10
+ }
11
+ // Add API key to query params for Gemini
12
+ const url = `${baseUrl}/${targetPath}?key=${apiKey}`;
13
+ // Prepare headers
14
+ const headers = {
15
+ 'Content-Type': 'application/json',
16
+ ...request.headers,
17
+ };
18
+ delete headers['host'];
19
+ delete headers['connection'];
20
+ delete headers['authorization'];
21
+ // Make the request
22
+ const response = await fetch(url, {
23
+ method: request.method,
24
+ headers,
25
+ body: request.method !== 'GET' && request.method !== 'HEAD' ? JSON.stringify(request.body) : undefined,
26
+ });
27
+ const responseBody = await response.text();
28
+ let parsedBody;
29
+ try {
30
+ parsedBody = JSON.parse(responseBody);
31
+ }
32
+ catch {
33
+ parsedBody = responseBody;
34
+ }
35
+ return {
36
+ status: response.status,
37
+ headers: Object.fromEntries(response.headers.entries()),
38
+ body: parsedBody,
39
+ };
40
+ }
41
+ async listModels(provider) {
42
+ const apiKey = decryptApiKey(provider.api_key);
43
+ const baseUrl = provider.base_url || 'https://generativelanguage.googleapis.com/v1';
44
+ try {
45
+ const response = await fetch(`${baseUrl}/models?key=${apiKey}`, {
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ },
49
+ });
50
+ if (!response.ok) {
51
+ throw new Error(`Failed to fetch models: ${response.statusText}`);
52
+ }
53
+ const data = await response.json();
54
+ return (data.models || []).map((model) => ({
55
+ id: model.name.replace('models/', ''),
56
+ name: model.displayName || model.name,
57
+ }));
58
+ }
59
+ catch (error) {
60
+ console.error('Error fetching Gemini models:', error);
61
+ throw error;
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,16 @@
1
+ import { OpenAIAdapter } from './openai.js';
2
+ import { AnthropicAdapter } from './anthropic.js';
3
+ import { GeminiAdapter } from './gemini.js';
4
+ export function getProviderAdapter(protocol) {
5
+ switch (protocol) {
6
+ case 'openai':
7
+ return new OpenAIAdapter();
8
+ case 'anthropic':
9
+ return new AnthropicAdapter();
10
+ case 'gemini':
11
+ return new GeminiAdapter();
12
+ default:
13
+ throw new Error(`Unsupported protocol: ${protocol}`);
14
+ }
15
+ }
16
+ export { OpenAIAdapter, AnthropicAdapter, GeminiAdapter };
@@ -0,0 +1,86 @@
1
+ import { decryptApiKey } from '../crypto.js';
2
+ export class OpenAIAdapter {
3
+ async forwardRequest(model, request) {
4
+ const apiKey = decryptApiKey(model.provider.api_key);
5
+ const baseUrl = model.provider.base_url || 'https://api.openai.com/v1';
6
+ // Build the target URL
7
+ let targetPath = request.path;
8
+ if (targetPath.startsWith('/v1/')) {
9
+ targetPath = targetPath.substring(4);
10
+ }
11
+ const url = `${baseUrl}/${targetPath}`;
12
+ // Prepare headers
13
+ const headers = {
14
+ 'Authorization': `Bearer ${apiKey}`,
15
+ 'Content-Type': 'application/json',
16
+ ...request.headers,
17
+ };
18
+ delete headers['host'];
19
+ delete headers['connection'];
20
+ // Make the request
21
+ const response = await fetch(url, {
22
+ method: request.method,
23
+ headers,
24
+ body: request.method !== 'GET' && request.method !== 'HEAD' ? JSON.stringify(request.body) : undefined,
25
+ });
26
+ const responseBody = await response.text();
27
+ let parsedBody;
28
+ try {
29
+ parsedBody = JSON.parse(responseBody);
30
+ }
31
+ catch {
32
+ parsedBody = responseBody;
33
+ }
34
+ return {
35
+ status: response.status,
36
+ headers: Object.fromEntries(response.headers.entries()),
37
+ body: parsedBody,
38
+ };
39
+ }
40
+ async listModels(provider) {
41
+ const apiKey = decryptApiKey(provider.api_key);
42
+ let baseUrl = provider.base_url || 'https://api.openai.com/v1';
43
+ // Ensure baseUrl doesn't end with / and has /v1
44
+ baseUrl = baseUrl.trim().replace(/\/+$/, ''); // Remove trailing slashes
45
+ if (!baseUrl.endsWith('/v1')) {
46
+ baseUrl = baseUrl.endsWith('/') ? baseUrl + 'v1' : baseUrl + '/v1';
47
+ }
48
+ const url = `${baseUrl}/models`;
49
+ console.log('[OpenAI] Fetching models:', {
50
+ baseUrl: provider.base_url,
51
+ normalizedBaseUrl: baseUrl,
52
+ url,
53
+ apiKeyPrefix: apiKey.substring(0, 10) + '...',
54
+ apiKeyLength: apiKey.length,
55
+ });
56
+ try {
57
+ const response = await fetch(url, {
58
+ headers: {
59
+ 'Authorization': `Bearer ${apiKey}`,
60
+ 'Content-Type': 'application/json',
61
+ },
62
+ });
63
+ console.log('[OpenAI] Response status:', response.status, response.statusText);
64
+ if (!response.ok) {
65
+ const errorText = await response.text();
66
+ console.error('[OpenAI] Error response body:', errorText);
67
+ throw new Error(`Failed to fetch models: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);
68
+ }
69
+ const data = await response.json();
70
+ console.log('[OpenAI] Fetched models count:', data.data?.length || 0);
71
+ return (data.data || []).map((model) => ({
72
+ id: model.id,
73
+ name: model.id, // OpenAI uses model ID as name
74
+ }));
75
+ }
76
+ catch (error) {
77
+ console.error('[OpenAI] Error fetching models:', {
78
+ error: error instanceof Error ? error.message : String(error),
79
+ stack: error instanceof Error ? error.stack : undefined,
80
+ url,
81
+ baseUrl: provider.base_url,
82
+ });
83
+ throw error;
84
+ }
85
+ }
86
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,286 @@
1
+ import { spawn, exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { getServiceStatus, setServiceStatus, updateServiceStatus } from '../db/queries.js';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import net from 'net';
7
+ const execAsync = promisify(exec);
8
+ class ServiceManager {
9
+ constructor() {
10
+ this.childProcess = null;
11
+ this.isStarting = false;
12
+ }
13
+ /**
14
+ * Check if a process with the given PID exists
15
+ */
16
+ async checkProcessExists(pid) {
17
+ try {
18
+ // Use 'ps' command to check if process exists
19
+ // This works on Unix-like systems (macOS, Linux)
20
+ await execAsync(`ps -p ${pid} -o pid=`);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /**
28
+ * Check if a port is available
29
+ */
30
+ async checkPortAvailable(port) {
31
+ return new Promise((resolve) => {
32
+ const server = net.createServer();
33
+ server.listen(port, () => {
34
+ server.once('close', () => resolve(true));
35
+ server.close();
36
+ });
37
+ server.on('error', () => {
38
+ resolve(false);
39
+ });
40
+ });
41
+ }
42
+ /**
43
+ * Check if a port is in use and get process info
44
+ */
45
+ async checkPortInUse(port) {
46
+ return new Promise((resolve) => {
47
+ const server = net.createServer();
48
+ server.listen(port, () => {
49
+ server.once('close', () => resolve({ inUse: false }));
50
+ server.close();
51
+ });
52
+ server.on('error', (err) => {
53
+ if (err.code === 'EADDRINUSE') {
54
+ // Try to get process info
55
+ execAsync(`lsof -ti:${port} 2>/dev/null || echo ''`)
56
+ .then(({ stdout }) => {
57
+ const pid = stdout.trim();
58
+ if (pid) {
59
+ resolve({ inUse: true, processInfo: `Port ${port} is in use by process ${pid}` });
60
+ }
61
+ else {
62
+ resolve({ inUse: true, processInfo: `Port ${port} is already in use` });
63
+ }
64
+ })
65
+ .catch(() => {
66
+ resolve({ inUse: true, processInfo: `Port ${port} is already in use` });
67
+ });
68
+ }
69
+ else {
70
+ resolve({ inUse: false });
71
+ }
72
+ });
73
+ });
74
+ }
75
+ /**
76
+ * Get current service status, verifying process existence
77
+ */
78
+ async getStatus() {
79
+ const dbStatus = getServiceStatus();
80
+ if (!dbStatus) {
81
+ return { status: 'stopped' };
82
+ }
83
+ // If status is running, verify process actually exists
84
+ if (dbStatus.status === 'running' && dbStatus.pid) {
85
+ const processExists = await this.checkProcessExists(dbStatus.pid);
86
+ if (!processExists) {
87
+ // Process doesn't exist, update database
88
+ updateServiceStatus({ status: 'stopped', pid: null });
89
+ return { status: 'stopped' };
90
+ }
91
+ }
92
+ return {
93
+ status: dbStatus.status,
94
+ port: dbStatus.port,
95
+ pid: dbStatus.pid,
96
+ started_at: dbStatus.started_at,
97
+ };
98
+ }
99
+ /**
100
+ * Start the gateway service
101
+ */
102
+ async start(port) {
103
+ // Prevent concurrent starts
104
+ if (this.isStarting) {
105
+ return { status: 'stopped', error: 'Service is already starting' };
106
+ }
107
+ // Check if service is already running
108
+ const currentStatus = await this.getStatus();
109
+ if (currentStatus.status === 'running') {
110
+ return { status: 'running', error: 'Service is already running', port: currentStatus.port, pid: currentStatus.pid };
111
+ }
112
+ // Check port availability with more details
113
+ const portCheck = await this.checkPortInUse(port);
114
+ if (portCheck.inUse) {
115
+ // In development, if port 3000 is in use, suggest using a different port
116
+ const isDev = process.env.NODE_ENV !== 'production';
117
+ if (isDev && port === 3000) {
118
+ return {
119
+ status: 'stopped',
120
+ error: `Port 3000 is already in use (likely by the development server). Please configure a different port (e.g., 3001) in the settings.`
121
+ };
122
+ }
123
+ return { status: 'stopped', error: portCheck.processInfo || `Port ${port} is already in use` };
124
+ }
125
+ this.isStarting = true;
126
+ try {
127
+ const cliPath = path.join(process.cwd(), 'dist', 'src', 'cli', 'index.js');
128
+ const tsCliPath = path.join(process.cwd(), 'src', 'cli', 'index.ts');
129
+ let child;
130
+ // Create isolated environment for child process
131
+ // Remove Next.js dev server specific env vars to avoid conflicts
132
+ const childEnv = { ...process.env };
133
+ // Remove PORT if it might conflict
134
+ if (childEnv.PORT && parseInt(childEnv.PORT) === 3000) {
135
+ delete childEnv.PORT;
136
+ }
137
+ // Remove Next.js specific env vars that might cause conflicts
138
+ delete childEnv.NEXT_TELEMETRY_DISABLED;
139
+ // Gateway server doesn't need Next.js, so we can use any NODE_ENV
140
+ childEnv.NODE_ENV = process.env.NODE_ENV || 'production';
141
+ // Check if compiled code exists
142
+ if (fs.existsSync(cliPath)) {
143
+ // Use compiled JavaScript
144
+ child = spawn(process.execPath, [cliPath, 'start', '-p', port.toString()], {
145
+ detached: false, // Keep attached for proper tracking
146
+ stdio: ['ignore', 'pipe', 'pipe'],
147
+ cwd: process.cwd(),
148
+ env: childEnv,
149
+ });
150
+ }
151
+ else if (fs.existsSync(tsCliPath)) {
152
+ // In development, try to use tsx via npx
153
+ // Note: tsx should be installed as dev dependency for this to work
154
+ child = spawn('npx', ['--yes', 'tsx', tsCliPath, 'start', '-p', port.toString()], {
155
+ detached: false, // Keep attached for proper tracking
156
+ stdio: ['ignore', 'pipe', 'pipe'],
157
+ cwd: process.cwd(),
158
+ env: childEnv,
159
+ shell: true, // Use shell to resolve npx
160
+ });
161
+ }
162
+ else {
163
+ this.isStarting = false;
164
+ return {
165
+ status: 'stopped',
166
+ error: 'CLI not found. Please run "npm run build" first, or ensure src/cli/index.ts exists.'
167
+ };
168
+ }
169
+ this.childProcess = child;
170
+ // Collect error output for better error messages
171
+ let errorOutput = '';
172
+ let hasExited = false;
173
+ let exitCode = null;
174
+ // Handle process output (optional, for debugging)
175
+ // Use setImmediate to avoid blocking the event loop
176
+ child.stdout?.on('data', (data) => {
177
+ setImmediate(() => {
178
+ const output = data.toString();
179
+ // Only log if it's not empty and not just whitespace
180
+ if (output.trim()) {
181
+ console.log(`[Gateway Service] ${output}`);
182
+ }
183
+ });
184
+ });
185
+ child.stderr?.on('data', (data) => {
186
+ setImmediate(() => {
187
+ const errorText = data.toString();
188
+ console.error(`[Gateway Service Error] ${errorText}`);
189
+ errorOutput += errorText;
190
+ });
191
+ });
192
+ // Handle process exit
193
+ child.on('exit', (code, signal) => {
194
+ hasExited = true;
195
+ exitCode = code;
196
+ console.log(`[Gateway Service] Process exited with code ${code}, signal ${signal}`);
197
+ this.childProcess = null;
198
+ this.isStarting = false;
199
+ // Update database status
200
+ updateServiceStatus({ status: 'stopped', pid: null });
201
+ });
202
+ // Wait a bit to see if process starts successfully
203
+ await new Promise((resolve) => setTimeout(resolve, 2000));
204
+ // Check if process exited during startup
205
+ if (hasExited || child.killed || child.exitCode !== null) {
206
+ this.isStarting = false;
207
+ this.childProcess = null;
208
+ // Extract meaningful error message
209
+ let errorMessage = 'Service failed to start';
210
+ if (errorOutput) {
211
+ // Try to extract the main error message
212
+ const errorMatch = errorOutput.match(/Error: ([^\n]+)/);
213
+ if (errorMatch) {
214
+ errorMessage = errorMatch[1];
215
+ }
216
+ else if (errorOutput.includes('production build')) {
217
+ errorMessage = 'Production build not found. Please run "npm run build" first, or use development mode.';
218
+ }
219
+ else {
220
+ // Use first meaningful line of error
221
+ const lines = errorOutput.split('\n').filter(line => line.trim());
222
+ if (lines.length > 0) {
223
+ errorMessage = lines[0].substring(0, 200); // Limit length
224
+ }
225
+ }
226
+ }
227
+ return { status: 'stopped', error: errorMessage };
228
+ }
229
+ // Save status to database
230
+ const pid = child.pid || null;
231
+ const startedAt = new Date().toISOString();
232
+ setServiceStatus({
233
+ status: 'running',
234
+ port,
235
+ pid,
236
+ started_at: startedAt,
237
+ });
238
+ this.isStarting = false;
239
+ return {
240
+ status: 'running',
241
+ port,
242
+ pid,
243
+ started_at: startedAt,
244
+ };
245
+ }
246
+ catch (error) {
247
+ this.isStarting = false;
248
+ this.childProcess = null;
249
+ return { status: 'stopped', error: error.message || 'Failed to start service' };
250
+ }
251
+ }
252
+ /**
253
+ * Stop the gateway service
254
+ */
255
+ async stop() {
256
+ const currentStatus = await this.getStatus();
257
+ if (currentStatus.status === 'stopped') {
258
+ return { status: 'stopped' };
259
+ }
260
+ try {
261
+ // If we have a reference to the child process, kill it
262
+ if (this.childProcess) {
263
+ this.childProcess.kill('SIGTERM');
264
+ this.childProcess = null;
265
+ }
266
+ else if (currentStatus.pid) {
267
+ // Try to kill by PID
268
+ try {
269
+ process.kill(currentStatus.pid, 'SIGTERM');
270
+ }
271
+ catch (error) {
272
+ // Process might not exist
273
+ console.warn(`Failed to kill process ${currentStatus.pid}:`, error);
274
+ }
275
+ }
276
+ // Update database status
277
+ updateServiceStatus({ status: 'stopped', pid: null });
278
+ return { status: 'stopped' };
279
+ }
280
+ catch (error) {
281
+ return { status: 'stopped', error: error.message || 'Failed to stop service' };
282
+ }
283
+ }
284
+ }
285
+ // Export singleton instance
286
+ export const serviceManager = new ServiceManager();
package/docs/TODO.md ADDED
@@ -0,0 +1,19 @@
1
+ # TODO
2
+
3
+ ## 2026-01-11
4
+
5
+ - [ ] [需求] 模型列表支持按供应商分类,支持搜索
6
+ - [ ] [修复] 一键拉取模型出现了 成功拉取了undefine个模型的提示
7
+ - [ ] [需求] 供应商测试连接应该只能选取该供应商下的模型,并支持搜索
8
+ - [ ] [需求] 开启AI网关之后,支持点击测试按钮进行测试,模型可以搜索选择,可以给出示例链接配置,以 json 的格式
9
+
10
+ ## 2026-01-10
11
+
12
+ - [x] [需求] 需要简化 cli 指令为 aar
13
+ - [x] [优化] 一键拉取模型列表 名称改为 快捷拉取
14
+ - [x] [优化] 供应商和网关配置 API Key 需要加上小眼睛切换按钮,方便查看和拷贝
15
+ - [x] [修复] 供应商 API Key 无法保存的问题修复
16
+ - [x] [优化] Toast 的位置需要水平居中
17
+ - [x] [需求] 数据库存储到用户目录下,路径如 `~/.aar/`
18
+ - [x] [需求] 点击启动之后,后台服务需要保持启动状态,不能因为刷新页面就没了
19
+ - [x] [需求] 供应商添加测试连接按钮,可下拉选择某个模型进行测试连接
package/next.config.js ADDED
@@ -0,0 +1,7 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // Only use standalone output in production
4
+ ...(process.env.NODE_ENV === 'production' && { output: 'standalone' }),
5
+ }
6
+
7
+ module.exports = nextConfig