The daily job is gone. A refresh only matters at one moment — you are picking a provider and want to know its models are current — and at that moment a button answers the question, where a nightly pass can only have already answered it, invisibly, or not. It also bought a standing obligation: something hitting a vendor from every install forever whether anyone opens Atelier or not. So the add dialog is now the curated file made visible. Provider comes from the catalogue, models come from that provider's entry in it, and two buttons re-read it from the vendor: one for the provider in front of you, one for all of them. `POST /providers/catalogue/refresh?provider=` is the only trigger there is. The dialog also says how old the answer is — "as shipped" until a refresh has ever run, which is worth showing, because it means the models listed are exactly what the plugin was released with. A provider with no refresher answers 400 rather than a silent success: from a button, "nothing happened" and "nothing could happen" look identical.
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import type { SidecarCommand, SidecarEvent } from '@@/sidecar/protocol';
|
|
import { createSidecarConnector } from '@@/sidecar/connect';
|
|
import { API_URL } from '@@/officer-url.mjs';
|
|
import { buildHonoApp } from './hono';
|
|
import { waitForDatabase } from './db';
|
|
import { startQueue } from './generations/queue';
|
|
|
|
// The officer-atelier sidecar. Owns the whole image-generation contract: providers, the generation
|
|
// queue, and the `atelier` Postgres database (kept separate from officer_db — see ../PLUGIN.md).
|
|
//
|
|
// The platform API (../api/router.ts) is a thin auth-gated forwarder holding no provider credentials
|
|
// of its own. This process binds a loopback port and reports it over the registration socket, exactly
|
|
// like offscale's sidecar; `createSidecarProxy` on the platform side learns the port from the
|
|
// `atelier:server` event and forwards `/api/atelier/*` here with the prefix stripped.
|
|
|
|
/** Grab an ephemeral free port by briefly binding one and releasing it. */
|
|
function getFreePort(): number {
|
|
const probe = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response('') });
|
|
const p = probe.port;
|
|
probe.stop(true);
|
|
if (p == null) throw new Error('failed to acquire a free port');
|
|
return p;
|
|
}
|
|
|
|
const port = getFreePort();
|
|
const app = buildHonoApp();
|
|
|
|
const server = Bun.serve({
|
|
port,
|
|
hostname: '127.0.0.1',
|
|
// Generation runs 20-60s and a caller may long-poll for up to MAX_WAIT_SECONDS (sidecar/api/images.ts)
|
|
// — the ceiling Bun allows, one below the ceiling rejected outright.
|
|
idleTimeout: 255,
|
|
// OpenAI's edits endpoint takes up to 16 images at 50MB each.
|
|
maxRequestBodySize: 1024 * 1024 * 900,
|
|
fetch(req, srv) {
|
|
const url = new URL(req.url);
|
|
// Liveness only — matches the shape every other HTTP sidecar in this codebase uses.
|
|
if (url.pathname === '/_health') return Response.json({ ok: true });
|
|
return app.fetch(req, srv);
|
|
},
|
|
});
|
|
|
|
console.log(`[atelier] listening on 127.0.0.1:${port}`);
|
|
|
|
type ReplyFn = (msg: SidecarEvent) => void;
|
|
|
|
function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
|
|
switch (cmd.type) {
|
|
case 'ping':
|
|
reply({ type: 'pong', id: cmd.id });
|
|
break;
|
|
default:
|
|
reply({
|
|
type: 'error',
|
|
id: (cmd as SidecarCommand).id,
|
|
error: `Unknown command type: ${(cmd as Record<string, unknown>).type}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
const connection = createSidecarConnector({
|
|
apiUrl: `${API_URL}/api/sidecar/register`,
|
|
name: 'atelier',
|
|
handles: ['atelier'],
|
|
onCommand(cmd, reply) {
|
|
handleCommand(cmd as SidecarCommand, reply as ReplyFn);
|
|
},
|
|
onConnected() {
|
|
// Tell the API where we're listening, so it can forward /api/atelier/* here.
|
|
connection.send({ type: 'atelier:server', port });
|
|
console.log(`[atelier] reported port ${port} to API`);
|
|
},
|
|
});
|
|
|
|
// Awaited, not fired and forgotten. postgres-js is lazy, so on a cold boot the first query loses a
|
|
// race with Postgres and the restart sweep (`failInterrupted`) — the only thing that corrects rows
|
|
// left `running` by the last process — would fail once and be swallowed silently.
|
|
void (async () => {
|
|
if (await waitForDatabase()) await startQueue();
|
|
else console.error('[atelier:queue] not started: no database');
|
|
|
|
})();
|
|
|
|
function shutdown(signal: string) {
|
|
console.log(`[atelier] ${signal} received, shutting down...`);
|
|
try {
|
|
server.stop(true);
|
|
} catch {
|
|
/* already stopped */
|
|
}
|
|
connection.destroy();
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|