feat: add ai gateway local core flow
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER="${AI_GATEWAY_PG_CONTAINER:-easyai-pgvector}"
|
||||
if [[ -n "${AI_GATEWAY_PG_CONTAINER:-}" ]]; then
|
||||
CONTAINER="$AI_GATEWAY_PG_CONTAINER"
|
||||
elif docker inspect easyai-pgvector >/dev/null 2>&1; then
|
||||
CONTAINER="easyai-pgvector"
|
||||
elif docker inspect postgres >/dev/null 2>&1; then
|
||||
CONTAINER="postgres"
|
||||
else
|
||||
CONTAINER="easyai-pgvector"
|
||||
fi
|
||||
PGUSER="${AI_GATEWAY_PG_USER:-easyai}"
|
||||
DB_NAME="${AI_GATEWAY_DATABASE_NAME:-easyai_ai_gateway}"
|
||||
|
||||
|
||||
+18
-2
@@ -1,10 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
export AI_GATEWAY_PG_CONTAINER="${AI_GATEWAY_PG_CONTAINER:-easyai-pgvector}"
|
||||
if [[ -z "${AI_GATEWAY_PG_CONTAINER:-}" ]]; then
|
||||
if docker inspect easyai-pgvector >/dev/null 2>&1; then
|
||||
export AI_GATEWAY_PG_CONTAINER="easyai-pgvector"
|
||||
elif docker inspect postgres >/dev/null 2>&1; then
|
||||
export AI_GATEWAY_PG_CONTAINER="postgres"
|
||||
else
|
||||
export AI_GATEWAY_PG_CONTAINER="easyai-pgvector"
|
||||
fi
|
||||
fi
|
||||
export AI_GATEWAY_PG_USER="${AI_GATEWAY_PG_USER:-easyai}"
|
||||
if [[ -z "${AI_GATEWAY_PG_PASSWORD:-}" ]] && docker inspect "$AI_GATEWAY_PG_CONTAINER" >/dev/null 2>&1; then
|
||||
AI_GATEWAY_PG_PASSWORD="$(
|
||||
docker inspect "$AI_GATEWAY_PG_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
| awk -F= '$1 == "POSTGRES_PASSWORD" {print $2; exit}'
|
||||
)"
|
||||
export AI_GATEWAY_PG_PASSWORD
|
||||
fi
|
||||
export AI_GATEWAY_PG_PASSWORD="${AI_GATEWAY_PG_PASSWORD:-easyai2025}"
|
||||
export AI_GATEWAY_DATABASE_NAME="${AI_GATEWAY_DATABASE_NAME:-easyai_ai_gateway}"
|
||||
export AI_GATEWAY_DATABASE_URL="${AI_GATEWAY_DATABASE_URL:-postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable}"
|
||||
export AI_GATEWAY_DATABASE_URL="${AI_GATEWAY_DATABASE_URL:-postgresql://${AI_GATEWAY_PG_USER}:${AI_GATEWAY_PG_PASSWORD}@localhost:5432/${AI_GATEWAY_DATABASE_NAME}?sslmode=disable}"
|
||||
|
||||
echo "[ai-gateway] using database: ${AI_GATEWAY_DATABASE_URL}"
|
||||
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
const watchRoot = process.env.GO_WATCH_ROOT || process.cwd();
|
||||
const intervalMs = Number(process.env.GO_WATCH_INTERVAL_MS || 700);
|
||||
const debounceMs = Number(process.env.GO_WATCH_DEBOUNCE_MS || 250);
|
||||
const shutdownGraceMs = Number(process.env.GO_WATCH_SHUTDOWN_GRACE_MS || 2500);
|
||||
const restartDelayMs = Number(process.env.GO_WATCH_RESTART_DELAY_MS || 200);
|
||||
const commandIndex = process.argv.indexOf('--');
|
||||
const command = commandIndex >= 0 ? process.argv.slice(commandIndex + 1) : ['go', 'run', './cmd/gateway'];
|
||||
const ignoredDirs = new Set([
|
||||
'.git',
|
||||
'.nx',
|
||||
'coverage',
|
||||
'dist',
|
||||
'node_modules',
|
||||
'tmp',
|
||||
'vendor',
|
||||
]);
|
||||
const watchedExtensions = new Set(['.go', '.mod', '.sum']);
|
||||
|
||||
if (!command.length) {
|
||||
console.error('[go-watch] missing command after --');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let child = null;
|
||||
let knownSignature = '';
|
||||
let restarting = false;
|
||||
let restartQueued = false;
|
||||
let pendingRestart = null;
|
||||
let shuttingDown = false;
|
||||
let stopped = false;
|
||||
|
||||
function log(message) {
|
||||
console.log(`[go-watch] ${message}`);
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function collectFiles(dir, files = []) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return files;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.') && entry.name !== '.env') {
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
if (!ignoredDirs.has(entry.name)) {
|
||||
await collectFiles(path.join(dir, entry.name), files);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (watchedExtensions.has(path.extname(entry.name))) {
|
||||
files.push(path.join(dir, entry.name));
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function signature() {
|
||||
const files = await collectFiles(watchRoot);
|
||||
files.sort();
|
||||
const parts = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
const info = await stat(file);
|
||||
parts.push(`${file}:${info.mtimeMs}:${info.size}`);
|
||||
} catch {
|
||||
parts.push(`${file}:deleted`);
|
||||
}
|
||||
}
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
log(`starting: ${command.join(' ')}`);
|
||||
child = spawn(command[0], command.slice(1), {
|
||||
cwd: watchRoot,
|
||||
detached: process.platform !== 'win32',
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.on('exit', (code, signal) => {
|
||||
if (stopped || restarting) {
|
||||
return;
|
||||
}
|
||||
log(`process exited code=${code ?? 'null'} signal=${signal ?? 'null'}`);
|
||||
});
|
||||
}
|
||||
|
||||
function signalProcessTree(processToStop, signal) {
|
||||
if (!processToStop || processToStop.pid === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
processToStop.kill(signal);
|
||||
} else {
|
||||
process.kill(-processToStop.pid, signal);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ESRCH') {
|
||||
console.error(`[go-watch] failed to send ${signal}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stopProcessTree(processToStop) {
|
||||
if (!processToStop || processToStop.exitCode !== null || processToStop.signalCode !== null) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = () => {
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
clearTimeout(forceTimer);
|
||||
clearTimeout(resolveTimer);
|
||||
resolve();
|
||||
};
|
||||
const forceTimer = setTimeout(() => {
|
||||
signalProcessTree(processToStop, 'SIGKILL');
|
||||
}, shutdownGraceMs);
|
||||
const resolveTimer = setTimeout(finish, shutdownGraceMs + 500);
|
||||
forceTimer.unref();
|
||||
resolveTimer.unref();
|
||||
processToStop.once('exit', finish);
|
||||
signalProcessTree(processToStop, 'SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
async function restartNow() {
|
||||
if (restarting) {
|
||||
restartQueued = true;
|
||||
return;
|
||||
}
|
||||
restarting = true;
|
||||
log('change detected, restarting');
|
||||
const previous = child;
|
||||
child = null;
|
||||
await stopProcessTree(previous);
|
||||
if (restartDelayMs > 0) {
|
||||
await sleep(restartDelayMs);
|
||||
}
|
||||
restarting = false;
|
||||
if (!stopped) {
|
||||
start();
|
||||
}
|
||||
if (restartQueued && !stopped) {
|
||||
restartQueued = false;
|
||||
restart();
|
||||
}
|
||||
}
|
||||
|
||||
function restart() {
|
||||
if (pendingRestart) {
|
||||
clearTimeout(pendingRestart);
|
||||
}
|
||||
pendingRestart = setTimeout(() => {
|
||||
pendingRestart = null;
|
||||
void restartNow();
|
||||
}, debounceMs);
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const nextSignature = await signature();
|
||||
if (!knownSignature) {
|
||||
knownSignature = nextSignature;
|
||||
} else if (nextSignature !== knownSignature) {
|
||||
knownSignature = nextSignature;
|
||||
restart();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[go-watch] scan failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
log(`watching ${watchRoot}`);
|
||||
knownSignature = await signature();
|
||||
start();
|
||||
const timer = setInterval(poll, intervalMs);
|
||||
const shutdown = async (exitCode) => {
|
||||
if (shuttingDown) {
|
||||
return;
|
||||
}
|
||||
shuttingDown = true;
|
||||
stopped = true;
|
||||
clearInterval(timer);
|
||||
if (pendingRestart) {
|
||||
clearTimeout(pendingRestart);
|
||||
}
|
||||
await stopProcessTree(child);
|
||||
process.exit(exitCode);
|
||||
};
|
||||
process.on('SIGINT', () => {
|
||||
void shutdown(130);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
void shutdown(143);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[go-watch] fatal', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user