@rhizomatics/signalk-einklabel-plugin 1.3.1 → 1.3.2
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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/cli/index.js +0 -0
- package/dist/devices/bleBackend.js +10 -5
- package/dist/plugin.js +7 -3
- package/dist/repaintScheduler.d.ts +11 -2
- package/dist/repaintScheduler.js +13 -4
- package/docs/bluetooth.md +9 -5
- package/package.json +3 -3
- package/dist/email/emailSender.d.ts +0 -27
- package/dist/email/emailSender.js +0 -66
- package/dist/render/fieldsTable.d.ts +0 -15
- package/dist/render/fieldsTable.js +0 -52
- package/dist/render/llmPrompt.d.ts +0 -86
- package/dist/render/llmPrompt.js +0 -199
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# 1.3.2
|
|
2
|
+
|
|
3
|
+
## 🐛 Bug fixes
|
|
4
|
+
|
|
5
|
+
- On a SignalK server shutdown or admin UI restart (server v2.35.0+), the plugin now waits at most 5 seconds for a repaint in progress instead of 30, so it stops inside the server's shutdown deadline and the server can close the label's Bluetooth connection cleanly before exiting
|
|
6
|
+
|
|
7
|
+
## 📚 Documentation
|
|
8
|
+
|
|
9
|
+
- [Bluetooth](https://signalk-einklabel.rhizomatics.org.uk/bluetooth/) docs updated for the BLE Manager fixes in SignalK v2.34.0 and v2.35.0. v2.34.0 is now the practical minimum for BLE Manager, with v2.35.0 recommended. The BLE Manager setting stays off by default, since Zhsunyco labels not yet proven with it.
|
|
10
|
+
|
|
1
11
|
# 1.3.1
|
|
2
12
|
|
|
3
13
|
## 🐛 Bug fixes
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/actions/workflows/signalk-ci.yml)
|
|
6
6
|
[](https://github.com)
|
|
7
7
|
[](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/LICENSE)
|
|
8
|
-
[](https://boat-tech-directory.rhizomatics.org.uk)
|
|
8
|
+
[](https://boat-tech-directory.rhizomatics.org.uk) [](https://github.com/rhizomatics/hq)
|
|
9
9
|
|
|
10
10
|
A SignalK plugin to display data from SignalK paths, Resource APIs and plugins on Electronic Shelf Labels (ESL) over a Bluetooth Low Energy (BLE) connection using simple SVG templates, or optionally created from a crafted prompt by GenAI if the companion [`@rhizomatics/signalk-einklabel-genai-plugin`](https://github.com/rhizomatics/signalk-einklabel-genai-plugin) is installed. Supports ESLs from two of the major Chinese manufacturers, and requires **no firmware or hardware modifications**, switch on and go.
|
|
11
11
|
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
|
@@ -213,7 +213,9 @@ function bleApiBackend(bleApi, pluginId) {
|
|
|
213
213
|
// A previous ungraceful stop (crash, plugin reload mid-paint) can leave a stale GATT claim
|
|
214
214
|
// registered under our own pluginId, which would otherwise block this connect with
|
|
215
215
|
// "already claimed" until the server restarts - see signalk-bluetti-plugin's BleManagerDevice
|
|
216
|
-
// for the same defensive call. A no-op if we don't currently hold the claim.
|
|
216
|
+
// for the same defensive call. A no-op if we don't currently hold the claim. Servers from 2.34.0
|
|
217
|
+
// release a stopped plugin's claims themselves (signalk-server#3093), so this only matters on
|
|
218
|
+
// older ones - kept for them.
|
|
217
219
|
await boundedCleanup(bleApi.releaseGATTDevice(address, pluginId));
|
|
218
220
|
await ensureDeviceVisible(bleApi, pluginId, address, DEVICE_DISCOVERY_TIMEOUT_MS);
|
|
219
221
|
const connecting = bleApi.connectGATT(address, pluginId);
|
|
@@ -232,9 +234,12 @@ function bleApiBackend(bleApi, pluginId) {
|
|
|
232
234
|
// `connectGATT()` and get rejected with "has a GATT claim in progress" for its trouble.
|
|
233
235
|
await boundedCleanup(bleApi.releaseGATTDevice(address, pluginId));
|
|
234
236
|
// ...except a claim still *connecting* isn't in the server's claim table yet, only its pending
|
|
235
|
-
// set
|
|
236
|
-
//
|
|
237
|
-
//
|
|
237
|
+
// set. Before 2.34.0 `releaseGATTDevice` didn't touch that at all; since then
|
|
238
|
+
// (signalk-server#3092) it marks the pending claim released, so the server closes the
|
|
239
|
+
// connection itself when it arrives - but the device stays reserved until the server's own
|
|
240
|
+
// connect returns (bounded at 60s since signalk-server#3082), and until then a retry's
|
|
241
|
+
// `connectGATT()` is rejected outright with "has a GATT claim in progress". Either way, give
|
|
242
|
+
// that connect a bounded chance to settle first, so the retry gets a clean slate.
|
|
238
243
|
const late = await Promise.race([
|
|
239
244
|
connecting.catch(() => undefined),
|
|
240
245
|
(0, bleDiscovery_1.sleep)(Math.min(timeoutMs, CONNECT_SETTLE_GRACE_MS)).then(() => undefined),
|
|
@@ -282,7 +287,7 @@ function bleApiBackend(bleApi, pluginId) {
|
|
|
282
287
|
// ever reaches `connectGatt` below) stops advertising while claimed - which would otherwise wedge
|
|
283
288
|
// this wait forever, since nothing else in this path ever calls `connectGatt` (and so never gets a
|
|
284
289
|
// chance to release the stale claim) unless a fresh advertisement shows up first. A no-op if we
|
|
285
|
-
// don't currently hold the claim.
|
|
290
|
+
// don't currently hold the claim; only needed before server 2.34.0, as in `connectGatt`.
|
|
286
291
|
await boundedCleanup(bleApi.releaseGATTDevice(address, pluginId));
|
|
287
292
|
return new Promise((resolve) => {
|
|
288
293
|
const timer = setTimeout(() => {
|
package/dist/plugin.js
CHANGED
|
@@ -104,14 +104,18 @@ function createPlugin(app) {
|
|
|
104
104
|
},
|
|
105
105
|
// Returns a Promise, as the server asks of a stop() with async work: it resolves once a paint
|
|
106
106
|
// already under way has finished (bounded by `STOP_DRAIN_TIMEOUT_MS`), so a config save mid-paint
|
|
107
|
-
// or a server shutdown doesn't leave that BLE session running behind the next start().
|
|
108
|
-
|
|
107
|
+
// or a server shutdown doesn't leave that BLE session running behind the next start(). On a
|
|
108
|
+
// server shutdown (`reason`, passed by signalk-server 2.35.0+) the wait is cut to
|
|
109
|
+
// `SHUTDOWN_DRAIN_TIMEOUT_MS` to fit the server's own shutdown deadline - see its doc comment.
|
|
110
|
+
// `reason` is typed loosely so this still compiles against server-api versions before 2.35.0.
|
|
111
|
+
stop(options) {
|
|
109
112
|
generation++;
|
|
110
113
|
const stoppingScheduler = scheduler;
|
|
111
114
|
scheduler = undefined;
|
|
112
115
|
stopDiscoveryListener?.();
|
|
113
116
|
stopDiscoveryListener = undefined;
|
|
114
|
-
|
|
117
|
+
const drainTimeoutMs = options?.reason === "shutdown" ? repaintScheduler_1.SHUTDOWN_DRAIN_TIMEOUT_MS : repaintScheduler_1.STOP_DRAIN_TIMEOUT_MS;
|
|
118
|
+
stopping = (stoppingScheduler?.stop(drainTimeoutMs) ?? Promise.resolve()).then(() => app.debug(options?.reason ? `stopped (${options.reason})` : "stopped"));
|
|
115
119
|
return stopping;
|
|
116
120
|
},
|
|
117
121
|
};
|
|
@@ -7,12 +7,21 @@ import { PluginConfig } from "./config";
|
|
|
7
7
|
* stuck; such a paint is still ended by its own connect timeout and `PAINT_ATTEMPT_BACKSTOP_MS`.
|
|
8
8
|
*/
|
|
9
9
|
export declare const STOP_DRAIN_TIMEOUT_MS = 30000;
|
|
10
|
+
/**
|
|
11
|
+
* The drain limit for a server shutdown (signalk-server 2.35.0+ passes `stop()` a `reason`), in place
|
|
12
|
+
* of `STOP_DRAIN_TIMEOUT_MS`. The server gives all plugins' `stop()`s 6s together before it moves on,
|
|
13
|
+
* and exits at 8s - so a 30s drain would be cut off anyway, mid-transfer. Resolving inside that
|
|
14
|
+
* window instead lets the server's own release of this plugin's BLE Manager claims (signalk-server
|
|
15
|
+
* #3093, run once `stop()` returns) close the connection before the process exits. Since 2.35.0 the
|
|
16
|
+
* admin UI's Restart button goes through the same shutdown path; a config save doesn't.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
10
19
|
export interface RepaintScheduler {
|
|
11
20
|
/**
|
|
12
21
|
* Stops every trigger, lets no new paint or retry start, and resolves once any paint already in
|
|
13
|
-
* progress finishes - or after `STOP_DRAIN_TIMEOUT_MS
|
|
22
|
+
* progress finishes - or after `drainTimeoutMs` (default `STOP_DRAIN_TIMEOUT_MS`), whichever comes first.
|
|
14
23
|
*/
|
|
15
|
-
stop(): Promise<void>;
|
|
24
|
+
stop(drainTimeoutMs?: number): Promise<void>;
|
|
16
25
|
}
|
|
17
26
|
/**
|
|
18
27
|
* The most recent wall-clock instant at or before `now` that an `interval`-triggered device's own
|
package/dist/repaintScheduler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.STOP_DRAIN_TIMEOUT_MS = void 0;
|
|
3
|
+
exports.SHUTDOWN_DRAIN_TIMEOUT_MS = exports.STOP_DRAIN_TIMEOUT_MS = void 0;
|
|
4
4
|
exports.mostRecentScheduledSlot = mostRecentScheduledSlot;
|
|
5
5
|
exports.oneAtATimePerKey = oneAtATimePerKey;
|
|
6
6
|
exports.startRepaintScheduler = startRepaintScheduler;
|
|
@@ -41,6 +41,15 @@ const PAINT_ATTEMPT_BACKSTOP_MS = 7 * 60000;
|
|
|
41
41
|
* stuck; such a paint is still ended by its own connect timeout and `PAINT_ATTEMPT_BACKSTOP_MS`.
|
|
42
42
|
*/
|
|
43
43
|
exports.STOP_DRAIN_TIMEOUT_MS = 30000;
|
|
44
|
+
/**
|
|
45
|
+
* The drain limit for a server shutdown (signalk-server 2.35.0+ passes `stop()` a `reason`), in place
|
|
46
|
+
* of `STOP_DRAIN_TIMEOUT_MS`. The server gives all plugins' `stop()`s 6s together before it moves on,
|
|
47
|
+
* and exits at 8s - so a 30s drain would be cut off anyway, mid-transfer. Resolving inside that
|
|
48
|
+
* window instead lets the server's own release of this plugin's BLE Manager claims (signalk-server
|
|
49
|
+
* #3093, run once `stop()` returns) close the connection before the process exits. Since 2.35.0 the
|
|
50
|
+
* admin UI's Restart button goes through the same shutdown path; a config save doesn't.
|
|
51
|
+
*/
|
|
52
|
+
exports.SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
44
53
|
const RESOURCES_API_PATH = "/signalk/v2/api/resources";
|
|
45
54
|
/**
|
|
46
55
|
* The most recent wall-clock instant at or before `now` that an `interval`-triggered device's own
|
|
@@ -559,7 +568,7 @@ function startRepaintScheduler(app, config) {
|
|
|
559
568
|
}, settleMs);
|
|
560
569
|
unsubscribes.push(() => clearTimeout(startupCheckTimer));
|
|
561
570
|
return {
|
|
562
|
-
async stop() {
|
|
571
|
+
async stop(drainTimeoutMs = exports.STOP_DRAIN_TIMEOUT_MS) {
|
|
563
572
|
stopping = true;
|
|
564
573
|
for (const unsubscribe of unsubscribes)
|
|
565
574
|
unsubscribe();
|
|
@@ -568,12 +577,12 @@ function startRepaintScheduler(app, config) {
|
|
|
568
577
|
const drained = await Promise.race([
|
|
569
578
|
repaint.idle().then(() => true),
|
|
570
579
|
new Promise((resolve) => {
|
|
571
|
-
timer = setTimeout(() => resolve(false),
|
|
580
|
+
timer = setTimeout(() => resolve(false), drainTimeoutMs);
|
|
572
581
|
}),
|
|
573
582
|
]);
|
|
574
583
|
clearTimeout(timer);
|
|
575
584
|
if (!drained) {
|
|
576
|
-
app.debug(`a repaint was still running ${
|
|
585
|
+
app.debug(`a repaint was still running ${drainTimeoutMs / 1000}s after stop - not waiting any longer`);
|
|
577
586
|
}
|
|
578
587
|
},
|
|
579
588
|
};
|
package/docs/bluetooth.md
CHANGED
|
@@ -32,15 +32,19 @@ SignalK BLE Manager also supports BLE Gateways, which could be an MQTT topic or
|
|
|
32
32
|
|
|
33
33
|
## BLE Manager Readiness
|
|
34
34
|
|
|
35
|
-
v2.31.0 is the
|
|
35
|
+
v2.31.0 is the minimum version of SignalK possible for BLE Manager. v2.34.0 is the practical minimum for using it with this plugin, and v2.35.0 is recommended.
|
|
36
36
|
|
|
37
|
-
Gicisky labels have been painted successfully using BLE Manager
|
|
37
|
+
Gicisky labels have been painted successfully using BLE Manager. Zhsunyco labels needed three server fixes, all released in v2.34.0:
|
|
38
38
|
|
|
39
39
|
- [PR#3082](https://github.com/SignalK/signalk-server/pull/3082) - hung connections locking up device
|
|
40
40
|
- [PR#3088](https://github.com/SignalK/signalk-server/pull/3088) - Support plain GATT write requests
|
|
41
41
|
- [PR#3089](https://github.com/SignalK/signalk-server/pull/3089) - Pause scanning while GATT operation in progress
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Zhsunyco labels still have problems through BLE Manager even with these fixes, so the BLE Manager setting stays off by default for now. Direct BlueZ access remains the reliable choice for them.
|
|
44
|
+
|
|
45
|
+
v2.34.0 also releases a plugin's Bluetooth connections when the plugin stops ([PR#3093](https://github.com/SignalK/signalk-server/pull/3093)). v2.35.0 stops plugins cleanly when the server shuts down or is restarted from the admin UI ([PR#3194](https://github.com/SignalK/signalk-server/pull/3194)). On a shutdown this plugin only waits 5 seconds for a label part way through a repaint, rather than the usual 30, so that it finishes inside the server's shutdown deadline.
|
|
46
|
+
|
|
47
|
+
With BLE Manager, scanning pauses while any plugin is connecting to a Bluetooth device, for up to 60 seconds. If another plugin's device keeps failing to connect, a label may not be seen in time for a repaint, which then fails and is retried.
|
|
44
48
|
|
|
45
49
|
## Weak Signal
|
|
46
50
|
|
|
@@ -64,9 +68,9 @@ Sometime Bluetooth adapters, and/or the Linux services that use them, can get in
|
|
|
64
68
|
|
|
65
69
|
## Zhsunyco Labels and the BLE Manager
|
|
66
70
|
|
|
67
|
-
With the "Use the SignalK BLE Manager API" setting on, Zhsunyco labels can connect and authenticate, then fail as soon as the image upload starts, with `Operation failed with ATT error: 0x0e` in the log. SignalK's BLE Manager (2.33 and earlier) sends every write that waits for an acknowledgement as a Bluetooth "reliable" write, which these labels don't support.
|
|
71
|
+
With the "Use the SignalK BLE Manager API" setting on, Zhsunyco labels can connect and authenticate, then fail as soon as the image upload starts, with `Operation failed with ATT error: 0x0e` in the log. SignalK's BLE Manager (2.33 and earlier) sends every write that waits for an acknowledgement as a Bluetooth "reliable" write, which these labels don't support. This was fixed in SignalK v2.34.0, so upgrading is the first thing to try.
|
|
68
72
|
|
|
69
|
-
|
|
73
|
+
On 2.33 and earlier, either:
|
|
70
74
|
|
|
71
75
|
- turn on _Send image without waiting for each write (Zhsunyco)_ in the label's _Advanced settings_ - the image is sent with a small gap between writes instead, and the label still confirms once the whole image has arrived; or
|
|
72
76
|
- switch the BLE Manager setting off, if no other plugins need to share Bluetooth with this one.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-einklabel-plugin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Display SignalK data on eInk Electronic Shelf Labels, includes working examples for tide clock and watch schedule.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ble",
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
"@types/luxon": "^3.7.5",
|
|
77
77
|
"@types/node": "^26.6.0",
|
|
78
78
|
"@types/pngjs": "^6.0.5",
|
|
79
|
-
"oxfmt": "^0.
|
|
80
|
-
"oxlint": "^1.
|
|
79
|
+
"oxfmt": "^0.75.0",
|
|
80
|
+
"oxlint": "^1.89.0",
|
|
81
81
|
"oxlint-tsgolint": "^7.0.2001",
|
|
82
82
|
"ts-node": "^10.9.2",
|
|
83
83
|
"typescript": "^5.5.0"
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { EmailConfig } from "../config";
|
|
2
|
-
import { Bitmap } from "../render/types";
|
|
3
|
-
import { FieldRow } from "../render/fieldsTable";
|
|
4
|
-
/** Either the SVG template's field-by-field table (`buildFieldsTable`), a `TemplateProvider`'s own rendered-content text (`describeContent`), or neither (a provider offered no `describeContent`). */
|
|
5
|
-
export type EmailContentBody = {
|
|
6
|
-
kind: "table";
|
|
7
|
-
rows: FieldRow[];
|
|
8
|
-
} | {
|
|
9
|
-
kind: "text";
|
|
10
|
-
text: string;
|
|
11
|
-
} | {
|
|
12
|
-
kind: "none";
|
|
13
|
-
};
|
|
14
|
-
export interface LabelEmailContent {
|
|
15
|
-
friendlyName: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
bitmap: Bitmap;
|
|
18
|
-
body: EmailContentBody;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Renders and sends one device's repaint as an email - a PNG attachment (also shown inline via a `cid`
|
|
22
|
-
* reference), the same field-by-field table `esl-cli fields` shows for a hand-authored SVG template (or
|
|
23
|
-
* a `TemplateProvider`'s own rendered-content text for something like a GenAI prompt - see `body`), and a
|
|
24
|
-
* link back to the docs site. Called from `considerRepaint` (`../repaintScheduler.ts`) whenever a
|
|
25
|
-
* device's `emailTo` is set and its render succeeds.
|
|
26
|
-
*/
|
|
27
|
-
export declare function sendLabelEmail(emailConfig: EmailConfig, to: string, content: LabelEmailContent): Promise<void>;
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.sendLabelEmail = sendLabelEmail;
|
|
7
|
-
const nodemailer_1 = __importDefault(require("nodemailer"));
|
|
8
|
-
const png_1 = require("../render/png");
|
|
9
|
-
const DOCS_URL = "https://rhizomatics.github.io/signalk-einklabel-plugin/";
|
|
10
|
-
function escapeHtml(text) {
|
|
11
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
12
|
-
}
|
|
13
|
-
function bodyHtml(body) {
|
|
14
|
-
if (body.kind === "table") {
|
|
15
|
-
if (body.rows.length === 0)
|
|
16
|
-
return "";
|
|
17
|
-
const rows = body.rows
|
|
18
|
-
.map((row) => `<tr><td>${escapeHtml(row.id)}</td><td>${escapeHtml(row.spec)}</td><td>${escapeHtml(row.value)}</td></tr>`)
|
|
19
|
-
.join("");
|
|
20
|
-
return ('<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-family:monospace;font-size:13px">' +
|
|
21
|
-
`<thead><tr><th>id</th><th>spec</th><th>value</th></tr></thead><tbody>${rows}</tbody></table>`);
|
|
22
|
-
}
|
|
23
|
-
if (body.kind === "text") {
|
|
24
|
-
return `<pre style="white-space:pre-wrap;font-family:monospace;font-size:13px">${escapeHtml(body.text)}</pre>`;
|
|
25
|
-
}
|
|
26
|
-
return "";
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Renders and sends one device's repaint as an email - a PNG attachment (also shown inline via a `cid`
|
|
30
|
-
* reference), the same field-by-field table `esl-cli fields` shows for a hand-authored SVG template (or
|
|
31
|
-
* a `TemplateProvider`'s own rendered-content text for something like a GenAI prompt - see `body`), and a
|
|
32
|
-
* link back to the docs site. Called from `considerRepaint` (`../repaintScheduler.ts`) whenever a
|
|
33
|
-
* device's `emailTo` is set and its render succeeds.
|
|
34
|
-
*/
|
|
35
|
-
async function sendLabelEmail(emailConfig, to, content) {
|
|
36
|
-
const transporter = nodemailer_1.default.createTransport({
|
|
37
|
-
host: emailConfig.smtpHost,
|
|
38
|
-
port: emailConfig.smtpPort,
|
|
39
|
-
secure: emailConfig.smtpSecure,
|
|
40
|
-
auth: emailConfig.smtpUser ? { user: emailConfig.smtpUser, pass: emailConfig.smtpPassword } : undefined,
|
|
41
|
-
});
|
|
42
|
-
const png = (0, png_1.bitmapToPng)(content.bitmap);
|
|
43
|
-
const html = [
|
|
44
|
-
'<div style="font-family:sans-serif">',
|
|
45
|
-
`<h2>${escapeHtml(content.friendlyName)}</h2>`,
|
|
46
|
-
content.description ? `<p>${escapeHtml(content.description)}</p>` : "",
|
|
47
|
-
`<p><img src="cid:label-image" alt="${escapeHtml(content.friendlyName)}" style="border:1px solid #ccc" /></p>`,
|
|
48
|
-
bodyHtml(content.body),
|
|
49
|
-
`<p><a href="${DOCS_URL}">${DOCS_URL}</a></p>`,
|
|
50
|
-
"</div>",
|
|
51
|
-
].join("\n");
|
|
52
|
-
await transporter.sendMail({
|
|
53
|
-
from: emailConfig.fromAddress,
|
|
54
|
-
to,
|
|
55
|
-
subject: `eInk Label: ${content.friendlyName}`,
|
|
56
|
-
html,
|
|
57
|
-
attachments: [
|
|
58
|
-
{
|
|
59
|
-
filename: "label.png",
|
|
60
|
-
content: png,
|
|
61
|
-
contentType: "image/png",
|
|
62
|
-
cid: "label-image",
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
});
|
|
66
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { TemplateContext } from "./types";
|
|
2
|
-
export interface FieldRow {
|
|
3
|
-
id: string;
|
|
4
|
-
spec: string;
|
|
5
|
-
value: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Synchronous counterpart to the CLI's `fields` command (`../cli/index.ts`) - same three columns (id,
|
|
9
|
-
* spec, resolved value), but resolved against a single already-assembled `context` rather than
|
|
10
|
-
* re-fetching per binding, since a caller like `considerRepaint` (`../repaintScheduler.ts`) already has
|
|
11
|
-
* the full context its render used. Only meaningful for a hand-authored SVG template - a
|
|
12
|
-
* `TemplateProvider`-backed template (e.g. GenAI) has no `<desc>` bindings to walk; see
|
|
13
|
-
* `TemplateProvider.describeContent` (`./templateProviders.ts`) for that case's own text summary instead.
|
|
14
|
-
*/
|
|
15
|
-
export declare function buildFieldsTable(svgSource: string, context: TemplateContext, templatesDir: string, bundledTemplatesDir: string): FieldRow[];
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildFieldsTable = buildFieldsTable;
|
|
4
|
-
const path_1 = require("path");
|
|
5
|
-
const xmldom_1 = require("@xmldom/xmldom");
|
|
6
|
-
const assets_1 = require("./assets");
|
|
7
|
-
const binding_1 = require("./binding");
|
|
8
|
-
function resolveFieldValue(tag, desc, context, templatesDir, bundledTemplatesDir) {
|
|
9
|
-
try {
|
|
10
|
-
const binding = (0, binding_1.parseBinding)(desc);
|
|
11
|
-
if (tag !== "image") {
|
|
12
|
-
return (0, binding_1.renderBinding)(binding, context);
|
|
13
|
-
}
|
|
14
|
-
if (!binding.assets) {
|
|
15
|
-
return 'ERROR: an <image> binding requires an "assets" key';
|
|
16
|
-
}
|
|
17
|
-
const key = (0, assets_1.normalizeAssetKey)((0, binding_1.resolveBinding)(binding, context));
|
|
18
|
-
if (!key)
|
|
19
|
-
return "(no usable value - image would be omitted)";
|
|
20
|
-
const assetPath = (0, assets_1.resolveAssetPath)(templatesDir, bundledTemplatesDir, binding.assets, key);
|
|
21
|
-
return assetPath ? `"${key}" -> ${(0, path_1.basename)(assetPath)}` : `"${key}" -> no matching asset file (image would be omitted)`;
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
return `ERROR: ${err.message}`;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Synchronous counterpart to the CLI's `fields` command (`../cli/index.ts`) - same three columns (id,
|
|
29
|
-
* spec, resolved value), but resolved against a single already-assembled `context` rather than
|
|
30
|
-
* re-fetching per binding, since a caller like `considerRepaint` (`../repaintScheduler.ts`) already has
|
|
31
|
-
* the full context its render used. Only meaningful for a hand-authored SVG template - a
|
|
32
|
-
* `TemplateProvider`-backed template (e.g. GenAI) has no `<desc>` bindings to walk; see
|
|
33
|
-
* `TemplateProvider.describeContent` (`./templateProviders.ts`) for that case's own text summary instead.
|
|
34
|
-
*/
|
|
35
|
-
function buildFieldsTable(svgSource, context, templatesDir, bundledTemplatesDir) {
|
|
36
|
-
const doc = new xmldom_1.DOMParser().parseFromString(svgSource, "image/svg+xml");
|
|
37
|
-
const rows = [];
|
|
38
|
-
for (const tag of ["text", "image"]) {
|
|
39
|
-
const elements = doc.getElementsByTagName(tag);
|
|
40
|
-
for (let i = 0; i < elements.length; i++) {
|
|
41
|
-
const element = elements.item(i);
|
|
42
|
-
if (!element)
|
|
43
|
-
continue;
|
|
44
|
-
const id = element.getAttribute("id") ?? `${tag}#${i}`;
|
|
45
|
-
const desc = element.getElementsByTagName("desc").item(0);
|
|
46
|
-
if (!desc?.textContent)
|
|
47
|
-
continue;
|
|
48
|
-
rows.push({ id, spec: desc.textContent, value: resolveFieldValue(tag, desc.textContent, context, templatesDir, bundledTemplatesDir) });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return rows;
|
|
52
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { PluginConfig } from "../config";
|
|
2
|
-
import { Colour } from "../devices/types";
|
|
3
|
-
import { Binding } from "./binding";
|
|
4
|
-
import { TemplateContext } from "./types";
|
|
5
|
-
/** Facts about one physical label a prompt's `source=label,path=...` placeholders can reference - see `buildLabelContext`. */
|
|
6
|
-
export interface LabelMeta {
|
|
7
|
-
manufacturer: string;
|
|
8
|
-
/** The physical panel's own size label, e.g. `'3.7"'` - `DeviceMetadata.label` verbatim, see `../devices/types.ts`. */
|
|
9
|
-
label: string;
|
|
10
|
-
width: number;
|
|
11
|
-
height: number;
|
|
12
|
-
colours: Colour[];
|
|
13
|
-
description?: string;
|
|
14
|
-
position?: {
|
|
15
|
-
latitude: number;
|
|
16
|
-
longitude: number;
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Builds the `context.label` object a prompt/target-guidance fragment addresses via
|
|
21
|
-
* `source=label,path=...` bindings (e.g. `{source=label,path=width}`, or the bare-path shorthand
|
|
22
|
-
* `{width}` is *not* supported here deliberately - see `./binding.ts`'s `parseBinding`, every `{...}`
|
|
23
|
-
* placeholder is a real binding, so a `label` path always needs the explicit `source=label,path=`
|
|
24
|
-
* form to disambiguate it from a `signalk` self path). `colours`/`fonts` are left as arrays (each
|
|
25
|
-
* colour entry pre-annotated with its hex code, e.g. `"black (#000000)"`) rather than joined into a
|
|
26
|
-
* single string, so a prompt author can either use them bare (renders as JSON, e.g. for a model that
|
|
27
|
-
* parses structured hints) or add `format=csv` (see `./formatters.ts`) for a plain comma-separated list.
|
|
28
|
-
*/
|
|
29
|
-
export declare function buildLabelContext(meta: LabelMeta): Record<string, unknown>;
|
|
30
|
-
/**
|
|
31
|
-
* Every binding referenced across one or more prompt fragments - every `{...}` placeholder, deduplicated
|
|
32
|
-
* across all of `texts` combined, parsed exactly the way a template's `<desc>` binding is
|
|
33
|
-
* (`parseBinding`, see `./binding.ts`), so a bare path (`{design.length}`, `source=signalk,context=self`
|
|
34
|
-
* shorthand), the full binding grammar (`{source=signalk,path=navigation.position,format=position}`),
|
|
35
|
-
* and a `source=label,path=...` binding all work uniformly. Pass the result to `assembleRawContext`
|
|
36
|
-
* (repaintScheduler.ts) exactly as a template's own bindings are - it fetches the `signalk`/
|
|
37
|
-
* `resources`-sourced ones and silently ignores `label`/`einklabel`-sourced ones, which resolve directly
|
|
38
|
-
* against `context.label`/`context.meta` instead (built by the caller, not fetched).
|
|
39
|
-
*
|
|
40
|
-
* A placeholder that isn't valid binding grammar (e.g. a typo like `{source=taheight}`) is silently
|
|
41
|
-
* skipped here rather than thrown - the same per-field isolation `SvgRenderer` gives a bad `<desc>`
|
|
42
|
-
* binding, so one malformed placeholder doesn't take down the whole prompt. `substitutePlaceholders`
|
|
43
|
-
* hits the identical parse error at substitution time and turns it into "???" for just that field.
|
|
44
|
-
*/
|
|
45
|
-
export declare function findPromptBindings(...texts: string[]): Binding[];
|
|
46
|
-
/**
|
|
47
|
-
* Substitutes every `{...}` placeholder in `text` with its resolved binding value against `context`
|
|
48
|
-
* (built by `assembleRawContext` plus `context.label` from `buildLabelContext` - see
|
|
49
|
-
* `findPromptBindings`). Mirrors `renderBinding`'s per-field isolation, but substitutes "???" for
|
|
50
|
-
* anything that resolves to no value at all (missing path, invalid binding grammar) rather than "" -
|
|
51
|
-
* prose with a silently-blank word reads as a fact ("for the sailor of a m vessel"), not as a gap the
|
|
52
|
-
* reader would notice. Two things override that "???": a binding that resolves successfully to a
|
|
53
|
-
* legitimately empty string (e.g. an unset `{source=label,path=description}`) is left as empty, since
|
|
54
|
-
* that's a real answer, not a miss; and a binding with an explicit `default=` (see `./binding.ts`) uses
|
|
55
|
-
* that default instead, since the prompt author has already said what a missing value should read as.
|
|
56
|
-
*/
|
|
57
|
-
export declare function substitutePlaceholders(text: string, context: TemplateContext): string;
|
|
58
|
-
/**
|
|
59
|
-
* Strips everything outside the first `<svg`...last `</svg>` span - a chat model asked for "only raw
|
|
60
|
-
* SVG markup" still often wraps it in a markdown code fence or adds a sentence of commentary either
|
|
61
|
-
* side, despite the target-guidance fragment telling it not to.
|
|
62
|
-
*/
|
|
63
|
-
export declare function extractSvg(raw: string): string;
|
|
64
|
-
export type LlmSettings = Pick<PluginConfig, "llmProvider" | "llmApiKey" | "llmModel" | "llmBaseUrl"> & {
|
|
65
|
-
llmTimeoutSeconds?: number;
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* `ai` and every `@ai-sdk/*` provider package are `optionalDependencies` (see package.json), not plain
|
|
69
|
-
* `dependencies` - most installs (every device using `renderMode: "svg-template"` only) never touch an
|
|
70
|
-
* LLM at all, so they shouldn't have to pull in this whole gateway stack, and an install that fails to
|
|
71
|
-
* fetch one of these (network hiccup, `npm install --omit=optional`, an unsupported platform) must not
|
|
72
|
-
* break BLE painting/templates, which have nothing to do with it. That means every reference to one of
|
|
73
|
-
* these packages has to be a `require()` reached only when a `renderMode: "llm-prompt"` device actually
|
|
74
|
-
* calls `callLlm` - a top-level `import` would be resolved eagerly the moment this module loads (which
|
|
75
|
-
* is every plugin start, via `repaintScheduler.ts`), throwing before any config is even read. Types are
|
|
76
|
-
* still fully checked via `typeof import(...)` below, which - unlike a value `import` - is erased
|
|
77
|
-
* entirely at compile time and leaves no runtime trace for `tsc` to eagerly require.
|
|
78
|
-
*/
|
|
79
|
-
export declare function loadOptional<M>(moduleName: string): M;
|
|
80
|
-
/**
|
|
81
|
-
* Calls the configured LLM gateway with `prompt`, returning its raw text response - not yet extracted/
|
|
82
|
-
* validated as SVG, see `extractSvg`. This is the first network call in the codebase that needs its own
|
|
83
|
-
* timeout (`fetchJson` in `../httpJson.ts` has none - every existing call is to the local SignalK
|
|
84
|
-
* server's own REST API).
|
|
85
|
-
*/
|
|
86
|
-
export declare function callLlm(settings: LlmSettings, prompt: string): Promise<string>;
|
package/dist/render/llmPrompt.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildLabelContext = buildLabelContext;
|
|
4
|
-
exports.findPromptBindings = findPromptBindings;
|
|
5
|
-
exports.substitutePlaceholders = substitutePlaceholders;
|
|
6
|
-
exports.extractSvg = extractSvg;
|
|
7
|
-
exports.loadOptional = loadOptional;
|
|
8
|
-
exports.callLlm = callLlm;
|
|
9
|
-
const binding_1 = require("./binding");
|
|
10
|
-
const formatters_1 = require("./formatters");
|
|
11
|
-
const COLOUR_HEX = { black: "#000000", white: "#FFFFFF", red: "#FF0000", yellow: "#FFFF00" };
|
|
12
|
-
/** The only `font-family` values `SvgRenderer` is guaranteed to render - see `expandGenericFontFamilies` in `./svgRenderer.ts`. */
|
|
13
|
-
const SAFE_FONT_FAMILIES = ["serif", "sans-serif", "monospace"];
|
|
14
|
-
/**
|
|
15
|
-
* Builds the `context.label` object a prompt/target-guidance fragment addresses via
|
|
16
|
-
* `source=label,path=...` bindings (e.g. `{source=label,path=width}`, or the bare-path shorthand
|
|
17
|
-
* `{width}` is *not* supported here deliberately - see `./binding.ts`'s `parseBinding`, every `{...}`
|
|
18
|
-
* placeholder is a real binding, so a `label` path always needs the explicit `source=label,path=`
|
|
19
|
-
* form to disambiguate it from a `signalk` self path). `colours`/`fonts` are left as arrays (each
|
|
20
|
-
* colour entry pre-annotated with its hex code, e.g. `"black (#000000)"`) rather than joined into a
|
|
21
|
-
* single string, so a prompt author can either use them bare (renders as JSON, e.g. for a model that
|
|
22
|
-
* parses structured hints) or add `format=csv` (see `./formatters.ts`) for a plain comma-separated list.
|
|
23
|
-
*/
|
|
24
|
-
function buildLabelContext(meta) {
|
|
25
|
-
return {
|
|
26
|
-
manufacturer: meta.manufacturer,
|
|
27
|
-
label: meta.label,
|
|
28
|
-
width: meta.width,
|
|
29
|
-
height: meta.height,
|
|
30
|
-
colours: meta.colours.map((colour) => `${colour} (${COLOUR_HEX[colour]})`),
|
|
31
|
-
fonts: SAFE_FONT_FAMILIES,
|
|
32
|
-
description: meta.description ?? "",
|
|
33
|
-
position: meta.position ? (0, formatters_1.applyFormat)("position", meta.position, {}, 3) : undefined,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Every binding referenced across one or more prompt fragments - every `{...}` placeholder, deduplicated
|
|
38
|
-
* across all of `texts` combined, parsed exactly the way a template's `<desc>` binding is
|
|
39
|
-
* (`parseBinding`, see `./binding.ts`), so a bare path (`{design.length}`, `source=signalk,context=self`
|
|
40
|
-
* shorthand), the full binding grammar (`{source=signalk,path=navigation.position,format=position}`),
|
|
41
|
-
* and a `source=label,path=...` binding all work uniformly. Pass the result to `assembleRawContext`
|
|
42
|
-
* (repaintScheduler.ts) exactly as a template's own bindings are - it fetches the `signalk`/
|
|
43
|
-
* `resources`-sourced ones and silently ignores `label`/`einklabel`-sourced ones, which resolve directly
|
|
44
|
-
* against `context.label`/`context.meta` instead (built by the caller, not fetched).
|
|
45
|
-
*
|
|
46
|
-
* A placeholder that isn't valid binding grammar (e.g. a typo like `{source=taheight}`) is silently
|
|
47
|
-
* skipped here rather than thrown - the same per-field isolation `SvgRenderer` gives a bad `<desc>`
|
|
48
|
-
* binding, so one malformed placeholder doesn't take down the whole prompt. `substitutePlaceholders`
|
|
49
|
-
* hits the identical parse error at substitution time and turns it into "???" for just that field.
|
|
50
|
-
*/
|
|
51
|
-
function findPromptBindings(...texts) {
|
|
52
|
-
const seen = new Set();
|
|
53
|
-
const bindings = [];
|
|
54
|
-
for (const text of texts) {
|
|
55
|
-
for (const match of text.matchAll(/\{([^{}]+)\}/g)) {
|
|
56
|
-
const key = match[1].trim();
|
|
57
|
-
if (seen.has(key))
|
|
58
|
-
continue;
|
|
59
|
-
seen.add(key);
|
|
60
|
-
try {
|
|
61
|
-
bindings.push((0, binding_1.parseBinding)(key));
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// see doc comment above - left for `substitutePlaceholders` to turn into "???"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return bindings;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Substitutes every `{...}` placeholder in `text` with its resolved binding value against `context`
|
|
72
|
-
* (built by `assembleRawContext` plus `context.label` from `buildLabelContext` - see
|
|
73
|
-
* `findPromptBindings`). Mirrors `renderBinding`'s per-field isolation, but substitutes "???" for
|
|
74
|
-
* anything that resolves to no value at all (missing path, invalid binding grammar) rather than "" -
|
|
75
|
-
* prose with a silently-blank word reads as a fact ("for the sailor of a m vessel"), not as a gap the
|
|
76
|
-
* reader would notice. Two things override that "???": a binding that resolves successfully to a
|
|
77
|
-
* legitimately empty string (e.g. an unset `{source=label,path=description}`) is left as empty, since
|
|
78
|
-
* that's a real answer, not a miss; and a binding with an explicit `default=` (see `./binding.ts`) uses
|
|
79
|
-
* that default instead, since the prompt author has already said what a missing value should read as.
|
|
80
|
-
*/
|
|
81
|
-
function substitutePlaceholders(text, context) {
|
|
82
|
-
return text.replace(/\{([^{}]+)\}/g, (_match, raw) => {
|
|
83
|
-
const key = raw.trim();
|
|
84
|
-
try {
|
|
85
|
-
const binding = (0, binding_1.parseBinding)(key);
|
|
86
|
-
const value = (0, binding_1.resolveBinding)(binding, context);
|
|
87
|
-
if ((value === undefined || value === null) && binding.default === undefined)
|
|
88
|
-
return "???";
|
|
89
|
-
return (0, binding_1.renderBinding)(binding, context);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return "???";
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Strips everything outside the first `<svg`...last `</svg>` span - a chat model asked for "only raw
|
|
98
|
-
* SVG markup" still often wraps it in a markdown code fence or adds a sentence of commentary either
|
|
99
|
-
* side, despite the target-guidance fragment telling it not to.
|
|
100
|
-
*/
|
|
101
|
-
function extractSvg(raw) {
|
|
102
|
-
const start = raw.indexOf("<svg");
|
|
103
|
-
const end = raw.lastIndexOf("</svg>");
|
|
104
|
-
if (start === -1 || end === -1 || end < start) {
|
|
105
|
-
throw new Error('LLM response did not contain a "<svg>...</svg>" document');
|
|
106
|
-
}
|
|
107
|
-
return raw.slice(start, end + "</svg>".length);
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* `ai` and every `@ai-sdk/*` provider package are `optionalDependencies` (see package.json), not plain
|
|
111
|
-
* `dependencies` - most installs (every device using `renderMode: "svg-template"` only) never touch an
|
|
112
|
-
* LLM at all, so they shouldn't have to pull in this whole gateway stack, and an install that fails to
|
|
113
|
-
* fetch one of these (network hiccup, `npm install --omit=optional`, an unsupported platform) must not
|
|
114
|
-
* break BLE painting/templates, which have nothing to do with it. That means every reference to one of
|
|
115
|
-
* these packages has to be a `require()` reached only when a `renderMode: "llm-prompt"` device actually
|
|
116
|
-
* calls `callLlm` - a top-level `import` would be resolved eagerly the moment this module loads (which
|
|
117
|
-
* is every plugin start, via `repaintScheduler.ts`), throwing before any config is even read. Types are
|
|
118
|
-
* still fully checked via `typeof import(...)` below, which - unlike a value `import` - is erased
|
|
119
|
-
* entirely at compile time and leaves no runtime trace for `tsc` to eagerly require.
|
|
120
|
-
*/
|
|
121
|
-
function loadOptional(moduleName) {
|
|
122
|
-
try {
|
|
123
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
124
|
-
return require(moduleName);
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
throw new Error(`LLM provider support needs the optional dependency "${moduleName}", which isn't installed - run "npm install ${moduleName}" (${err.message})`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/** Ollama's own default local listen address - see the `"ollama"` case in `resolveModel` below. */
|
|
131
|
-
const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434/v1";
|
|
132
|
-
/**
|
|
133
|
-
* Resolves `settings` to a Vercel AI SDK model handle. `"ollama"` and `"local"` both go through the
|
|
134
|
-
* generic `@ai-sdk/openai-compatible` provider (Ollama/LM Studio/vLLM all expose an OpenAI-compatible
|
|
135
|
-
* endpoint, and none of them has - or needs - its own dedicated `@ai-sdk/*` package): `"ollama"`
|
|
136
|
-
* defaults `llmBaseUrl` to Ollama's own standard local address so it works with no further config
|
|
137
|
-
* (override it only if Ollama is running elsewhere, e.g. on the SignalK server's own host reached over
|
|
138
|
-
* the network); `"local"` is for anything else OpenAI-compatible, where there's no sensible universal
|
|
139
|
-
* default, so `llmBaseUrl` is required.
|
|
140
|
-
*/
|
|
141
|
-
function resolveModel(settings) {
|
|
142
|
-
const model = settings.llmModel;
|
|
143
|
-
if (!model) {
|
|
144
|
-
throw new Error("no LLM model configured (PluginConfig.llmModel)");
|
|
145
|
-
}
|
|
146
|
-
switch (settings.llmProvider ?? "openai") {
|
|
147
|
-
case "anthropic": {
|
|
148
|
-
const { createAnthropic } = loadOptional("@ai-sdk/anthropic");
|
|
149
|
-
return createAnthropic({ apiKey: settings.llmApiKey })(model);
|
|
150
|
-
}
|
|
151
|
-
case "google": {
|
|
152
|
-
const { createGoogleGenerativeAI } = loadOptional("@ai-sdk/google");
|
|
153
|
-
return createGoogleGenerativeAI({ apiKey: settings.llmApiKey })(model);
|
|
154
|
-
}
|
|
155
|
-
case "xai": {
|
|
156
|
-
const { createXai } = loadOptional("@ai-sdk/xai");
|
|
157
|
-
return createXai({ apiKey: settings.llmApiKey })(model);
|
|
158
|
-
}
|
|
159
|
-
case "ollama": {
|
|
160
|
-
const { createOpenAICompatible } = loadOptional("@ai-sdk/openai-compatible");
|
|
161
|
-
return createOpenAICompatible({
|
|
162
|
-
name: "ollama",
|
|
163
|
-
baseURL: settings.llmBaseUrl || DEFAULT_OLLAMA_BASE_URL,
|
|
164
|
-
apiKey: settings.llmApiKey,
|
|
165
|
-
})(model);
|
|
166
|
-
}
|
|
167
|
-
case "local": {
|
|
168
|
-
if (!settings.llmBaseUrl) {
|
|
169
|
-
throw new Error('llmBaseUrl is required when llmProvider is "local"');
|
|
170
|
-
}
|
|
171
|
-
const { createOpenAICompatible } = loadOptional("@ai-sdk/openai-compatible");
|
|
172
|
-
return createOpenAICompatible({ name: "local", baseURL: settings.llmBaseUrl, apiKey: settings.llmApiKey })(model);
|
|
173
|
-
}
|
|
174
|
-
case "openai":
|
|
175
|
-
default: {
|
|
176
|
-
const { createOpenAI } = loadOptional("@ai-sdk/openai");
|
|
177
|
-
return createOpenAI({ apiKey: settings.llmApiKey })(model);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Calls the configured LLM gateway with `prompt`, returning its raw text response - not yet extracted/
|
|
183
|
-
* validated as SVG, see `extractSvg`. This is the first network call in the codebase that needs its own
|
|
184
|
-
* timeout (`fetchJson` in `../httpJson.ts` has none - every existing call is to the local SignalK
|
|
185
|
-
* server's own REST API).
|
|
186
|
-
*/
|
|
187
|
-
async function callLlm(settings, prompt) {
|
|
188
|
-
const model = resolveModel(settings);
|
|
189
|
-
const { generateText } = loadOptional("ai");
|
|
190
|
-
const controller = new AbortController();
|
|
191
|
-
const timer = setTimeout(() => controller.abort(), (settings.llmTimeoutSeconds ?? 30) * 1000);
|
|
192
|
-
try {
|
|
193
|
-
const { text } = await generateText({ model, prompt, abortSignal: controller.signal });
|
|
194
|
-
return text;
|
|
195
|
-
}
|
|
196
|
-
finally {
|
|
197
|
-
clearTimeout(timer);
|
|
198
|
-
}
|
|
199
|
-
}
|