@tiens.nguyen/gonext-local-worker 1.0.0 → 1.0.1
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/README.md +40 -1
- package/gonext-local-worker.mjs +299 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ You must create a **Worker API key** in the web app **Settings** (stored as a ha
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install -g @
|
|
14
|
+
npm install -g @tiens.nguyen/gonext-local-worker
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Or from source:
|
|
@@ -56,11 +56,50 @@ Options:
|
|
|
56
56
|
```bash
|
|
57
57
|
gonext-local-worker --poll-ms 2000
|
|
58
58
|
gonext-local-worker --api-base https://other-host.example
|
|
59
|
+
gonext-local-worker --mode webhook --webhook-port 5001
|
|
60
|
+
gonext-local-worker --mode both
|
|
59
61
|
gonext-local-worker --help
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
Leave this process **running** while you use async local models from the web app.
|
|
63
65
|
|
|
66
|
+
## Push-dispatch mode (new option)
|
|
67
|
+
|
|
68
|
+
You now have 2 ways to update job status:
|
|
69
|
+
|
|
70
|
+
1. **Polling mode** (existing): worker polls `/api/worker/jobs/next`.
|
|
71
|
+
2. **Webhook mode** (new): API push-dispatches job payloads to your local worker endpoint.
|
|
72
|
+
|
|
73
|
+
Run local webhook endpoint:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
gonext-local-worker --mode webhook --webhook-port 5001
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
or keep both (recommended transition):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
gonext-local-worker --mode both --webhook-port 5001
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Expose webhook with ngrok:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ngrok http 5001
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then in web app Settings set **Worker webhook URL** to:
|
|
92
|
+
|
|
93
|
+
`https://<your-ngrok-domain>/dispatch-job`
|
|
94
|
+
|
|
95
|
+
Local worker API endpoints on port 5001:
|
|
96
|
+
|
|
97
|
+
- `POST /chat` -> proxies to `http://localhost:11434/api/chat`
|
|
98
|
+
- `POST /generate` -> proxies to `http://localhost:11434/api/generate`
|
|
99
|
+
- `GET /tags` -> proxies to `http://localhost:11434/api/tags`
|
|
100
|
+
- `POST /dispatch-job` -> cloud dispatch entrypoint (token-protected)
|
|
101
|
+
- `GET /health`
|
|
102
|
+
|
|
64
103
|
## Run at login (macOS LaunchAgent)
|
|
65
104
|
|
|
66
105
|
1. Copy `launchd/com.gonext.worker.plist.example` to `~/Library/LaunchAgents/com.gonext.worker.plist`.
|
package/gonext-local-worker.mjs
CHANGED
|
@@ -13,21 +13,33 @@
|
|
|
13
13
|
* ./.env (cwd)
|
|
14
14
|
*/
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
16
18
|
import { join } from "node:path";
|
|
17
19
|
import dotenv from "dotenv";
|
|
20
|
+
import express from "express";
|
|
18
21
|
import OpenAI from "openai";
|
|
19
22
|
|
|
20
23
|
dotenv.config({ path: join(homedir(), ".gonext", "worker.env") });
|
|
21
24
|
dotenv.config();
|
|
22
25
|
|
|
23
26
|
function parseArgs(argv) {
|
|
24
|
-
const out = {
|
|
27
|
+
const out = {
|
|
28
|
+
help: false,
|
|
29
|
+
pollMs: undefined,
|
|
30
|
+
apiBase: undefined,
|
|
31
|
+
mode: undefined,
|
|
32
|
+
webhookPort: undefined,
|
|
33
|
+
};
|
|
25
34
|
for (let i = 2; i < argv.length; i++) {
|
|
26
35
|
const a = argv[i];
|
|
27
36
|
if (a === "--help" || a === "-h") out.help = true;
|
|
28
37
|
else if (a === "--poll-ms" && argv[i + 1])
|
|
29
38
|
out.pollMs = Number(argv[++i]);
|
|
30
39
|
else if (a === "--api-base" && argv[i + 1]) out.apiBase = argv[++i];
|
|
40
|
+
else if (a === "--mode" && argv[i + 1]) out.mode = argv[++i];
|
|
41
|
+
else if (a === "--webhook-port" && argv[i + 1])
|
|
42
|
+
out.webhookPort = Number(argv[++i]);
|
|
31
43
|
}
|
|
32
44
|
return out;
|
|
33
45
|
}
|
|
@@ -44,13 +56,15 @@ Usage:
|
|
|
44
56
|
Options:
|
|
45
57
|
--poll-ms <ms> Idle poll interval (default 1500 or GONEXT_POLL_MS)
|
|
46
58
|
--api-base <url> Override GONEXT_API_BASE
|
|
59
|
+
--mode <poll|webhook|both> Worker mode (default: poll)
|
|
60
|
+
--webhook-port <n> Local webhook port (default: 5001)
|
|
47
61
|
|
|
48
62
|
Config files (optional):
|
|
49
63
|
~/.gonext/worker.env
|
|
50
64
|
.env in current directory
|
|
51
65
|
|
|
52
|
-
Install
|
|
53
|
-
|
|
66
|
+
Install:
|
|
67
|
+
npm install -g @tiens.nguyen/gonext-local-worker
|
|
54
68
|
|
|
55
69
|
Then keep this running while you use the web app with local models.
|
|
56
70
|
`);
|
|
@@ -72,6 +86,17 @@ const pollMs =
|
|
|
72
86
|
(Number.isFinite(args.pollMs) && args.pollMs > 0
|
|
73
87
|
? args.pollMs
|
|
74
88
|
: Number(process.env.GONEXT_POLL_MS ?? "1500")) || 1500;
|
|
89
|
+
const modeRaw = String(args.mode ?? process.env.GONEXT_WORKER_MODE ?? "poll");
|
|
90
|
+
const mode = ["poll", "webhook", "both"].includes(modeRaw)
|
|
91
|
+
? modeRaw
|
|
92
|
+
: "poll";
|
|
93
|
+
const webhookPort =
|
|
94
|
+
(Number.isFinite(args.webhookPort) && args.webhookPort > 0
|
|
95
|
+
? args.webhookPort
|
|
96
|
+
: Number(process.env.GONEXT_WEBHOOK_PORT ?? "5001")) || 5001;
|
|
97
|
+
const ollamaBase = (
|
|
98
|
+
process.env.GONEXT_LOCAL_OLLAMA_BASE ?? "http://127.0.0.1:11434"
|
|
99
|
+
).replace(/\/+$/, "");
|
|
75
100
|
|
|
76
101
|
if (!apiBase || !workerKey) {
|
|
77
102
|
console.error(
|
|
@@ -80,6 +105,12 @@ if (!apiBase || !workerKey) {
|
|
|
80
105
|
process.exit(1);
|
|
81
106
|
}
|
|
82
107
|
|
|
108
|
+
const WORKER_VERSION = "1.0.0";
|
|
109
|
+
const WORKER_HOST = os.hostname();
|
|
110
|
+
const DISPATCH_TOKEN = createHash("sha256")
|
|
111
|
+
.update(workerKey, "utf8")
|
|
112
|
+
.digest("hex");
|
|
113
|
+
|
|
83
114
|
function ts() {
|
|
84
115
|
return new Date().toISOString();
|
|
85
116
|
}
|
|
@@ -113,10 +144,31 @@ async function workerFetch(path, init = {}) {
|
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
let shuttingDown = false;
|
|
147
|
+
let activeJobId = "";
|
|
148
|
+
let lastError = "";
|
|
149
|
+
let webhookServer = null;
|
|
150
|
+
|
|
151
|
+
async function postHeartbeat(payload) {
|
|
152
|
+
try {
|
|
153
|
+
await workerFetch("/api/worker/heartbeat", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
workerVersion: WORKER_VERSION,
|
|
157
|
+
host: WORKER_HOST,
|
|
158
|
+
...payload,
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
} catch {
|
|
162
|
+
/* keep worker loop alive */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
116
165
|
|
|
117
166
|
async function runChatJob(job) {
|
|
118
167
|
const { jobId, payload } = job;
|
|
119
168
|
const start = Date.now();
|
|
169
|
+
activeJobId = jobId;
|
|
170
|
+
lastError = "";
|
|
171
|
+
await postHeartbeat({ state: "running", currentJobId: jobId });
|
|
120
172
|
const patchRunning = await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
121
173
|
method: "PATCH",
|
|
122
174
|
body: JSON.stringify({ jobStatus: "running" }),
|
|
@@ -124,6 +176,13 @@ async function runChatJob(job) {
|
|
|
124
176
|
if (!patchRunning.ok) {
|
|
125
177
|
const t = await patchRunning.text().catch(() => "");
|
|
126
178
|
console.error(`[${ts()}] PATCH running failed ${patchRunning.status}`, t);
|
|
179
|
+
await postHeartbeat({
|
|
180
|
+
state: "error",
|
|
181
|
+
currentJobId: jobId,
|
|
182
|
+
lastError: `PATCH running failed ${patchRunning.status}`,
|
|
183
|
+
});
|
|
184
|
+
lastError = `PATCH running failed ${patchRunning.status}`;
|
|
185
|
+
activeJobId = "";
|
|
127
186
|
return;
|
|
128
187
|
}
|
|
129
188
|
|
|
@@ -152,6 +211,12 @@ async function runChatJob(job) {
|
|
|
152
211
|
console.log(
|
|
153
212
|
`[${ts()}] completed job ${jobId} (${totalTimeSeconds.toFixed(1)}s)`
|
|
154
213
|
);
|
|
214
|
+
await postHeartbeat({
|
|
215
|
+
state: "idle",
|
|
216
|
+
currentJobId: "",
|
|
217
|
+
lastJobCompletedAt: new Date().toISOString(),
|
|
218
|
+
});
|
|
219
|
+
activeJobId = "";
|
|
155
220
|
} catch (e) {
|
|
156
221
|
const message = e instanceof Error ? e.message : String(e);
|
|
157
222
|
await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
@@ -163,6 +228,13 @@ async function runChatJob(job) {
|
|
|
163
228
|
}),
|
|
164
229
|
});
|
|
165
230
|
console.error(`[${ts()}] failed job ${jobId}:`, message);
|
|
231
|
+
await postHeartbeat({
|
|
232
|
+
state: "error",
|
|
233
|
+
currentJobId: jobId,
|
|
234
|
+
lastError: message,
|
|
235
|
+
});
|
|
236
|
+
lastError = message;
|
|
237
|
+
activeJobId = "";
|
|
166
238
|
}
|
|
167
239
|
}
|
|
168
240
|
|
|
@@ -183,18 +255,230 @@ function sleep(ms) {
|
|
|
183
255
|
return new Promise((r) => setTimeout(r, ms));
|
|
184
256
|
}
|
|
185
257
|
|
|
258
|
+
function requireDispatchToken(req, res) {
|
|
259
|
+
const token = req.header("x-gonext-dispatch-token") ?? "";
|
|
260
|
+
if (token !== DISPATCH_TOKEN) {
|
|
261
|
+
res.status(403).json({ error: "Invalid dispatch token." });
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function toOllamaChatMessages(messages) {
|
|
268
|
+
return (messages ?? []).map((m) => ({
|
|
269
|
+
role: m.role,
|
|
270
|
+
content: m.content,
|
|
271
|
+
...(Array.isArray(m.attachments) && m.attachments.length > 0
|
|
272
|
+
? { images: m.attachments.map((a) => a.data) }
|
|
273
|
+
: {}),
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function runOllamaAndMaybeUpdateJob(params) {
|
|
278
|
+
const { endpoint, requestBody, jobId, extractResultText } = params;
|
|
279
|
+
const hasJob = typeof jobId === "string" && jobId.length > 0;
|
|
280
|
+
const start = Date.now();
|
|
281
|
+
if (hasJob) {
|
|
282
|
+
activeJobId = jobId;
|
|
283
|
+
lastError = "";
|
|
284
|
+
await postHeartbeat({ state: "running", currentJobId: jobId });
|
|
285
|
+
await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
286
|
+
method: "PATCH",
|
|
287
|
+
body: JSON.stringify({ jobStatus: "running" }),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const res = await fetch(`${ollamaBase}${endpoint}`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: { "Content-Type": "application/json" },
|
|
294
|
+
body: JSON.stringify({
|
|
295
|
+
stream: false,
|
|
296
|
+
...requestBody,
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
const raw = await res.text();
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
throw new Error(`Ollama ${endpoint} failed ${res.status}: ${raw}`);
|
|
302
|
+
}
|
|
303
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
304
|
+
if (hasJob) {
|
|
305
|
+
await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
306
|
+
method: "PATCH",
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
jobStatus: "completed",
|
|
309
|
+
resultText: extractResultText(parsed),
|
|
310
|
+
tokenCount: 1,
|
|
311
|
+
totalTimeSeconds: (Date.now() - start) / 1000,
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
await postHeartbeat({
|
|
315
|
+
state: "idle",
|
|
316
|
+
currentJobId: "",
|
|
317
|
+
lastJobCompletedAt: new Date().toISOString(),
|
|
318
|
+
});
|
|
319
|
+
activeJobId = "";
|
|
320
|
+
}
|
|
321
|
+
return parsed;
|
|
322
|
+
} catch (e) {
|
|
323
|
+
if (hasJob) {
|
|
324
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
325
|
+
await workerFetch(`/api/worker/jobs/${jobId}`, {
|
|
326
|
+
method: "PATCH",
|
|
327
|
+
body: JSON.stringify({
|
|
328
|
+
jobStatus: "failed",
|
|
329
|
+
errorMessage: message,
|
|
330
|
+
totalTimeSeconds: (Date.now() - start) / 1000,
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
await postHeartbeat({
|
|
334
|
+
state: "error",
|
|
335
|
+
currentJobId: jobId,
|
|
336
|
+
lastError: message,
|
|
337
|
+
});
|
|
338
|
+
lastError = message;
|
|
339
|
+
activeJobId = "";
|
|
340
|
+
}
|
|
341
|
+
throw e;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function startWebhookServer() {
|
|
346
|
+
const app = express();
|
|
347
|
+
app.use(express.json({ limit: "10mb" }));
|
|
348
|
+
|
|
349
|
+
app.get("/health", (_req, res) => {
|
|
350
|
+
const state =
|
|
351
|
+
activeJobId.length > 0 ? "running" : lastError ? "error" : "idle";
|
|
352
|
+
res.json({
|
|
353
|
+
ok: true,
|
|
354
|
+
workerVersion: WORKER_VERSION,
|
|
355
|
+
host: WORKER_HOST,
|
|
356
|
+
mode,
|
|
357
|
+
state,
|
|
358
|
+
activeJobId: activeJobId || undefined,
|
|
359
|
+
lastError: lastError || undefined,
|
|
360
|
+
dispatchPath: "/dispatch-job",
|
|
361
|
+
chatPath: "/chat",
|
|
362
|
+
generatePath: "/generate",
|
|
363
|
+
tagsPath: "/tags",
|
|
364
|
+
ollamaBase,
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
app.get("/tags", async (_req, res) => {
|
|
369
|
+
try {
|
|
370
|
+
const r = await fetch(`${ollamaBase}/api/tags`, { method: "GET" });
|
|
371
|
+
const raw = await r.text();
|
|
372
|
+
if (!r.ok) {
|
|
373
|
+
res
|
|
374
|
+
.status(r.status)
|
|
375
|
+
.json({ error: `Ollama /api/tags failed ${r.status}`, raw });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
res.type("application/json").send(raw || "{}");
|
|
379
|
+
} catch (e) {
|
|
380
|
+
res.status(500).json({ error: e instanceof Error ? e.message : "error" });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
app.post("/dispatch-job", async (req, res) => {
|
|
385
|
+
if (!requireDispatchToken(req, res)) return;
|
|
386
|
+
const body = req.body ?? {};
|
|
387
|
+
if (!body?.jobId || !body?.payload) {
|
|
388
|
+
res.status(400).json({ error: "Expected { jobId, payload }." });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (activeJobId) {
|
|
392
|
+
res.status(409).json({ error: "Worker busy.", activeJobId });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const payload = body.payload;
|
|
396
|
+
const chatBody = {
|
|
397
|
+
model: payload.modelId,
|
|
398
|
+
messages: toOllamaChatMessages(payload.messages),
|
|
399
|
+
};
|
|
400
|
+
void runOllamaAndMaybeUpdateJob({
|
|
401
|
+
endpoint: "/api/chat",
|
|
402
|
+
requestBody: chatBody,
|
|
403
|
+
jobId: String(body.jobId),
|
|
404
|
+
extractResultText: (j) => j?.message?.content ?? "",
|
|
405
|
+
}).catch((e) => {
|
|
406
|
+
console.error("[dispatch-job] error:", e instanceof Error ? e.message : e);
|
|
407
|
+
});
|
|
408
|
+
res.status(202).json({ ok: true, accepted: true, jobId: body.jobId });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
app.post("/chat", async (req, res) => {
|
|
412
|
+
try {
|
|
413
|
+
const body = req.body ?? {};
|
|
414
|
+
const jobId = typeof body.jobId === "string" ? body.jobId : "";
|
|
415
|
+
if (jobId && !requireDispatchToken(req, res)) return;
|
|
416
|
+
const requestBody =
|
|
417
|
+
body.request && typeof body.request === "object" ? body.request : body;
|
|
418
|
+
const parsed = await runOllamaAndMaybeUpdateJob({
|
|
419
|
+
endpoint: "/api/chat",
|
|
420
|
+
requestBody,
|
|
421
|
+
jobId,
|
|
422
|
+
extractResultText: (j) => j?.message?.content ?? "",
|
|
423
|
+
});
|
|
424
|
+
res.json(parsed);
|
|
425
|
+
} catch (e) {
|
|
426
|
+
res.status(500).json({ error: e instanceof Error ? e.message : "error" });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
app.post("/generate", async (req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
const body = req.body ?? {};
|
|
433
|
+
const jobId = typeof body.jobId === "string" ? body.jobId : "";
|
|
434
|
+
if (jobId && !requireDispatchToken(req, res)) return;
|
|
435
|
+
const requestBody =
|
|
436
|
+
body.request && typeof body.request === "object" ? body.request : body;
|
|
437
|
+
const parsed = await runOllamaAndMaybeUpdateJob({
|
|
438
|
+
endpoint: "/api/generate",
|
|
439
|
+
requestBody,
|
|
440
|
+
jobId,
|
|
441
|
+
extractResultText: (j) => j?.response ?? "",
|
|
442
|
+
});
|
|
443
|
+
res.json(parsed);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
res.status(500).json({ error: e instanceof Error ? e.message : "error" });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
webhookServer = app.listen(webhookPort, "127.0.0.1", () => {
|
|
450
|
+
console.log(
|
|
451
|
+
`[${ts()}] local worker API on http://127.0.0.1:${webhookPort} (/chat, /generate, /tags, /dispatch-job, /health)`
|
|
452
|
+
);
|
|
453
|
+
console.log(`[${ts()}] dispatch token is SHA256(worker key)`);
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
186
457
|
async function main() {
|
|
187
458
|
console.log(`[${ts()}] gonext-local-worker`);
|
|
188
459
|
console.log(` API ${apiBase}`);
|
|
189
|
-
console.log(`
|
|
460
|
+
console.log(` mode ${mode}`);
|
|
461
|
+
if (mode === "poll" || mode === "both") {
|
|
462
|
+
console.log(` poll every ${pollMs}ms (idle)`);
|
|
463
|
+
}
|
|
464
|
+
if (mode === "webhook" || mode === "both") {
|
|
465
|
+
console.log(` hook http://127.0.0.1:${webhookPort}/dispatch-job`);
|
|
466
|
+
}
|
|
190
467
|
console.log(` stop Ctrl+C`);
|
|
468
|
+
await postHeartbeat({ state: "idle", currentJobId: "" });
|
|
191
469
|
|
|
192
470
|
const loop = async () => {
|
|
193
471
|
while (!shuttingDown) {
|
|
194
472
|
try {
|
|
195
473
|
await pollOnce();
|
|
474
|
+
await postHeartbeat({ state: "idle", currentJobId: "" });
|
|
196
475
|
} catch (e) {
|
|
197
476
|
console.error(`[${ts()}] poll error:`, e instanceof Error ? e.message : e);
|
|
477
|
+
await postHeartbeat({
|
|
478
|
+
state: "error",
|
|
479
|
+
currentJobId: "",
|
|
480
|
+
lastError: e instanceof Error ? e.message : String(e),
|
|
481
|
+
});
|
|
198
482
|
}
|
|
199
483
|
if (shuttingDown) break;
|
|
200
484
|
await sleep(pollMs);
|
|
@@ -205,12 +489,22 @@ async function main() {
|
|
|
205
489
|
if (shuttingDown) return;
|
|
206
490
|
shuttingDown = true;
|
|
207
491
|
console.log(`\n[${ts()}] shutting down…`);
|
|
492
|
+
if (webhookServer) {
|
|
493
|
+
webhookServer.close();
|
|
494
|
+
}
|
|
208
495
|
process.exit(0);
|
|
209
496
|
};
|
|
210
497
|
process.on("SIGINT", stop);
|
|
211
498
|
process.on("SIGTERM", stop);
|
|
212
499
|
|
|
213
|
-
|
|
500
|
+
if (mode === "webhook" || mode === "both") {
|
|
501
|
+
startWebhookServer();
|
|
502
|
+
}
|
|
503
|
+
if (mode === "poll" || mode === "both") {
|
|
504
|
+
await loop();
|
|
505
|
+
} else {
|
|
506
|
+
await new Promise(() => {});
|
|
507
|
+
}
|
|
214
508
|
}
|
|
215
509
|
|
|
216
510
|
main().catch((e) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"dotenv": "^16.4.5",
|
|
28
|
+
"express": "^4.21.0",
|
|
28
29
|
"openai": "^4.77.0"
|
|
29
30
|
},
|
|
30
31
|
"engines": {
|