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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/fallback-build-manifest.json +2 -2
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/gateway/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/gateway/models/route.js.nft.json +1 -1
- package/.next/server/app/api/gateway/route.js.nft.json +1 -1
- package/.next/server/app/api/ide/claude/apply/route.js.nft.json +1 -1
- package/.next/server/app/api/ide/claude/available-models/route.js.nft.json +1 -1
- package/.next/server/app/api/ide/claude/save/route.js.nft.json +1 -1
- package/.next/server/app/api/ide/claude/status/route.js.nft.json +1 -1
- package/.next/server/app/api/ide/claude/test/route.js.nft.json +1 -1
- package/.next/server/app/api/logs/route.js.nft.json +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/providers/route.js.nft.json +1 -1
- package/.next/server/app/api/providers/test/route.js.nft.json +1 -1
- package/.next/server/app/api/service/force-stop/route.js.nft.json +1 -1
- package/.next/server/app/api/service/start/route.js.nft.json +1 -1
- package/.next/server/app/api/service/status/route.js.nft.json +1 -1
- package/.next/server/app/api/service/stop/route.js.nft.json +1 -1
- package/.next/server/app/ide.html +1 -1
- package/.next/server/app/ide.rsc +1 -1
- package/.next/server/app/ide.segments/_full.segment.rsc +1 -1
- package/.next/server/app/ide.segments/_head.segment.rsc +1 -1
- package/.next/server/app/ide.segments/_index.segment.rsc +1 -1
- package/.next/server/app/ide.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/ide.segments/ide/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/ide.segments/ide.segment.rsc +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +1 -1
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/logs.html +1 -1
- package/.next/server/app/logs.rsc +2 -2
- package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
- package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_index.segment.rsc +1 -1
- package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
- package/.next/server/app/models.html +1 -1
- package/.next/server/app/models.rsc +1 -1
- package/.next/server/app/models.segments/_full.segment.rsc +1 -1
- package/.next/server/app/models.segments/_head.segment.rsc +1 -1
- package/.next/server/app/models.segments/_index.segment.rsc +1 -1
- package/.next/server/app/models.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/models.segments/models/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/models.segments/models.segment.rsc +1 -1
- package/.next/server/app/providers.html +1 -1
- package/.next/server/app/providers.rsc +1 -1
- package/.next/server/app/providers.segments/_full.segment.rsc +1 -1
- package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
- package/.next/server/app/providers.segments/_index.segment.rsc +1 -1
- package/.next/server/app/providers.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
- package/.next/server/chunks/[root-of-the-server]__1480f018._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1480f018._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__1909f3aa._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1909f3aa._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__1d4b7fc5._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1d4b7fc5._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__372ef2bf._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__372ef2bf._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__3aaf963c._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__3aaf963c._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__6ce199d2._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__6ce199d2._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__772134c6._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__772134c6._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__7b77f523._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__7b77f523._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__c1b4b601._.js +18 -18
- package/.next/server/chunks/[root-of-the-server]__c1b4b601._.js.map +1 -1
- package/.next/server/chunks/[root-of-the-server]__ccfc7f1d._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__ccfc7f1d._.js.map +1 -1
- package/.next/server/chunks/ssr/src_app_logs_page_tsx_7b7b7b83._.js +1 -1
- package/.next/server/chunks/ssr/src_app_logs_page_tsx_7b7b7b83._.js.map +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +2 -2
- package/.next/static/chunks/{81c904164fe81379.js → b6b258e8582e47c4.js} +1 -1
- package/README.md +100 -111
- package/dist/src/app/api/gateway/[...path]/route.js +1 -1
- package/dist/src/app/api/gateway/route.js +1 -1
- package/dist/src/app/api/logs/route.js +2 -2
- package/dist/src/app/api/models/route.js +5 -5
- package/dist/src/app/api/providers/route.js +4 -4
- package/dist/src/app/api/providers/test/route.js +1 -1
- package/dist/src/app/api/service/start/route.js +1 -1
- package/dist/src/app/api/service/status/route.js +1 -1
- package/dist/src/app/api/service/stop/route.js +1 -1
- package/dist/src/app/logs/page.js +13 -1
- package/dist/src/cli/index.js +218 -20
- package/dist/src/db/database.js +35 -1
- package/dist/src/db/queries.js +6 -6
- package/dist/src/server/logger.js +22 -4
- package/package.json +2 -1
- package/src/app/api/gateway/[...path]/route.ts +1 -1
- package/src/app/api/gateway/route.ts +1 -1
- package/src/app/api/logs/route.ts +2 -2
- package/src/app/api/models/route.ts +5 -5
- package/src/app/api/providers/route.ts +4 -4
- package/src/app/api/providers/test/route.ts +1 -1
- package/src/app/api/service/start/route.ts +1 -1
- package/src/app/api/service/status/route.ts +1 -1
- package/src/app/api/service/stop/route.ts +1 -1
- package/src/app/logs/page.tsx +15 -5
- package/src/cli/index.ts +228 -25
- package/src/db/database.ts +34 -4
- package/src/db/queries.ts +6 -6
- package/src/server/logger.ts +19 -4
- /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_buildManifest.js +0 -0
- /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_clientMiddlewareManifest.json +0 -0
- /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_ssgManifest.js +0 -0
package/src/cli/index.ts
CHANGED
|
@@ -1,16 +1,88 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import { spawn } from 'child_process';
|
|
4
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import http from 'http';
|
|
6
9
|
import { getDatabase } from '../db/database';
|
|
7
|
-
import { getConfig, setConfig } from '../db/queries';
|
|
10
|
+
import { getConfig, setConfig, setServiceStatus, updateServiceStatus } from '../db/queries';
|
|
8
11
|
|
|
9
12
|
const program = new Command();
|
|
10
13
|
|
|
11
14
|
const packageJsonPath = require.resolve('../../../package.json');
|
|
12
15
|
const packageJson = require(packageJsonPath);
|
|
13
16
|
|
|
17
|
+
const AAR_DIR = path.join(os.homedir(), '.aar');
|
|
18
|
+
const UI_PID_FILE = path.join(AAR_DIR, 'ui.pid');
|
|
19
|
+
const GATEWAY_PID_FILE = path.join(AAR_DIR, 'gateway.pid');
|
|
20
|
+
const GATEWAY_PORT_FILE = path.join(AAR_DIR, 'gateway.port');
|
|
21
|
+
|
|
22
|
+
function ensureAarDir(): void {
|
|
23
|
+
if (!fs.existsSync(AAR_DIR)) {
|
|
24
|
+
fs.mkdirSync(AAR_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getPackageRoot(): string {
|
|
29
|
+
return path.dirname(path.dirname(path.dirname(__dirname)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Wait for HTTP server to respond (e.g. UI or gateway ready) */
|
|
33
|
+
function waitForReady(baseUrl: string, maxWaitMs: number = 30000): Promise<boolean> {
|
|
34
|
+
const url = new URL(baseUrl);
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
const tryOnce = () => {
|
|
38
|
+
const req = http.request(
|
|
39
|
+
{
|
|
40
|
+
hostname: url.hostname,
|
|
41
|
+
port: url.port || '80',
|
|
42
|
+
path: url.pathname || '/',
|
|
43
|
+
method: 'GET',
|
|
44
|
+
timeout: 2000,
|
|
45
|
+
},
|
|
46
|
+
(res) => {
|
|
47
|
+
resolve(res.statusCode !== undefined && res.statusCode < 500);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
req.on('error', () => {
|
|
51
|
+
if (Date.now() - start >= maxWaitMs) {
|
|
52
|
+
resolve(false);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setTimeout(tryOnce, 800);
|
|
56
|
+
});
|
|
57
|
+
req.on('timeout', () => {
|
|
58
|
+
req.destroy();
|
|
59
|
+
if (Date.now() - start >= maxWaitMs) resolve(false);
|
|
60
|
+
else setTimeout(tryOnce, 800);
|
|
61
|
+
});
|
|
62
|
+
req.end();
|
|
63
|
+
};
|
|
64
|
+
tryOnce();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printStatus(uiPort: number, gatewayPort: number, gatewayRunning: boolean, background: boolean): void {
|
|
69
|
+
const uiUrl = `http://localhost:${uiPort}`;
|
|
70
|
+
const gatewayUrl = `http://localhost:${gatewayPort}`;
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log('-------------------------------------------');
|
|
73
|
+
console.log(' AI Agent Router');
|
|
74
|
+
console.log('-------------------------------------------');
|
|
75
|
+
console.log(` 前台 UI: ${uiUrl}`);
|
|
76
|
+
console.log(` 网关地址: ${gatewayUrl}`);
|
|
77
|
+
console.log(` 网关状态: ${gatewayRunning ? '运行中' : '未启动'}`);
|
|
78
|
+
if (background) {
|
|
79
|
+
console.log(' 运行模式: 后台运行(关闭终端不影响)');
|
|
80
|
+
console.log(' 停止服务: aar stop');
|
|
81
|
+
}
|
|
82
|
+
console.log('-------------------------------------------');
|
|
83
|
+
console.log('');
|
|
84
|
+
}
|
|
85
|
+
|
|
14
86
|
program
|
|
15
87
|
.name('aar')
|
|
16
88
|
.description('AI Agent Router - Web UI for managing the API gateway')
|
|
@@ -18,47 +90,178 @@ program
|
|
|
18
90
|
|
|
19
91
|
program
|
|
20
92
|
.command('start')
|
|
21
|
-
.description('Start the Web UI
|
|
93
|
+
.description('Start the Web UI and gateway (default: both; use --no-gateway for UI only)')
|
|
22
94
|
.option('-p, --port <port>', 'Port for Web UI', '9527')
|
|
95
|
+
.option('-g, --gateway-port <port>', 'Port for gateway (default: from config or 1357)')
|
|
96
|
+
.option('--no-gateway', 'Only start Web UI, do not start gateway')
|
|
97
|
+
.option('--no-background', 'Run in foreground (attach to terminal)')
|
|
23
98
|
.action(async (options) => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.log(` Access the UI at: http://localhost:${port}`);
|
|
29
|
-
console.log('');
|
|
99
|
+
const uiPort = parseInt(options.port || '9527', 10);
|
|
100
|
+
const startGateway = options.gateway !== false;
|
|
101
|
+
const background = options.background !== false;
|
|
102
|
+
const packageRoot = getPackageRoot();
|
|
30
103
|
|
|
31
|
-
|
|
32
|
-
|
|
104
|
+
let gatewayPort = 1357;
|
|
105
|
+
if (startGateway) {
|
|
106
|
+
try {
|
|
107
|
+
await getDatabase();
|
|
108
|
+
const portConfig = await getConfig('port');
|
|
109
|
+
gatewayPort = options.gatewayPort
|
|
110
|
+
? parseInt(options.gatewayPort, 10)
|
|
111
|
+
: parseInt(portConfig?.value || '1357', 10);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
gatewayPort = options.gatewayPort ? parseInt(options.gatewayPort, 10) : 1357;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
33
116
|
|
|
34
|
-
// Start Web UI using Next.js
|
|
35
|
-
// Always use production mode for globally installed package
|
|
36
117
|
const serverPath = path.join(packageRoot, 'node_modules', 'next', 'dist', 'bin', 'next');
|
|
37
|
-
const
|
|
118
|
+
const gatewayScriptPath = path.join(packageRoot, 'dist', 'src', 'cli', 'gateway-server.js');
|
|
119
|
+
|
|
120
|
+
if (background) {
|
|
121
|
+
ensureAarDir();
|
|
122
|
+
const envBase = { ...process.env, NODE_ENV: 'production' as const };
|
|
123
|
+
|
|
124
|
+
const uiProc: ChildProcess = spawn(process.execPath, [serverPath, 'start', '-p', uiPort.toString()], {
|
|
125
|
+
cwd: packageRoot,
|
|
126
|
+
env: { ...envBase, PORT: uiPort.toString() },
|
|
127
|
+
detached: true,
|
|
128
|
+
stdio: 'ignore',
|
|
129
|
+
});
|
|
130
|
+
uiProc.unref();
|
|
131
|
+
if (uiProc.pid) {
|
|
132
|
+
fs.writeFileSync(UI_PID_FILE, String(uiProc.pid));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let gatewayRunning = false;
|
|
136
|
+
if (startGateway && fs.existsSync(gatewayScriptPath)) {
|
|
137
|
+
const gwProc: ChildProcess = spawn(process.execPath, [gatewayScriptPath, '--port', String(gatewayPort)], {
|
|
138
|
+
cwd: packageRoot,
|
|
139
|
+
env: envBase,
|
|
140
|
+
detached: true,
|
|
141
|
+
stdio: 'ignore',
|
|
142
|
+
});
|
|
143
|
+
gwProc.unref();
|
|
144
|
+
if (gwProc.pid) {
|
|
145
|
+
fs.writeFileSync(GATEWAY_PID_FILE, String(gwProc.pid));
|
|
146
|
+
fs.writeFileSync(GATEWAY_PORT_FILE, String(gatewayPort));
|
|
147
|
+
gatewayRunning = true;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await getDatabase();
|
|
151
|
+
await setServiceStatus({
|
|
152
|
+
status: 'running',
|
|
153
|
+
port: gatewayPort,
|
|
154
|
+
pid: gwProc.pid ?? null,
|
|
155
|
+
started_at: new Date().toISOString(),
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
// ignore db errors
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
printStatus(uiPort, gatewayPort, gatewayRunning, true);
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Foreground: start UI first
|
|
167
|
+
console.log('Starting AI Agent Router...');
|
|
168
|
+
const uiProcess = spawn(process.execPath, [serverPath, 'start', '-p', uiPort.toString()], {
|
|
38
169
|
cwd: packageRoot,
|
|
39
170
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
40
|
-
env: { ...process.env, PORT:
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// Handle UI process exit
|
|
44
|
-
uiProcess.on('exit', (code) => {
|
|
45
|
-
console.log(`Web UI process exited with code ${code}`);
|
|
46
|
-
process.exit(code || 0);
|
|
171
|
+
env: { ...process.env, PORT: uiPort.toString(), NODE_ENV: 'production' },
|
|
47
172
|
});
|
|
48
173
|
|
|
49
|
-
uiProcess.on('error', (error) => {
|
|
174
|
+
uiProcess.on('error', (error: Error) => {
|
|
50
175
|
console.error(`Failed to start Web UI: ${error.message}`);
|
|
51
176
|
process.exit(1);
|
|
52
177
|
});
|
|
53
178
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
179
|
+
const gatewayProcess: ChildProcess | null = startGateway && fs.existsSync(gatewayScriptPath)
|
|
180
|
+
? spawn(process.execPath, [gatewayScriptPath, '--port', String(gatewayPort)], {
|
|
181
|
+
cwd: packageRoot,
|
|
182
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
184
|
+
})
|
|
185
|
+
: null;
|
|
186
|
+
|
|
187
|
+
if (gatewayProcess?.stdout) {
|
|
188
|
+
gatewayProcess.stdout.on('data', (d) => process.stdout.write(d));
|
|
189
|
+
}
|
|
190
|
+
if (gatewayProcess?.stderr) {
|
|
191
|
+
gatewayProcess.stderr.on('data', (d) => process.stderr.write(d));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const ready = await waitForReady(`http://localhost:${uiPort}`);
|
|
195
|
+
if (ready) {
|
|
196
|
+
printStatus(uiPort, gatewayPort, !!gatewayProcess, false);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const shutdown = (signal: string) => {
|
|
200
|
+
console.log(`\nShutting down (${signal})...`);
|
|
201
|
+
if (gatewayProcess?.pid) {
|
|
202
|
+
try {
|
|
203
|
+
process.kill(gatewayProcess.pid, 'SIGTERM');
|
|
204
|
+
} catch {
|
|
205
|
+
// ignore
|
|
206
|
+
}
|
|
207
|
+
}
|
|
57
208
|
uiProcess.kill('SIGTERM');
|
|
58
209
|
process.exit(0);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
213
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
214
|
+
|
|
215
|
+
uiProcess.on('exit', (code) => {
|
|
216
|
+
if (gatewayProcess?.pid) {
|
|
217
|
+
try {
|
|
218
|
+
process.kill(gatewayProcess.pid, 'SIGTERM');
|
|
219
|
+
} catch {
|
|
220
|
+
// ignore
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
process.exit(code ?? 0);
|
|
59
224
|
});
|
|
60
225
|
});
|
|
61
226
|
|
|
227
|
+
program
|
|
228
|
+
.command('stop')
|
|
229
|
+
.description('Stop AI Agent Router when running in background')
|
|
230
|
+
.action(async () => {
|
|
231
|
+
ensureAarDir();
|
|
232
|
+
let stopped = 0;
|
|
233
|
+
const killPidFile = (file: string, name: string) => {
|
|
234
|
+
if (!fs.existsSync(file)) return;
|
|
235
|
+
try {
|
|
236
|
+
const pid = parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
|
|
237
|
+
if (!isNaN(pid)) {
|
|
238
|
+
try {
|
|
239
|
+
process.kill(pid, 'SIGTERM');
|
|
240
|
+
console.log(`Stopped ${name} (PID ${pid})`);
|
|
241
|
+
stopped++;
|
|
242
|
+
} catch (e: any) {
|
|
243
|
+
if (e?.code !== 'ESRCH') console.warn(`${name} (PID ${pid}): ${e.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
fs.unlinkSync(file);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
// ignore
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
killPidFile(GATEWAY_PID_FILE, 'gateway');
|
|
252
|
+
killPidFile(UI_PID_FILE, 'Web UI');
|
|
253
|
+
if (fs.existsSync(GATEWAY_PORT_FILE)) fs.unlinkSync(GATEWAY_PORT_FILE);
|
|
254
|
+
try {
|
|
255
|
+
await getDatabase();
|
|
256
|
+
await updateServiceStatus({ status: 'stopped', pid: null });
|
|
257
|
+
} catch {
|
|
258
|
+
// ignore
|
|
259
|
+
}
|
|
260
|
+
if (stopped === 0) {
|
|
261
|
+
console.log('No background processes found (or already stopped).');
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
62
265
|
program
|
|
63
266
|
.command('config')
|
|
64
267
|
.description('Manage gateway configuration')
|
package/src/db/database.ts
CHANGED
|
@@ -8,6 +8,25 @@ const DB_PATH = process.env.DB_PATH || path.join(os.homedir(), '.aar', 'gateway.
|
|
|
8
8
|
|
|
9
9
|
let dbInstance: Database | null = null;
|
|
10
10
|
let sqlJsInstance: any = null;
|
|
11
|
+
/** 上次从磁盘加载 DB 的时间(用于多进程时发现磁盘被其他进程更新则重新加载) */
|
|
12
|
+
let lastLoadMtimeMs: number = 0;
|
|
13
|
+
|
|
14
|
+
/** 若磁盘上的 DB 被其他进程更新则重新加载,以便读到最新数据 */
|
|
15
|
+
function reloadFromFileIfNewer(): void {
|
|
16
|
+
if (!dbInstance || !fs.existsSync(DB_PATH)) return;
|
|
17
|
+
const mtime = fs.statSync(DB_PATH).mtimeMs;
|
|
18
|
+
if (mtime <= lastLoadMtimeMs) return;
|
|
19
|
+
try {
|
|
20
|
+
const fresh = loadDatabase();
|
|
21
|
+
if (fresh) {
|
|
22
|
+
dbInstance.close();
|
|
23
|
+
dbInstance = fresh;
|
|
24
|
+
lastLoadMtimeMs = fs.statSync(DB_PATH).mtimeMs;
|
|
25
|
+
}
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.warn('[Database] Reload from file failed:', (e as Error).message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
11
30
|
|
|
12
31
|
async function initSqlJsEngine(): Promise<any> {
|
|
13
32
|
if (!sqlJsInstance) {
|
|
@@ -32,6 +51,9 @@ function saveDatabase(db: Database): void {
|
|
|
32
51
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
33
52
|
}
|
|
34
53
|
fs.writeFileSync(DB_PATH, buffer);
|
|
54
|
+
if (fs.existsSync(DB_PATH)) {
|
|
55
|
+
lastLoadMtimeMs = fs.statSync(DB_PATH).mtimeMs;
|
|
56
|
+
}
|
|
35
57
|
} catch (error) {
|
|
36
58
|
console.error('Failed to save database:', error);
|
|
37
59
|
}
|
|
@@ -53,7 +75,14 @@ export async function getDatabase(): Promise<Database> {
|
|
|
53
75
|
if (!dbInstance) {
|
|
54
76
|
const engine = await initSqlJsEngine();
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
const loaded = loadDatabase();
|
|
79
|
+
if (loaded) {
|
|
80
|
+
dbInstance = loaded;
|
|
81
|
+
if (fs.existsSync(DB_PATH)) lastLoadMtimeMs = fs.statSync(DB_PATH).mtimeMs;
|
|
82
|
+
} else {
|
|
83
|
+
dbInstance = new engine.Database();
|
|
84
|
+
lastLoadMtimeMs = 0;
|
|
85
|
+
}
|
|
57
86
|
|
|
58
87
|
try {
|
|
59
88
|
dbInstance!.run(CREATE_TABLES_SQL);
|
|
@@ -112,9 +141,10 @@ export async function getDatabase(): Promise<Database> {
|
|
|
112
141
|
}
|
|
113
142
|
}
|
|
114
143
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
144
|
+
|
|
145
|
+
reloadFromFileIfNewer();
|
|
146
|
+
return dbInstance!;
|
|
147
|
+
}
|
|
118
148
|
|
|
119
149
|
export async function closeDatabase(): Promise<void> {
|
|
120
150
|
if (dbInstance) {
|
package/src/db/queries.ts
CHANGED
|
@@ -157,10 +157,10 @@ export async function createRequestLog(log: Omit<RequestLog, 'id' | 'created_at'
|
|
|
157
157
|
const result = await run(
|
|
158
158
|
`INSERT INTO request_logs (
|
|
159
159
|
model_id, request_method, request_path, request_headers,
|
|
160
|
-
request_query, request_body, response_status, response_body, response_time_ms
|
|
161
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
160
|
+
request_query, request_body, response_status, response_body, response_time_ms, created_at
|
|
161
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`,
|
|
162
162
|
[
|
|
163
|
-
log.model_id
|
|
163
|
+
log.model_id === undefined ? null : log.model_id,
|
|
164
164
|
log.request_method,
|
|
165
165
|
log.request_path,
|
|
166
166
|
log.request_headers,
|
|
@@ -182,10 +182,10 @@ export async function createRequestLog(log: Omit<RequestLog, 'id' | 'created_at'
|
|
|
182
182
|
const result = await run(
|
|
183
183
|
`INSERT INTO request_logs (
|
|
184
184
|
model_id, request_method, request_path, request_headers,
|
|
185
|
-
request_query, request_body, response_status, response_body, response_time_ms
|
|
186
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
185
|
+
request_query, request_body, response_status, response_body, response_time_ms, created_at
|
|
186
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`,
|
|
187
187
|
[
|
|
188
|
-
null,
|
|
188
|
+
log.model_id === undefined ? null : log.model_id,
|
|
189
189
|
log.request_method,
|
|
190
190
|
log.request_path,
|
|
191
191
|
log.request_headers,
|
package/src/server/logger.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { createRequestLog } from '../db/queries';
|
|
2
2
|
import { maskApiKey, maskToken } from './crypto';
|
|
3
3
|
|
|
4
|
+
/** Safe JSON.stringify that never throws; use for logging arbitrary response/request bodies. */
|
|
5
|
+
function safeStringify(value: any): string {
|
|
6
|
+
if (value === undefined) return '';
|
|
7
|
+
if (value === null) return 'null';
|
|
8
|
+
try {
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
} catch {
|
|
11
|
+
try {
|
|
12
|
+
return String(value);
|
|
13
|
+
} catch {
|
|
14
|
+
return '[Non-serializable]';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
export interface LogRequest {
|
|
5
20
|
modelId: number | null; // Allow null for gateway requests
|
|
6
21
|
method: string;
|
|
@@ -30,11 +45,11 @@ export async function logRequest(
|
|
|
30
45
|
model_id: request.modelId,
|
|
31
46
|
request_method: request.method,
|
|
32
47
|
request_path: request.path,
|
|
33
|
-
request_headers:
|
|
34
|
-
request_query:
|
|
35
|
-
request_body:
|
|
48
|
+
request_headers: safeStringify(maskedHeaders),
|
|
49
|
+
request_query: safeStringify(request.query),
|
|
50
|
+
request_body: safeStringify(maskedBody),
|
|
36
51
|
response_status: response.status,
|
|
37
|
-
response_body:
|
|
52
|
+
response_body: safeStringify(response.body),
|
|
38
53
|
response_time_ms: response.responseTimeMs,
|
|
39
54
|
});
|
|
40
55
|
} catch (error) {
|
|
File without changes
|
/package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_clientMiddlewareManifest.json
RENAMED
|
File without changes
|
|
File without changes
|