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/dist/src/cli/index.js
CHANGED
|
@@ -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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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')
|
package/dist/src/db/database.js
CHANGED
|
@@ -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
|
-
|
|
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() {
|
package/dist/src/db/queries.js
CHANGED
|
@@ -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
|
|
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:
|
|
16
|
-
request_query:
|
|
17
|
-
request_body:
|
|
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:
|
|
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.
|
|
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"
|
|
@@ -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
|
|
|
@@ -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;
|
package/src/app/logs/page.tsx
CHANGED
|
@@ -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">
|