@7n/tauri-components 0.6.0 → 0.8.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/package.json +5 -1
- package/src/core/resolve-omlx-base-url.js +84 -0
- package/src/index.js +1 -0
- package/src/vue/index.js +1 -0
- package/src/vue/use-omlx.js +17 -4
- package/src/vue/use-updater.js +116 -0
- package/types/core/resolve-omlx-base-url.d.ts +42 -0
- package/types/index.d.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/tauri-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared LLM agent engine + Vue/Quasar UI for Tauri apps (chat, journal, trust-tier approval).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@tauri-apps/api": "^2.0.0",
|
|
38
38
|
"@tauri-apps/plugin-http": "^2.0.0",
|
|
39
|
+
"@tauri-apps/plugin-process": "^2.0.0",
|
|
40
|
+
"@tauri-apps/plugin-updater": "^2.0.0",
|
|
39
41
|
"@vue/test-utils": "^2.0.0",
|
|
40
42
|
"quasar": "^2.0.0",
|
|
41
43
|
"vue": "^3.0.0"
|
|
@@ -43,6 +45,8 @@
|
|
|
43
45
|
"peerDependenciesMeta": {
|
|
44
46
|
"@tauri-apps/api": { "optional": true },
|
|
45
47
|
"@tauri-apps/plugin-http": { "optional": true },
|
|
48
|
+
"@tauri-apps/plugin-process": { "optional": true },
|
|
49
|
+
"@tauri-apps/plugin-updater": { "optional": true },
|
|
46
50
|
"@vue/test-utils": { "optional": true },
|
|
47
51
|
"quasar": { "optional": true }
|
|
48
52
|
},
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Resolve the effective omlx base URL: when the myllm reverse proxy
|
|
2
|
+
// (a Tauri app that forwards /v1/* to the real omlx server and logs every
|
|
3
|
+
// request/response for its history view) is alive on its well-known local
|
|
4
|
+
// port, route traffic through it; otherwise talk to omlx directly. The probe
|
|
5
|
+
// hits {proxy origin}/health — the proxy forwards it to omlx itself, so a
|
|
6
|
+
// 2xx proves BOTH the proxy and the upstream are up (omlx down behind a live
|
|
7
|
+
// proxy yields 502 → direct). Framework-free so headless consumers (MCP
|
|
8
|
+
// wrappers, node scripts) can reuse it; pass a Tauri fetch as fetchFn from
|
|
9
|
+
// webview contexts to avoid CORS on localhost.
|
|
10
|
+
|
|
11
|
+
export const DIRECT_OMLX_BASE_URL = 'http://127.0.0.1:8000/v1'
|
|
12
|
+
export const PROXY_OMLX_BASE_URL = 'http://127.0.0.1:8088/v1'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 400
|
|
15
|
+
const DEFAULT_TTL_MS = 12_000
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Is this URL the default local omlx server — the only target eligible for
|
|
19
|
+
* the proxy override? A user who deliberately pointed an app at another
|
|
20
|
+
* host/port must never be silently rerouted (the proxy's upstream is
|
|
21
|
+
* hardwired to the local :8000).
|
|
22
|
+
* @param {string} url candidate base URL
|
|
23
|
+
* @returns {boolean} true for 127.0.0.1:8000 / localhost:8000, false otherwise (incl. parse errors)
|
|
24
|
+
*/
|
|
25
|
+
export function isDirectOmlxUrl(url) {
|
|
26
|
+
try {
|
|
27
|
+
const { host } = new URL(url)
|
|
28
|
+
return host === '127.0.0.1:8000' || host === 'localhost:8000'
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One-shot probe: GET {proxy origin}/health with a short timeout. 2xx means
|
|
37
|
+
* the proxy (and omlx behind it) is alive → use the proxy; anything else
|
|
38
|
+
* (timeout, refused, 502) → use the direct URL.
|
|
39
|
+
* @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number }} [params] config
|
|
40
|
+
* @returns {Promise<string>} the base URL to use
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveOmlxBaseUrl({
|
|
43
|
+
directUrl = DIRECT_OMLX_BASE_URL,
|
|
44
|
+
proxyUrl = PROXY_OMLX_BASE_URL,
|
|
45
|
+
fetchFn = fetch,
|
|
46
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
47
|
+
} = {}) {
|
|
48
|
+
try {
|
|
49
|
+
const healthUrl = new URL('/health', proxyUrl).toString()
|
|
50
|
+
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(timeoutMs) : undefined
|
|
51
|
+
const response = await fetchFn(healthUrl, { signal })
|
|
52
|
+
return response.ok ? proxyUrl : directUrl
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return directUrl
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// proxyUrl → { promise, expiresAt }. Caching the promise (not the value)
|
|
60
|
+
// dedupes concurrent probes: several composables firing loadEnv() at once
|
|
61
|
+
// trigger a single /health request.
|
|
62
|
+
const cache = new Map()
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Cached {@link resolveOmlxBaseUrl}: at most one probe per proxyUrl per TTL
|
|
66
|
+
* window, so callers may resolve before every LLM call without paying the
|
|
67
|
+
* probe latency each time, while still noticing the proxy starting/stopping
|
|
68
|
+
* within one TTL.
|
|
69
|
+
* @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number, ttlMs?: number, now?: () => number }} [params] config
|
|
70
|
+
* @returns {Promise<string>} the base URL to use
|
|
71
|
+
*/
|
|
72
|
+
export function resolveOmlxBaseUrlCached({ ttlMs = DEFAULT_TTL_MS, now = Date.now, ...probeOptions } = {}) {
|
|
73
|
+
const proxyUrl = probeOptions.proxyUrl ?? PROXY_OMLX_BASE_URL
|
|
74
|
+
const cached = cache.get(proxyUrl)
|
|
75
|
+
if (cached && now() < cached.expiresAt) return cached.promise
|
|
76
|
+
const promise = resolveOmlxBaseUrl(probeOptions)
|
|
77
|
+
cache.set(proxyUrl, { promise, expiresAt: now() + ttlMs })
|
|
78
|
+
return promise
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Test hook: drop all cached probe results. */
|
|
82
|
+
export function __resetOmlxBaseUrlCache() {
|
|
83
|
+
cache.clear()
|
|
84
|
+
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { handleApprove, handleRequest, handleRespond } from './core/agent-handle
|
|
|
8
8
|
export { createDispatch, validateInput } from './core/dispatch.js'
|
|
9
9
|
export { createOpenAiChat, runAgent } from './core/llm.js'
|
|
10
10
|
export { listOmlxModels, resolveModel } from './core/omlx-models.js'
|
|
11
|
+
export { DIRECT_OMLX_BASE_URL, isDirectOmlxUrl, PROXY_OMLX_BASE_URL, resolveOmlxBaseUrl, resolveOmlxBaseUrlCached } from './core/resolve-omlx-base-url.js'
|
|
11
12
|
export { listTools, toJsonSchema, toolManifest } from './core/manifest.js'
|
|
12
13
|
export { classify, DEFAULT_ACTOR_TIERS, scopedManifest, scopedToolNames } from './core/scope.js'
|
|
13
14
|
export { getTool } from './core/tools.js'
|
package/src/vue/index.js
CHANGED
package/src/vue/use-omlx.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ref } from 'vue'
|
|
2
2
|
import { invoke } from '@tauri-apps/api/core'
|
|
3
|
+
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
|
4
|
+
import { isDirectOmlxUrl, resolveOmlxBaseUrlCached } from '../core/resolve-omlx-base-url.js'
|
|
3
5
|
|
|
4
6
|
// Persisted config for the local omlx server (OpenAI-compatible MLX) that drives
|
|
5
7
|
// the in-app agent. The API key / base URL come from Rust (omlx_config), which
|
|
@@ -9,6 +11,12 @@ import { invoke } from '@tauri-apps/api/core'
|
|
|
9
11
|
// when localStorage is empty. Priority for base: localStorage > omlx_config >
|
|
10
12
|
// hardcoded default. The key always comes from omlx_config.
|
|
11
13
|
//
|
|
14
|
+
// On top of that, when the resolved base is the default local :8000, loadEnv()
|
|
15
|
+
// probes the myllm reverse proxy (:8088/health, cached with a short TTL) and
|
|
16
|
+
// routes through it while it is alive — a runtime-only override that is never
|
|
17
|
+
// persisted, so the dialog keeps showing/editing the direct URL and a custom
|
|
18
|
+
// (non-:8000) base is never silently rerouted.
|
|
19
|
+
//
|
|
12
20
|
// storagePrefix namespaces the localStorage keys per app (so `task` and `mlmail`
|
|
13
21
|
// keep independent base/model). defaults seed first launch.
|
|
14
22
|
|
|
@@ -70,10 +78,15 @@ export function useOmlx({ storagePrefix = 'agent', defaultBaseUrl = DEFAULT_BASE
|
|
|
70
78
|
catch {
|
|
71
79
|
return // not running under Tauri — keep localStorage / defaults
|
|
72
80
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
if (env) {
|
|
82
|
+
if (env.apiKey) apiKey.value = env.apiKey
|
|
83
|
+
if (env.baseUrl && !readStored(baseUrlKey)) baseUrl.value = env.baseUrl
|
|
84
|
+
if (env.model && !readStored(modelKey)) model.value = env.model
|
|
85
|
+
}
|
|
86
|
+
// myllm proxy override — only for the default local target, runtime-only.
|
|
87
|
+
if (isDirectOmlxUrl(baseUrl.value)) {
|
|
88
|
+
baseUrl.value = await resolveOmlxBaseUrlCached({ directUrl: baseUrl.value, fetchFn: tauriFetch })
|
|
89
|
+
}
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/**
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { relaunch } from '@tauri-apps/plugin-process'
|
|
3
|
+
import { check } from '@tauri-apps/plugin-updater'
|
|
4
|
+
import { useQuasar } from 'quasar'
|
|
5
|
+
|
|
6
|
+
// Checks for an app update on mount and hourly thereafter, prompting via a
|
|
7
|
+
// Quasar dialog; after install, offers an immediate relaunch. Requires the
|
|
8
|
+
// host app's tauri.conf.json to configure plugins.updater (pubkey + endpoint)
|
|
9
|
+
// and capabilities/default.json to grant "updater:default" — without the
|
|
10
|
+
// capability, check() fails with a silent permission-denied that only surfaces
|
|
11
|
+
// via console.error (no dialog, no visible error).
|
|
12
|
+
//
|
|
13
|
+
// No-op in dev builds: tauri.conf.json's version is the CI-injected release
|
|
14
|
+
// version, not what's running locally, so the updater would always think dev
|
|
15
|
+
// code is stale.
|
|
16
|
+
|
|
17
|
+
const FIRST_CHECK_DELAY_MS = 3000
|
|
18
|
+
const CHECK_INTERVAL_MS = 60 * 60 * 1000
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*/
|
|
23
|
+
export function useUpdater() {
|
|
24
|
+
if (import.meta.env.DEV) return
|
|
25
|
+
|
|
26
|
+
const $q = useQuasar()
|
|
27
|
+
let timer = null
|
|
28
|
+
// A dialog is showing or an install already ran — don't stack background checks.
|
|
29
|
+
let busy = false
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Asks the configured update endpoint for a newer version and, if found,
|
|
33
|
+
* walks the user through the install flow.
|
|
34
|
+
* @returns {Promise<void>}
|
|
35
|
+
*/
|
|
36
|
+
async function checkForUpdates() {
|
|
37
|
+
if (busy) return
|
|
38
|
+
try {
|
|
39
|
+
const update = await check()
|
|
40
|
+
if (!update) return
|
|
41
|
+
|
|
42
|
+
busy = true
|
|
43
|
+
$q.dialog({
|
|
44
|
+
title: 'Доступне оновлення',
|
|
45
|
+
message: `Версія ${update.version} готова. Встановити зараз?`,
|
|
46
|
+
cancel: { label: 'Пізніше', flat: true },
|
|
47
|
+
ok: { label: 'Встановити', color: 'primary' },
|
|
48
|
+
persistent: true,
|
|
49
|
+
})
|
|
50
|
+
.onOk(() => installAndRelaunch(update))
|
|
51
|
+
.onCancel(() => {
|
|
52
|
+
busy = false
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// No network, or the updater isn't configured/permitted — don't bother
|
|
57
|
+
// the user, but leave a trace for diagnosis.
|
|
58
|
+
console.error('[updater] check failed:', error)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Downloads and installs the update with progress, then offers a relaunch.
|
|
64
|
+
* @param {import('@tauri-apps/plugin-updater').Update} update the update found by check()
|
|
65
|
+
* @returns {Promise<void>}
|
|
66
|
+
*/
|
|
67
|
+
async function installAndRelaunch(update) {
|
|
68
|
+
let downloaded = 0
|
|
69
|
+
let total = 0
|
|
70
|
+
const dismiss = $q.notify({ group: false, timeout: 0, spinner: true, message: 'Завантаження оновлення…' })
|
|
71
|
+
try {
|
|
72
|
+
await update.downloadAndInstall(event => {
|
|
73
|
+
switch (event.event) {
|
|
74
|
+
case 'Started': {
|
|
75
|
+
total = event.data.contentLength ?? 0
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
case 'Progress': {
|
|
79
|
+
downloaded += event.data.chunkLength
|
|
80
|
+
if (total) dismiss({ message: `Завантаження… ${Math.round((downloaded / total) * 100)}%` })
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
case 'Finished': {
|
|
84
|
+
dismiss()
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
// No default
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
// busy stays true: the update is already on disk, a repeat check would
|
|
91
|
+
// only offer to install the same thing again.
|
|
92
|
+
$q.dialog({
|
|
93
|
+
title: 'Оновлення встановлено',
|
|
94
|
+
message: `Перезапустити зараз, щоб перейти на версію ${update.version}?`,
|
|
95
|
+
cancel: { label: 'Пізніше', flat: true },
|
|
96
|
+
ok: { label: 'Перезапустити', color: 'primary' },
|
|
97
|
+
persistent: true,
|
|
98
|
+
}).onOk(() => relaunch())
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
dismiss()
|
|
102
|
+
busy = false
|
|
103
|
+
console.error('[updater] install failed:', error)
|
|
104
|
+
$q.notify({ message: `Помилка оновлення: ${error}`, color: 'negative', timeout: 5000 })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onMounted(() => {
|
|
109
|
+
setTimeout(checkForUpdates, FIRST_CHECK_DELAY_MS)
|
|
110
|
+
timer = setInterval(checkForUpdates, CHECK_INTERVAL_MS)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
onUnmounted(() => {
|
|
114
|
+
if (timer) clearInterval(timer)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this URL the default local omlx server — the only target eligible for
|
|
3
|
+
* the proxy override? A user who deliberately pointed an app at another
|
|
4
|
+
* host/port must never be silently rerouted (the proxy's upstream is
|
|
5
|
+
* hardwired to the local :8000).
|
|
6
|
+
* @param {string} url candidate base URL
|
|
7
|
+
* @returns {boolean} true for 127.0.0.1:8000 / localhost:8000, false otherwise (incl. parse errors)
|
|
8
|
+
*/
|
|
9
|
+
export function isDirectOmlxUrl(url: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* One-shot probe: GET {proxy origin}/health with a short timeout. 2xx means
|
|
12
|
+
* the proxy (and omlx behind it) is alive → use the proxy; anything else
|
|
13
|
+
* (timeout, refused, 502) → use the direct URL.
|
|
14
|
+
* @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number }} [params] config
|
|
15
|
+
* @returns {Promise<string>} the base URL to use
|
|
16
|
+
*/
|
|
17
|
+
export function resolveOmlxBaseUrl({ directUrl, proxyUrl, fetchFn, timeoutMs, }?: {
|
|
18
|
+
directUrl?: string;
|
|
19
|
+
proxyUrl?: string;
|
|
20
|
+
fetchFn?: typeof fetch;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Cached {@link resolveOmlxBaseUrl}: at most one probe per proxyUrl per TTL
|
|
25
|
+
* window, so callers may resolve before every LLM call without paying the
|
|
26
|
+
* probe latency each time, while still noticing the proxy starting/stopping
|
|
27
|
+
* within one TTL.
|
|
28
|
+
* @param {{ directUrl?: string, proxyUrl?: string, fetchFn?: typeof fetch, timeoutMs?: number, ttlMs?: number, now?: () => number }} [params] config
|
|
29
|
+
* @returns {Promise<string>} the base URL to use
|
|
30
|
+
*/
|
|
31
|
+
export function resolveOmlxBaseUrlCached({ ttlMs, now, ...probeOptions }?: {
|
|
32
|
+
directUrl?: string;
|
|
33
|
+
proxyUrl?: string;
|
|
34
|
+
fetchFn?: typeof fetch;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
ttlMs?: number;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
/** Test hook: drop all cached probe results. */
|
|
40
|
+
export function __resetOmlxBaseUrlCache(): void;
|
|
41
|
+
export const DIRECT_OMLX_BASE_URL: "http://127.0.0.1:8000/v1";
|
|
42
|
+
export const PROXY_OMLX_BASE_URL: "http://127.0.0.1:8088/v1";
|
package/types/index.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export { handleApprove, handleRequest, handleRespond } from "./core/agent-handle
|
|
|
4
4
|
export { createDispatch, validateInput } from "./core/dispatch.js";
|
|
5
5
|
export { createOpenAiChat, runAgent } from "./core/llm.js";
|
|
6
6
|
export { listOmlxModels, resolveModel } from "./core/omlx-models.js";
|
|
7
|
+
export { DIRECT_OMLX_BASE_URL, isDirectOmlxUrl, PROXY_OMLX_BASE_URL, resolveOmlxBaseUrl, resolveOmlxBaseUrlCached } from "./core/resolve-omlx-base-url.js";
|
|
7
8
|
export { listTools, toJsonSchema, toolManifest } from "./core/manifest.js";
|
|
8
9
|
export { classify, DEFAULT_ACTOR_TIERS, scopedManifest, scopedToolNames } from "./core/scope.js";
|