ai-agent-router 0.1.21 → 0.2.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 (133) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +2 -2
  3. package/.next/fallback-build-manifest.json +2 -2
  4. package/.next/server/app/_global-error.html +2 -2
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/api/config/route.js.nft.json +1 -1
  20. package/.next/server/app/api/gateway/[...path]/route.js.nft.json +1 -1
  21. package/.next/server/app/api/gateway/models/route.js.nft.json +1 -1
  22. package/.next/server/app/api/gateway/route.js.nft.json +1 -1
  23. package/.next/server/app/api/ide/claude/apply/route.js.nft.json +1 -1
  24. package/.next/server/app/api/ide/claude/available-models/route.js.nft.json +1 -1
  25. package/.next/server/app/api/ide/claude/save/route.js.nft.json +1 -1
  26. package/.next/server/app/api/ide/claude/status/route.js.nft.json +1 -1
  27. package/.next/server/app/api/ide/claude/test/route.js.nft.json +1 -1
  28. package/.next/server/app/api/logs/route.js.nft.json +1 -1
  29. package/.next/server/app/api/models/route.js.nft.json +1 -1
  30. package/.next/server/app/api/providers/route.js.nft.json +1 -1
  31. package/.next/server/app/api/providers/test/route.js.nft.json +1 -1
  32. package/.next/server/app/api/service/force-stop/route.js.nft.json +1 -1
  33. package/.next/server/app/api/service/start/route.js.nft.json +1 -1
  34. package/.next/server/app/api/service/status/route.js.nft.json +1 -1
  35. package/.next/server/app/api/service/stop/route.js.nft.json +1 -1
  36. package/.next/server/app/ide.html +1 -1
  37. package/.next/server/app/ide.rsc +1 -1
  38. package/.next/server/app/ide.segments/_full.segment.rsc +1 -1
  39. package/.next/server/app/ide.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/ide.segments/_index.segment.rsc +1 -1
  41. package/.next/server/app/ide.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/ide.segments/ide/__PAGE__.segment.rsc +1 -1
  43. package/.next/server/app/ide.segments/ide.segment.rsc +1 -1
  44. package/.next/server/app/index.html +1 -1
  45. package/.next/server/app/index.rsc +1 -1
  46. package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  47. package/.next/server/app/index.segments/_full.segment.rsc +1 -1
  48. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  49. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  50. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  51. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  52. package/.next/server/app/logs.html +1 -1
  53. package/.next/server/app/logs.rsc +2 -2
  54. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  55. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  56. package/.next/server/app/logs.segments/_index.segment.rsc +1 -1
  57. package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
  58. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +2 -2
  59. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  60. package/.next/server/app/models.html +1 -1
  61. package/.next/server/app/models.rsc +1 -1
  62. package/.next/server/app/models.segments/_full.segment.rsc +1 -1
  63. package/.next/server/app/models.segments/_head.segment.rsc +1 -1
  64. package/.next/server/app/models.segments/_index.segment.rsc +1 -1
  65. package/.next/server/app/models.segments/_tree.segment.rsc +1 -1
  66. package/.next/server/app/models.segments/models/__PAGE__.segment.rsc +1 -1
  67. package/.next/server/app/models.segments/models.segment.rsc +1 -1
  68. package/.next/server/app/providers.html +1 -1
  69. package/.next/server/app/providers.rsc +1 -1
  70. package/.next/server/app/providers.segments/_full.segment.rsc +1 -1
  71. package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
  72. package/.next/server/app/providers.segments/_index.segment.rsc +1 -1
  73. package/.next/server/app/providers.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
  75. package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
  76. package/.next/server/chunks/[root-of-the-server]__1480f018._.js +1 -1
  77. package/.next/server/chunks/[root-of-the-server]__1480f018._.js.map +1 -1
  78. package/.next/server/chunks/[root-of-the-server]__1909f3aa._.js +1 -1
  79. package/.next/server/chunks/[root-of-the-server]__1909f3aa._.js.map +1 -1
  80. package/.next/server/chunks/[root-of-the-server]__1d4b7fc5._.js +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__1d4b7fc5._.js.map +1 -1
  82. package/.next/server/chunks/[root-of-the-server]__372ef2bf._.js +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__372ef2bf._.js.map +1 -1
  84. package/.next/server/chunks/[root-of-the-server]__3aaf963c._.js +1 -1
  85. package/.next/server/chunks/[root-of-the-server]__3aaf963c._.js.map +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__6ce199d2._.js +1 -1
  87. package/.next/server/chunks/[root-of-the-server]__6ce199d2._.js.map +1 -1
  88. package/.next/server/chunks/[root-of-the-server]__772134c6._.js +1 -1
  89. package/.next/server/chunks/[root-of-the-server]__772134c6._.js.map +1 -1
  90. package/.next/server/chunks/[root-of-the-server]__7b77f523._.js +1 -1
  91. package/.next/server/chunks/[root-of-the-server]__7b77f523._.js.map +1 -1
  92. package/.next/server/chunks/[root-of-the-server]__c1b4b601._.js +18 -18
  93. package/.next/server/chunks/[root-of-the-server]__c1b4b601._.js.map +1 -1
  94. package/.next/server/chunks/[root-of-the-server]__ccfc7f1d._.js +1 -1
  95. package/.next/server/chunks/[root-of-the-server]__ccfc7f1d._.js.map +1 -1
  96. package/.next/server/chunks/ssr/src_app_logs_page_tsx_7b7b7b83._.js +1 -1
  97. package/.next/server/chunks/ssr/src_app_logs_page_tsx_7b7b7b83._.js.map +1 -1
  98. package/.next/server/pages/404.html +1 -1
  99. package/.next/server/pages/500.html +2 -2
  100. package/.next/static/chunks/{81c904164fe81379.js → b6b258e8582e47c4.js} +1 -1
  101. package/README.md +100 -111
  102. package/dist/src/app/api/gateway/[...path]/route.js +1 -1
  103. package/dist/src/app/api/gateway/route.js +1 -1
  104. package/dist/src/app/api/logs/route.js +2 -2
  105. package/dist/src/app/api/models/route.js +5 -5
  106. package/dist/src/app/api/providers/route.js +4 -4
  107. package/dist/src/app/api/providers/test/route.js +1 -1
  108. package/dist/src/app/api/service/start/route.js +1 -1
  109. package/dist/src/app/api/service/status/route.js +1 -1
  110. package/dist/src/app/api/service/stop/route.js +1 -1
  111. package/dist/src/app/logs/page.js +13 -1
  112. package/dist/src/cli/index.js +218 -20
  113. package/dist/src/db/database.js +35 -1
  114. package/dist/src/db/queries.js +6 -6
  115. package/dist/src/server/logger.js +22 -4
  116. package/package.json +2 -1
  117. package/src/app/api/gateway/[...path]/route.ts +1 -1
  118. package/src/app/api/gateway/route.ts +1 -1
  119. package/src/app/api/logs/route.ts +2 -2
  120. package/src/app/api/models/route.ts +5 -5
  121. package/src/app/api/providers/route.ts +4 -4
  122. package/src/app/api/providers/test/route.ts +1 -1
  123. package/src/app/api/service/start/route.ts +1 -1
  124. package/src/app/api/service/status/route.ts +1 -1
  125. package/src/app/api/service/stop/route.ts +1 -1
  126. package/src/app/logs/page.tsx +15 -5
  127. package/src/cli/index.ts +228 -25
  128. package/src/db/database.ts +34 -4
  129. package/src/db/queries.ts +6 -6
  130. package/src/server/logger.ts +19 -4
  131. /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_buildManifest.js +0 -0
  132. /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_clientMiddlewareManifest.json +0 -0
  133. /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_ssgManifest.js +0 -0
@@ -7,51 +7,249 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const commander_1 = require("commander");
8
8
  const child_process_1 = require("child_process");
9
9
  const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const http_1 = __importDefault(require("http"));
10
13
  const database_1 = require("../db/database");
11
14
  const queries_1 = require("../db/queries");
12
15
  const program = new commander_1.Command();
13
16
  const packageJsonPath = require.resolve('../../../package.json');
14
17
  const packageJson = require(packageJsonPath);
18
+ const AAR_DIR = path_1.default.join(os_1.default.homedir(), '.aar');
19
+ const UI_PID_FILE = path_1.default.join(AAR_DIR, 'ui.pid');
20
+ const GATEWAY_PID_FILE = path_1.default.join(AAR_DIR, 'gateway.pid');
21
+ const GATEWAY_PORT_FILE = path_1.default.join(AAR_DIR, 'gateway.port');
22
+ function ensureAarDir() {
23
+ if (!fs_1.default.existsSync(AAR_DIR)) {
24
+ fs_1.default.mkdirSync(AAR_DIR, { recursive: true });
25
+ }
26
+ }
27
+ function getPackageRoot() {
28
+ return path_1.default.dirname(path_1.default.dirname(path_1.default.dirname(__dirname)));
29
+ }
30
+ /** Wait for HTTP server to respond (e.g. UI or gateway ready) */
31
+ function waitForReady(baseUrl, maxWaitMs = 30000) {
32
+ const url = new URL(baseUrl);
33
+ return new Promise((resolve) => {
34
+ const start = Date.now();
35
+ const tryOnce = () => {
36
+ const req = http_1.default.request({
37
+ hostname: url.hostname,
38
+ port: url.port || '80',
39
+ path: url.pathname || '/',
40
+ method: 'GET',
41
+ timeout: 2000,
42
+ }, (res) => {
43
+ resolve(res.statusCode !== undefined && res.statusCode < 500);
44
+ });
45
+ req.on('error', () => {
46
+ if (Date.now() - start >= maxWaitMs) {
47
+ resolve(false);
48
+ return;
49
+ }
50
+ setTimeout(tryOnce, 800);
51
+ });
52
+ req.on('timeout', () => {
53
+ req.destroy();
54
+ if (Date.now() - start >= maxWaitMs)
55
+ resolve(false);
56
+ else
57
+ setTimeout(tryOnce, 800);
58
+ });
59
+ req.end();
60
+ };
61
+ tryOnce();
62
+ });
63
+ }
64
+ function printStatus(uiPort, gatewayPort, gatewayRunning, background) {
65
+ const uiUrl = `http://localhost:${uiPort}`;
66
+ const gatewayUrl = `http://localhost:${gatewayPort}`;
67
+ console.log('');
68
+ console.log('-------------------------------------------');
69
+ console.log(' AI Agent Router');
70
+ console.log('-------------------------------------------');
71
+ console.log(` 前台 UI: ${uiUrl}`);
72
+ console.log(` 网关地址: ${gatewayUrl}`);
73
+ console.log(` 网关状态: ${gatewayRunning ? '运行中' : '未启动'}`);
74
+ if (background) {
75
+ console.log(' 运行模式: 后台运行(关闭终端不影响)');
76
+ console.log(' 停止服务: aar stop');
77
+ }
78
+ console.log('-------------------------------------------');
79
+ console.log('');
80
+ }
15
81
  program
16
82
  .name('aar')
17
83
  .description('AI Agent Router - Web UI for managing the API gateway')
18
84
  .version(packageJson.version);
19
85
  program
20
86
  .command('start')
21
- .description('Start the Web UI management interface')
87
+ .description('Start the Web UI and gateway (default: both; use --no-gateway for UI only)')
22
88
  .option('-p, --port <port>', 'Port for Web UI', '9527')
89
+ .option('-g, --gateway-port <port>', 'Port for gateway (default: from config or 1357)')
90
+ .option('--no-gateway', 'Only start Web UI, do not start gateway')
91
+ .option('--no-background', 'Run in foreground (attach to terminal)')
23
92
  .action(async (options) => {
24
- const port = parseInt(options.port || '9527');
25
- console.log(`Starting AI Agent Router Web UI`);
26
- console.log(` Port: ${port}`);
27
- console.log(` Access the UI at: http://localhost:${port}`);
28
- console.log('');
29
- // Get the package root directory (where package.json is located)
30
- const packageRoot = path_1.default.dirname(path_1.default.dirname(path_1.default.dirname(__dirname)));
31
- // Start Web UI using Next.js
32
- // Always use production mode for globally installed package
93
+ const uiPort = parseInt(options.port || '9527', 10);
94
+ const startGateway = options.gateway !== false;
95
+ const background = options.background !== false;
96
+ const packageRoot = getPackageRoot();
97
+ let gatewayPort = 1357;
98
+ if (startGateway) {
99
+ try {
100
+ await (0, database_1.getDatabase)();
101
+ const portConfig = await (0, queries_1.getConfig)('port');
102
+ gatewayPort = options.gatewayPort
103
+ ? parseInt(options.gatewayPort, 10)
104
+ : parseInt(portConfig?.value || '1357', 10);
105
+ }
106
+ catch (e) {
107
+ gatewayPort = options.gatewayPort ? parseInt(options.gatewayPort, 10) : 1357;
108
+ }
109
+ }
33
110
  const serverPath = path_1.default.join(packageRoot, 'node_modules', 'next', 'dist', 'bin', 'next');
34
- const uiProcess = (0, child_process_1.spawn)(process.execPath, [serverPath, 'start', '-p', port.toString()], {
111
+ const gatewayScriptPath = path_1.default.join(packageRoot, 'dist', 'src', 'cli', 'gateway-server.js');
112
+ if (background) {
113
+ ensureAarDir();
114
+ const envBase = { ...process.env, NODE_ENV: 'production' };
115
+ const uiProc = (0, child_process_1.spawn)(process.execPath, [serverPath, 'start', '-p', uiPort.toString()], {
116
+ cwd: packageRoot,
117
+ env: { ...envBase, PORT: uiPort.toString() },
118
+ detached: true,
119
+ stdio: 'ignore',
120
+ });
121
+ uiProc.unref();
122
+ if (uiProc.pid) {
123
+ fs_1.default.writeFileSync(UI_PID_FILE, String(uiProc.pid));
124
+ }
125
+ let gatewayRunning = false;
126
+ if (startGateway && fs_1.default.existsSync(gatewayScriptPath)) {
127
+ const gwProc = (0, child_process_1.spawn)(process.execPath, [gatewayScriptPath, '--port', String(gatewayPort)], {
128
+ cwd: packageRoot,
129
+ env: envBase,
130
+ detached: true,
131
+ stdio: 'ignore',
132
+ });
133
+ gwProc.unref();
134
+ if (gwProc.pid) {
135
+ fs_1.default.writeFileSync(GATEWAY_PID_FILE, String(gwProc.pid));
136
+ fs_1.default.writeFileSync(GATEWAY_PORT_FILE, String(gatewayPort));
137
+ gatewayRunning = true;
138
+ }
139
+ try {
140
+ await (0, database_1.getDatabase)();
141
+ await (0, queries_1.setServiceStatus)({
142
+ status: 'running',
143
+ port: gatewayPort,
144
+ pid: gwProc.pid ?? null,
145
+ started_at: new Date().toISOString(),
146
+ });
147
+ }
148
+ catch {
149
+ // ignore db errors
150
+ }
151
+ }
152
+ printStatus(uiPort, gatewayPort, gatewayRunning, true);
153
+ process.exit(0);
154
+ }
155
+ // Foreground: start UI first
156
+ console.log('Starting AI Agent Router...');
157
+ const uiProcess = (0, child_process_1.spawn)(process.execPath, [serverPath, 'start', '-p', uiPort.toString()], {
35
158
  cwd: packageRoot,
36
159
  stdio: ['ignore', 'inherit', 'inherit'],
37
- env: { ...process.env, PORT: port.toString(), NODE_ENV: 'production' },
38
- });
39
- // Handle UI process exit
40
- uiProcess.on('exit', (code) => {
41
- console.log(`Web UI process exited with code ${code}`);
42
- process.exit(code || 0);
160
+ env: { ...process.env, PORT: uiPort.toString(), NODE_ENV: 'production' },
43
161
  });
44
162
  uiProcess.on('error', (error) => {
45
163
  console.error(`Failed to start Web UI: ${error.message}`);
46
164
  process.exit(1);
47
165
  });
48
- // Keep the process alive
49
- process.on('SIGINT', () => {
50
- console.log('\nShutting down...');
166
+ const gatewayProcess = startGateway && fs_1.default.existsSync(gatewayScriptPath)
167
+ ? (0, child_process_1.spawn)(process.execPath, [gatewayScriptPath, '--port', String(gatewayPort)], {
168
+ cwd: packageRoot,
169
+ stdio: ['ignore', 'pipe', 'pipe'],
170
+ env: { ...process.env, NODE_ENV: 'production' },
171
+ })
172
+ : null;
173
+ if (gatewayProcess?.stdout) {
174
+ gatewayProcess.stdout.on('data', (d) => process.stdout.write(d));
175
+ }
176
+ if (gatewayProcess?.stderr) {
177
+ gatewayProcess.stderr.on('data', (d) => process.stderr.write(d));
178
+ }
179
+ const ready = await waitForReady(`http://localhost:${uiPort}`);
180
+ if (ready) {
181
+ printStatus(uiPort, gatewayPort, !!gatewayProcess, false);
182
+ }
183
+ const shutdown = (signal) => {
184
+ console.log(`\nShutting down (${signal})...`);
185
+ if (gatewayProcess?.pid) {
186
+ try {
187
+ process.kill(gatewayProcess.pid, 'SIGTERM');
188
+ }
189
+ catch {
190
+ // ignore
191
+ }
192
+ }
51
193
  uiProcess.kill('SIGTERM');
52
194
  process.exit(0);
195
+ };
196
+ process.on('SIGINT', () => shutdown('SIGINT'));
197
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
198
+ uiProcess.on('exit', (code) => {
199
+ if (gatewayProcess?.pid) {
200
+ try {
201
+ process.kill(gatewayProcess.pid, 'SIGTERM');
202
+ }
203
+ catch {
204
+ // ignore
205
+ }
206
+ }
207
+ process.exit(code ?? 0);
53
208
  });
54
209
  });
210
+ program
211
+ .command('stop')
212
+ .description('Stop AI Agent Router when running in background')
213
+ .action(async () => {
214
+ ensureAarDir();
215
+ let stopped = 0;
216
+ const killPidFile = (file, name) => {
217
+ if (!fs_1.default.existsSync(file))
218
+ return;
219
+ try {
220
+ const pid = parseInt(fs_1.default.readFileSync(file, 'utf8').trim(), 10);
221
+ if (!isNaN(pid)) {
222
+ try {
223
+ process.kill(pid, 'SIGTERM');
224
+ console.log(`Stopped ${name} (PID ${pid})`);
225
+ stopped++;
226
+ }
227
+ catch (e) {
228
+ if (e?.code !== 'ESRCH')
229
+ console.warn(`${name} (PID ${pid}): ${e.message}`);
230
+ }
231
+ }
232
+ fs_1.default.unlinkSync(file);
233
+ }
234
+ catch (e) {
235
+ // ignore
236
+ }
237
+ };
238
+ killPidFile(GATEWAY_PID_FILE, 'gateway');
239
+ killPidFile(UI_PID_FILE, 'Web UI');
240
+ if (fs_1.default.existsSync(GATEWAY_PORT_FILE))
241
+ fs_1.default.unlinkSync(GATEWAY_PORT_FILE);
242
+ try {
243
+ await (0, database_1.getDatabase)();
244
+ await (0, queries_1.updateServiceStatus)({ status: 'stopped', pid: null });
245
+ }
246
+ catch {
247
+ // ignore
248
+ }
249
+ if (stopped === 0) {
250
+ console.log('No background processes found (or already stopped).');
251
+ }
252
+ });
55
253
  program
56
254
  .command('config')
57
255
  .description('Manage gateway configuration')
@@ -17,6 +17,27 @@ const os_1 = __importDefault(require("os"));
17
17
  const DB_PATH = process.env.DB_PATH || path_1.default.join(os_1.default.homedir(), '.aar', 'gateway.db');
18
18
  let dbInstance = null;
19
19
  let sqlJsInstance = null;
20
+ /** 上次从磁盘加载 DB 的时间(用于多进程时发现磁盘被其他进程更新则重新加载) */
21
+ let lastLoadMtimeMs = 0;
22
+ /** 若磁盘上的 DB 被其他进程更新则重新加载,以便读到最新数据 */
23
+ function reloadFromFileIfNewer() {
24
+ if (!dbInstance || !fs_1.default.existsSync(DB_PATH))
25
+ return;
26
+ const mtime = fs_1.default.statSync(DB_PATH).mtimeMs;
27
+ if (mtime <= lastLoadMtimeMs)
28
+ return;
29
+ try {
30
+ const fresh = loadDatabase();
31
+ if (fresh) {
32
+ dbInstance.close();
33
+ dbInstance = fresh;
34
+ lastLoadMtimeMs = fs_1.default.statSync(DB_PATH).mtimeMs;
35
+ }
36
+ }
37
+ catch (e) {
38
+ console.warn('[Database] Reload from file failed:', e.message);
39
+ }
40
+ }
20
41
  async function initSqlJsEngine() {
21
42
  if (!sqlJsInstance) {
22
43
  sqlJsInstance = await (0, sql_js_1.default)({
@@ -39,6 +60,9 @@ function saveDatabase(db) {
39
60
  fs_1.default.mkdirSync(dbDir, { recursive: true });
40
61
  }
41
62
  fs_1.default.writeFileSync(DB_PATH, buffer);
63
+ if (fs_1.default.existsSync(DB_PATH)) {
64
+ lastLoadMtimeMs = fs_1.default.statSync(DB_PATH).mtimeMs;
65
+ }
42
66
  }
43
67
  catch (error) {
44
68
  console.error('Failed to save database:', error);
@@ -59,7 +83,16 @@ function loadDatabase() {
59
83
  async function getDatabase() {
60
84
  if (!dbInstance) {
61
85
  const engine = await initSqlJsEngine();
62
- dbInstance = loadDatabase() || new engine.Database();
86
+ const loaded = loadDatabase();
87
+ if (loaded) {
88
+ dbInstance = loaded;
89
+ if (fs_1.default.existsSync(DB_PATH))
90
+ lastLoadMtimeMs = fs_1.default.statSync(DB_PATH).mtimeMs;
91
+ }
92
+ else {
93
+ dbInstance = new engine.Database();
94
+ lastLoadMtimeMs = 0;
95
+ }
63
96
  try {
64
97
  dbInstance.run(schema_1.CREATE_TABLES_SQL);
65
98
  // Migration: Allow NULL model_id in request_logs table
@@ -113,6 +146,7 @@ async function getDatabase() {
113
146
  }
114
147
  }
115
148
  }
149
+ reloadFromFileIfNewer();
116
150
  return dbInstance;
117
151
  }
118
152
  async function closeDatabase() {
@@ -155,9 +155,9 @@ async function createRequestLog(log) {
155
155
  try {
156
156
  const result = await (0, database_1.run)(`INSERT INTO request_logs (
157
157
  model_id, request_method, request_path, request_headers,
158
- request_query, request_body, response_status, response_body, response_time_ms
159
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
160
- log.model_id || null,
158
+ request_query, request_body, response_status, response_body, response_time_ms, created_at
159
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`, [
160
+ log.model_id === undefined ? null : log.model_id,
161
161
  log.request_method,
162
162
  log.request_path,
163
163
  log.request_headers,
@@ -178,9 +178,9 @@ async function createRequestLog(log) {
178
178
  console.warn(`[RequestLog] Foreign key constraint failed for model_id ${log.model_id}, retrying with NULL`);
179
179
  const result = await (0, database_1.run)(`INSERT INTO request_logs (
180
180
  model_id, request_method, request_path, request_headers,
181
- request_query, request_body, response_status, response_body, response_time_ms
182
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
183
- null,
181
+ request_query, request_body, response_status, response_body, response_time_ms, created_at
182
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`, [
183
+ log.model_id === undefined ? null : log.model_id,
184
184
  log.request_method,
185
185
  log.request_path,
186
186
  log.request_headers,
@@ -3,6 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.logRequest = logRequest;
4
4
  const queries_1 = require("../db/queries");
5
5
  const crypto_1 = require("./crypto");
6
+ /** Safe JSON.stringify that never throws; use for logging arbitrary response/request bodies. */
7
+ function safeStringify(value) {
8
+ if (value === undefined)
9
+ return '';
10
+ if (value === null)
11
+ return 'null';
12
+ try {
13
+ return JSON.stringify(value);
14
+ }
15
+ catch {
16
+ try {
17
+ return String(value);
18
+ }
19
+ catch {
20
+ return '[Non-serializable]';
21
+ }
22
+ }
23
+ }
6
24
  async function logRequest(request, response) {
7
25
  try {
8
26
  // Mask sensitive information
@@ -12,11 +30,11 @@ async function logRequest(request, response) {
12
30
  model_id: request.modelId,
13
31
  request_method: request.method,
14
32
  request_path: request.path,
15
- request_headers: JSON.stringify(maskedHeaders),
16
- request_query: JSON.stringify(request.query),
17
- request_body: JSON.stringify(maskedBody),
33
+ request_headers: safeStringify(maskedHeaders),
34
+ request_query: safeStringify(request.query),
35
+ request_body: safeStringify(maskedBody),
18
36
  response_status: response.status,
19
- response_body: JSON.stringify(response.body),
37
+ response_body: safeStringify(response.body),
20
38
  response_time_ms: response.responseTimeMs,
21
39
  });
22
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agent-router",
3
- "version": "0.1.21",
3
+ "version": "0.2.0",
4
4
  "description": "A unified API gateway for managing multiple AI model providers (Anthropic, OpenAI, Gemini, etc.)",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "dev": "next dev -p 9527",
11
11
  "build": "next build && tsc",
12
12
  "start": "node dist/src/cli/index.js start",
13
+ "stop": "node dist/src/cli/index.js stop",
13
14
  "lint": "eslint .",
14
15
  "pub": "npm run build && npm publish",
15
16
  "type-check": "tsc --noEmit"
@@ -51,7 +51,7 @@ async function handleRequest(
51
51
  method: string
52
52
  ) {
53
53
  try {
54
- getDatabase();
54
+ await getDatabase();
55
55
 
56
56
  const path = params.path.join('/');
57
57
  const searchParams = request.nextUrl.searchParams;
@@ -19,7 +19,7 @@ async function handleGatewayRequestDirect(
19
19
  method: string
20
20
  ) {
21
21
  try {
22
- getDatabase();
22
+ await getDatabase();
23
23
  const searchParams = request.nextUrl.searchParams;
24
24
  let modelId = searchParams.get('model') || searchParams.get('model_id');
25
25
  const providerName = searchParams.get('provider');
@@ -34,7 +34,7 @@ function cleanLogData(log: any) {
34
34
 
35
35
  export async function GET(request: NextRequest) {
36
36
  try {
37
- getDatabase();
37
+ await getDatabase();
38
38
  const { searchParams } = new URL(request.url);
39
39
  const id = searchParams.get('id');
40
40
  const limit = parseInt(searchParams.get('limit') || '100');
@@ -79,7 +79,7 @@ export async function GET(request: NextRequest) {
79
79
 
80
80
  export async function DELETE(request: NextRequest) {
81
81
  try {
82
- getDatabase();
82
+ await getDatabase();
83
83
  const { searchParams } = new URL(request.url);
84
84
  const ids = searchParams.get('ids');
85
85
  const clearAll = searchParams.get('clear_all') === 'true';
@@ -16,7 +16,7 @@ export const runtime = 'nodejs';
16
16
 
17
17
  export async function GET(request: NextRequest) {
18
18
  try {
19
- getDatabase();
19
+ await getDatabase();
20
20
  const { searchParams } = new URL(request.url);
21
21
  const providerId = searchParams.get('provider_id');
22
22
  const enabledOnly = searchParams.get('enabled');
@@ -47,7 +47,7 @@ export async function GET(request: NextRequest) {
47
47
 
48
48
  export async function POST(request: NextRequest) {
49
49
  try {
50
- getDatabase();
50
+ await getDatabase();
51
51
  const body = await request.json();
52
52
  const { provider_id, name, model_id, enabled } = body;
53
53
 
@@ -77,7 +77,7 @@ export async function POST(request: NextRequest) {
77
77
 
78
78
  export async function PUT(request: NextRequest) {
79
79
  try {
80
- getDatabase();
80
+ await getDatabase();
81
81
  const body = await request.json();
82
82
  const { id, name, model_id, enabled, provider_id } = body;
83
83
 
@@ -114,7 +114,7 @@ export async function PUT(request: NextRequest) {
114
114
 
115
115
  export async function DELETE(request: NextRequest) {
116
116
  try {
117
- getDatabase();
117
+ await getDatabase();
118
118
  const { searchParams } = new URL(request.url);
119
119
  const id = searchParams.get('id');
120
120
 
@@ -146,7 +146,7 @@ export async function DELETE(request: NextRequest) {
146
146
  // Fetch models from provider
147
147
  export async function PATCH(request: NextRequest) {
148
148
  try {
149
- getDatabase();
149
+ await getDatabase();
150
150
  const body = await request.json();
151
151
  const { provider_id } = body;
152
152
 
@@ -8,7 +8,7 @@ export const runtime = 'nodejs';
8
8
 
9
9
  export async function GET(request: NextRequest) {
10
10
  try {
11
- getDatabase();
11
+ await getDatabase();
12
12
  const { searchParams } = new URL(request.url);
13
13
  const id = searchParams.get('id');
14
14
  const includeKey = searchParams.get('includeKey') === 'true';
@@ -48,7 +48,7 @@ export async function GET(request: NextRequest) {
48
48
 
49
49
  export async function POST(request: NextRequest) {
50
50
  try {
51
- getDatabase();
51
+ await getDatabase();
52
52
  const body = await request.json();
53
53
  const { name, protocol, base_url, api_key } = body;
54
54
 
@@ -82,7 +82,7 @@ export async function POST(request: NextRequest) {
82
82
 
83
83
  export async function PUT(request: NextRequest) {
84
84
  try {
85
- getDatabase();
85
+ await getDatabase();
86
86
  const body = await request.json();
87
87
  const { id, name, protocol, base_url, api_key } = body;
88
88
 
@@ -132,7 +132,7 @@ export async function PUT(request: NextRequest) {
132
132
 
133
133
  export async function DELETE(request: NextRequest) {
134
134
  try {
135
- getDatabase();
135
+ await getDatabase();
136
136
  const { searchParams } = new URL(request.url);
137
137
  const id = searchParams.get('id');
138
138
 
@@ -9,7 +9,7 @@ export const runtime = 'nodejs';
9
9
 
10
10
  export async function POST(request: NextRequest) {
11
11
  try {
12
- getDatabase();
12
+ await getDatabase();
13
13
  const body = await request.json();
14
14
  const { provider_id, model_id } = body;
15
15
 
@@ -10,7 +10,7 @@ export const runtime = 'nodejs';
10
10
  export async function POST(request: NextRequest) {
11
11
  try {
12
12
  // Initialize database
13
- getDatabase();
13
+ await getDatabase();
14
14
 
15
15
  const body = await request.json().catch(() => ({}));
16
16
  const port = body.port ? parseInt(body.port, 10) : null;
@@ -8,7 +8,7 @@ export const runtime = 'nodejs';
8
8
  export async function GET(request: NextRequest) {
9
9
  try {
10
10
  // Initialize database
11
- getDatabase();
11
+ await getDatabase();
12
12
 
13
13
  const status = await serviceManager.getStatus();
14
14
  return NextResponse.json(status);
@@ -8,7 +8,7 @@ export const runtime = 'nodejs';
8
8
  export async function POST(request: NextRequest) {
9
9
  try {
10
10
  // Initialize database
11
- getDatabase();
11
+ await getDatabase();
12
12
 
13
13
  const result = await serviceManager.stop();
14
14
 
@@ -7,9 +7,9 @@ import ConfirmDialog from '../components/ConfirmDialog';
7
7
 
8
8
  interface RequestLog {
9
9
  id: number;
10
- model_id: number;
11
- model_name?: string;
12
- provider_name?: string;
10
+ model_id: number | null;
11
+ model_name?: string | null;
12
+ provider_name?: string | null;
13
13
  request_method: string;
14
14
  request_path: string;
15
15
  request_headers: string;
@@ -21,6 +21,17 @@ interface RequestLog {
21
21
  created_at: string;
22
22
  }
23
23
 
24
+ /** 将服务端返回的 created_at 格式化为本地显示。无时区后缀时按 UTC 解析再转本地,避免差 8 小时。 */
25
+ function formatCreatedAt(createdAt: string): string {
26
+ if (!createdAt || typeof createdAt !== 'string') return '';
27
+ const s = createdAt.trim();
28
+ if (!s) return '';
29
+ const hasTz = /[Z+-]\d{2}:?\d{2}$/.test(s);
30
+ const iso = hasTz ? s : s.replace(' ', 'T') + 'Z';
31
+ const date = new Date(iso);
32
+ return Number.isNaN(date.getTime()) ? s : date.toLocaleString('zh-CN');
33
+ }
34
+
24
35
  export default function LogsPage() {
25
36
  const [logs, setLogs] = useState<RequestLog[]>([]);
26
37
  const [selectedLog, setSelectedLog] = useState<RequestLog | null>(null);
@@ -413,8 +424,7 @@ export default function LogsPage() {
413
424
  </td>
414
425
  <td className="px-4 py-3 whitespace-nowrap">
415
426
  <div className="text-xs text-slate-600">
416
- {/* 使用本地时区显示时间 */}
417
- {new Date(log.created_at + 'Z').toLocaleString('zh-CN')}
427
+ {formatCreatedAt(log.created_at)}
418
428
  </div>
419
429
  </td>
420
430
  <td className="px-4 py-3 whitespace-nowrap">