dsh-instruction-memory 1.4.6 → 1.4.8
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 +129 -0
- package/README.md +155 -19
- package/install.mjs +79 -10
- package/lib/client.js +169 -9
- package/lib/index.js +504 -35
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -20,7 +20,40 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
const React = require('react')
|
|
21
21
|
const h = React.createElement
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* F19: the route path RESOLVED against the page, not assumed to be at root.
|
|
25
|
+
*
|
|
26
|
+
* `/instruction-memory/api` is correct only when the GUI is served from `/`.
|
|
27
|
+
* Behind a reverse proxy that mounts the app under a prefix (`/ui/`) the
|
|
28
|
+
* browser posts to the proxy root instead, and every request 404s even
|
|
29
|
+
* though the panel rendered. The harness itself resolves its own client
|
|
30
|
+
* paths the same way (`new URL(path.slice(1), document.baseURI)`), so this
|
|
31
|
+
* follows the page wherever it is served from.
|
|
32
|
+
*
|
|
33
|
+
* Computed per call, not once at load: a test harness (or a page that
|
|
34
|
+
* rewrites `<base>`) can establish the base after this module evaluates.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} [suffix] query string to append, e.g. `?lite=1`.
|
|
37
|
+
* @returns {string} absolute or root-relative URL for the route.
|
|
38
|
+
*/
|
|
39
|
+
const apiEndpoint = (suffix) => {
|
|
40
|
+
const tail = typeof suffix === 'string' ? suffix : ''
|
|
41
|
+
const path = 'instruction-memory/api'
|
|
42
|
+
if (typeof document === 'undefined' || typeof document.baseURI !== 'string' || document.baseURI === '') {
|
|
43
|
+
return '/' + path + tail
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const base = new URL(document.baseURI)
|
|
47
|
+
// A base of `https://host/ui` (no trailing slash) would otherwise
|
|
48
|
+
// resolve against `/` — `ui` is the mount point itself, so treat it as
|
|
49
|
+
// the directory the app lives in.
|
|
50
|
+
if (!base.pathname.endsWith('/')) base.pathname += '/'
|
|
51
|
+
const url = new URL(path, base)
|
|
52
|
+
return url.pathname + url.search + tail
|
|
53
|
+
} catch {
|
|
54
|
+
return '/' + path + tail
|
|
55
|
+
}
|
|
56
|
+
}
|
|
24
57
|
|
|
25
58
|
// The Host bounds one delivery at 60 s (lib/mailer.js SMTP_TOTAL_TIMEOUT_MS).
|
|
26
59
|
// Waiting longer than the generic 15 s keeps the panel honest: aborting while
|
|
@@ -383,6 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
383
416
|
'diag.copied': '诊断信息已复制到剪贴板',
|
|
384
417
|
'diag.copyManual': '自动复制不可用;请手动全选复制下方文本。',
|
|
385
418
|
'diag.hostWarn': '安全提示:设置路由当前绑定在非回环地址({host}),同一网络内的设备可以无鉴权访问此页面,请勿在不可信网络暴露;如需对外发布,请阅读 README 的「经反向代理对外发布」。',
|
|
419
|
+
'diag.hostWarnFence': '安全提示:设置路由当前绑定在非回环地址({host}),但宿主鉴权门已挂载——访问仍需登录 DSH 后的浏览器会话,未登录请求会被 401/403 拒绝;仍建议只在可信网络暴露。',
|
|
386
420
|
'diag.groupStorage': '存储',
|
|
387
421
|
'diag.groupInjection': '注入',
|
|
388
422
|
'diag.groupBackup': '备份与历史',
|
|
@@ -667,6 +701,7 @@ window.__ModuleLoader__.load({
|
|
|
667
701
|
'diag.copied': 'Diagnostics copied to the clipboard',
|
|
668
702
|
'diag.copyManual': 'Automatic copy is unavailable; select and copy the text below manually.',
|
|
669
703
|
'diag.hostWarn': 'Security notice: the settings route is bound to a non-loopback address ({host}); other devices on the same network can reach this page without authentication. Do not expose it on an untrusted network — for public deployments read “Publishing behind a reverse proxy” in the README.',
|
|
704
|
+
'diag.hostWarnFence': 'Security notice: the settings route is bound to a non-loopback address ({host}), but the host authentication fence is attached — access still requires a signed-in DSH browser session, and unauthenticated requests are refused with 401/403. Keep it on a trusted network anyway.',
|
|
670
705
|
'diag.groupStorage': 'Storage',
|
|
671
706
|
'diag.groupInjection': 'Injection',
|
|
672
707
|
'diag.groupBackup': 'Backup & history',
|
|
@@ -1469,7 +1504,7 @@ window.__ModuleLoader__.load({
|
|
|
1469
1504
|
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
|
1470
1505
|
const timer = controller !== null ? setTimeout(() => controller.abort(), timeoutMs) : null
|
|
1471
1506
|
try {
|
|
1472
|
-
const response = await fetch(
|
|
1507
|
+
const response = await fetch(apiEndpoint(), {
|
|
1473
1508
|
method: 'POST',
|
|
1474
1509
|
headers: { 'content-type': 'application/json' },
|
|
1475
1510
|
body: JSON.stringify({ method, args: args === undefined ? null : args }),
|
|
@@ -2522,7 +2557,7 @@ window.__ModuleLoader__.load({
|
|
|
2522
2557
|
// flag ignores the query string and keeps sending full frames — the
|
|
2523
2558
|
// branch below still accepts those, so the negotiation is additive and
|
|
2524
2559
|
// one-way: neither side has to know the other's version.
|
|
2525
|
-
const source = new EventSource(
|
|
2560
|
+
const source = new EventSource(apiEndpoint('?lite=1'))
|
|
2526
2561
|
let alive = true
|
|
2527
2562
|
let refreshing = false
|
|
2528
2563
|
let missed = false
|
|
@@ -2875,9 +2910,15 @@ window.__ModuleLoader__.load({
|
|
|
2875
2910
|
feedbackCard,
|
|
2876
2911
|
// F12: the non-loopback warning stays high in the panel — directly
|
|
2877
2912
|
// under the feedback entry — so it cannot be missed.
|
|
2913
|
+
// Two different situations, two different texts: with the host fence
|
|
2914
|
+
// attached the route is NOT open to the LAN (it needs a signed-in
|
|
2915
|
+
// browser session), so claiming "no authentication" would be a false
|
|
2916
|
+
// alarm. `authFence` is what the route itself checked on this request.
|
|
2878
2917
|
security.hostGuarded === false
|
|
2879
2918
|
? h('div', { className: 'im-hostwarn', key: 'hostwarn' },
|
|
2880
|
-
|
|
2919
|
+
security.authFence === true
|
|
2920
|
+
? t('diag.hostWarnFence', { host: security.boundHost !== '' ? security.boundHost : '?' })
|
|
2921
|
+
: t('diag.hostWarn', { host: security.boundHost !== '' ? security.boundHost : '?' }))
|
|
2881
2922
|
: null,
|
|
2882
2923
|
h('div', { className: 'im-row' },
|
|
2883
2924
|
h('div', null,
|
|
@@ -3676,7 +3717,12 @@ window.__ModuleLoader__.load({
|
|
|
3676
3717
|
try {
|
|
3677
3718
|
mountPanel(ctx, target)
|
|
3678
3719
|
return true
|
|
3679
|
-
} catch {
|
|
3720
|
+
} catch (err) {
|
|
3721
|
+
// F19: this used to swallow the throw outright — the panel simply
|
|
3722
|
+
// never appeared and nothing anywhere said so.
|
|
3723
|
+
const detail = '注册设置页时抛出异常:' + (err !== null && err !== undefined && typeof err.message === 'string' ? err.message : String(err))
|
|
3724
|
+
console.error('[instruction-memory] ' + detail)
|
|
3725
|
+
showMountFailureNotice(detail)
|
|
3680
3726
|
return false
|
|
3681
3727
|
}
|
|
3682
3728
|
}
|
|
@@ -3696,10 +3742,10 @@ window.__ModuleLoader__.load({
|
|
|
3696
3742
|
return
|
|
3697
3743
|
}
|
|
3698
3744
|
if (attempt >= SLOT_RETRY_LIMIT) {
|
|
3699
|
-
|
|
3700
|
-
'
|
|
3701
|
-
|
|
3702
|
-
)
|
|
3745
|
+
const detail = '宿主没有发布 slots 服务(重试 '
|
|
3746
|
+
+ SLOT_RETRY_LIMIT + ' 次,约 ' + (SLOT_RETRY_LIMIT * SLOT_RETRY_DELAY_MS) + 'ms)'
|
|
3747
|
+
console.error('[instruction-memory] ' + detail + ';settings page not registered')
|
|
3748
|
+
showMountFailureNotice(detail)
|
|
3703
3749
|
return
|
|
3704
3750
|
}
|
|
3705
3751
|
schedule(retry)
|
|
@@ -3707,6 +3753,120 @@ window.__ModuleLoader__.load({
|
|
|
3707
3753
|
schedule(retry)
|
|
3708
3754
|
}
|
|
3709
3755
|
|
|
3756
|
+
/**
|
|
3757
|
+
* R8: read the running plugin version for the mount-failure notice.
|
|
3758
|
+
*
|
|
3759
|
+
* The settings route does not depend on `slots`, so `state` normally
|
|
3760
|
+
* answers even when the panel could not mount — which is exactly the
|
|
3761
|
+
* moment someone copies a version number into a bug report. Best effort in
|
|
3762
|
+
* every direction: no `fetch`, no route, a refusal by the host's auth
|
|
3763
|
+
* gate, a timeout or a malformed body all resolve to null, and the notice
|
|
3764
|
+
* simply omits the line.
|
|
3765
|
+
*/
|
|
3766
|
+
function probePluginVersion() {
|
|
3767
|
+
try {
|
|
3768
|
+
if (typeof fetch !== 'function') return Promise.resolve(null)
|
|
3769
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
|
3770
|
+
const options = {
|
|
3771
|
+
method: 'POST',
|
|
3772
|
+
headers: { 'content-type': 'application/json' },
|
|
3773
|
+
body: JSON.stringify({ method: 'state', args: null }),
|
|
3774
|
+
}
|
|
3775
|
+
if (controller !== null) {
|
|
3776
|
+
options.signal = controller.signal
|
|
3777
|
+
const timer = setTimeout(() => controller.abort(), 2000)
|
|
3778
|
+
if (timer !== null && typeof timer === 'object' && typeof timer.unref === 'function') timer.unref()
|
|
3779
|
+
}
|
|
3780
|
+
return fetch(apiEndpoint(), options)
|
|
3781
|
+
.then((response) => (response.ok === true ? response.json() : null))
|
|
3782
|
+
.then((payload) => (payload !== null && payload !== undefined
|
|
3783
|
+
&& payload.ok === true
|
|
3784
|
+
&& payload.snapshot !== null && typeof payload.snapshot === 'object'
|
|
3785
|
+
&& typeof payload.snapshot.pluginVersion === 'string'
|
|
3786
|
+
&& payload.snapshot.pluginVersion !== ''
|
|
3787
|
+
? payload.snapshot.pluginVersion
|
|
3788
|
+
: null))
|
|
3789
|
+
.catch(() => null)
|
|
3790
|
+
} catch {
|
|
3791
|
+
return Promise.resolve(null)
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
/**
|
|
3796
|
+
* F19: make a failed mount VISIBLE instead of console-only.
|
|
3797
|
+
*
|
|
3798
|
+
* The panel is how the user sees and edits their memory. When `slots` never
|
|
3799
|
+
* resolves, the previous code wrote one console.error: the plugin looked
|
|
3800
|
+
* installed, the settings page had no section, and nothing told the user
|
|
3801
|
+
* why. A console line is not a user interface.
|
|
3802
|
+
*
|
|
3803
|
+
* The notice is deliberately dependency-free — plain DOM, inline styles,
|
|
3804
|
+
* `textContent` only (never innerHTML) — because the very thing that failed
|
|
3805
|
+
* is the service that would supply a styled component. It is non-modal and
|
|
3806
|
+
* self-removing so it cannot become a permanent artefact of a transient
|
|
3807
|
+
* race (a late `slots` publish still mounts the panel; the banner then
|
|
3808
|
+
* only says the panel appeared late).
|
|
3809
|
+
*/
|
|
3810
|
+
function showMountFailureNotice(detail) {
|
|
3811
|
+
try {
|
|
3812
|
+
if (typeof document === 'undefined' || document.body === null) return
|
|
3813
|
+
const id = 'dsh-instruction-memory-mount-failure'
|
|
3814
|
+
const existing = document.getElementById(id)
|
|
3815
|
+
if (existing !== null) existing.remove()
|
|
3816
|
+
|
|
3817
|
+
const box = document.createElement('div')
|
|
3818
|
+
box.id = id
|
|
3819
|
+
box.setAttribute('role', 'status')
|
|
3820
|
+
box.style.cssText = [
|
|
3821
|
+
'position:fixed', 'right:16px', 'bottom:16px', 'z-index:2147483000',
|
|
3822
|
+
'max-width:min(380px,calc(100vw - 32px))',
|
|
3823
|
+
'background:#3a1d1d', 'color:#ffe8e8', 'border:1px solid #7a3a3a',
|
|
3824
|
+
'border-radius:10px', 'padding:12px 14px',
|
|
3825
|
+
'font:13px/1.5 system-ui,-apple-system,Segoe UI,sans-serif',
|
|
3826
|
+
'box-shadow:0 8px 24px rgba(0,0,0,.45)',
|
|
3827
|
+
].join(';')
|
|
3828
|
+
|
|
3829
|
+
const title = document.createElement('div')
|
|
3830
|
+
title.style.cssText = 'font-weight:600;margin-bottom:4px'
|
|
3831
|
+
title.textContent = '指令记忆:设置页挂载失败'
|
|
3832
|
+
box.appendChild(title)
|
|
3833
|
+
|
|
3834
|
+
const body = document.createElement('div')
|
|
3835
|
+
body.textContent = detail
|
|
3836
|
+
box.appendChild(body)
|
|
3837
|
+
|
|
3838
|
+
const hint = document.createElement('div')
|
|
3839
|
+
hint.style.cssText = 'margin-top:6px;opacity:.75'
|
|
3840
|
+
hint.textContent = '记忆注入本身仍在工作;请检查宿主版本与插件日志,或重装插件后刷新页面。'
|
|
3841
|
+
box.appendChild(hint)
|
|
3842
|
+
|
|
3843
|
+
const close = document.createElement('button')
|
|
3844
|
+
close.type = 'button'
|
|
3845
|
+
close.textContent = '关闭'
|
|
3846
|
+
close.style.cssText = 'margin-top:8px;background:transparent;color:#ffe8e8;border:1px solid #7a3a3a;border-radius:6px;padding:2px 10px;cursor:pointer;font:inherit'
|
|
3847
|
+
close.addEventListener('click', () => box.remove())
|
|
3848
|
+
box.appendChild(close)
|
|
3849
|
+
|
|
3850
|
+
document.body.appendChild(box)
|
|
3851
|
+
// R8: name the running version. Appended after the banner exists so a
|
|
3852
|
+
// slow or refused probe can never delay the warning itself.
|
|
3853
|
+
probePluginVersion().then((version) => {
|
|
3854
|
+
if (version === null) return
|
|
3855
|
+
const meta = document.createElement('div')
|
|
3856
|
+
meta.style.cssText = 'margin-top:6px;opacity:.75'
|
|
3857
|
+
meta.textContent = '插件版本 ' + version
|
|
3858
|
+
box.appendChild(meta)
|
|
3859
|
+
}).catch(() => {})
|
|
3860
|
+
// Non-modal and self-removing: a failure that persists past 30 s has
|
|
3861
|
+
// already been reported once; leaving a banner on screen forever is
|
|
3862
|
+
// worse than a dismissible one the user can re-trigger by reloading.
|
|
3863
|
+
const timer = setTimeout(() => box.remove(), 30000)
|
|
3864
|
+
if (timer !== null && typeof timer === 'object' && typeof timer.unref === 'function') timer.unref()
|
|
3865
|
+
} catch {
|
|
3866
|
+
// A banner is a courtesy: never let it be the reason the plugin throws.
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3710
3870
|
function schedule(fn) {
|
|
3711
3871
|
const timer = setTimeout(fn, SLOT_RETRY_DELAY_MS)
|
|
3712
3872
|
// Never hold a process open just to finish retrying.
|