@goodandready/dsh-context-lens 0.1.16 → 0.1.18
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 +21 -5
- package/README.ru.md +21 -5
- package/README.zh.md +21 -1
- package/lib/ast/skeletonizer.js +31 -15
- package/lib/client.js +284 -48
- package/lib/http.js +114 -0
- package/lib/index.js +136 -190
- package/lib/tokens/tracker.js +14 -6
- package/lib/tools.js +179 -0
- package/lib/updater.js +237 -0
- package/package.json +3 -1
package/lib/client.js
CHANGED
|
@@ -5,7 +5,14 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
const React = require('react');
|
|
6
6
|
const NS = '@goodandready/dsh-context-lens';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const en = {
|
|
9
|
+
'update.available': 'Update available: v{latestVersion} (current: v{currentVersion})',
|
|
10
|
+
'update.btn': 'Update Now',
|
|
11
|
+
'update.updating': 'Updating…',
|
|
12
|
+
'update.done': 'Successfully updated to v{version}! Please restart DSH.',
|
|
13
|
+
'update.failed': 'Update failed: {error}',
|
|
14
|
+
'update.checking': 'Checking for updates…',
|
|
15
|
+
'update.up_to_date': 'Up to date',
|
|
9
16
|
title: 'Context Lens & Token Guard',
|
|
10
17
|
sub: 'AST compression, log filtering, token budget',
|
|
11
18
|
mode: 'Compression mode',
|
|
@@ -23,39 +30,55 @@ window.__ModuleLoader__.load({
|
|
|
23
30
|
lowBudget: 'Budget nearly exhausted',
|
|
24
31
|
threshold: 'Auto-compress threshold (chars)',
|
|
25
32
|
autoCollapse: 'Warn when budget is nearly exhausted',
|
|
33
|
+
budgetAlertPercent: 'Budget alert threshold (%)',
|
|
26
34
|
previewApprox: 'Server preview',
|
|
27
35
|
noData: 'No data yet',
|
|
28
36
|
refresh: 'Refresh',
|
|
29
37
|
ops: 'ops',
|
|
30
38
|
active: 'Active',
|
|
31
39
|
readyState: 'Ready',
|
|
32
|
-
lowBudgetBadge: 'Low Budget ⚠'
|
|
40
|
+
lowBudgetBadge: 'Low Budget ⚠',
|
|
41
|
+
focusPaths: 'Active focus',
|
|
42
|
+
noFocus: 'No focus paths set',
|
|
43
|
+
clearFocus: 'Clear focus'
|
|
33
44
|
};
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
|
|
46
|
+
const zh = {
|
|
47
|
+
'update.available': '发现新版本 v{latestVersion}(当前版本 v{currentVersion})',
|
|
48
|
+
'update.btn': '立即更新',
|
|
49
|
+
'update.updating': '正在更新…',
|
|
50
|
+
'update.done': '更新成功至 v{version}!请重启 DSH 服务生效。',
|
|
51
|
+
'update.failed': '更新失败:{error}',
|
|
52
|
+
'update.checking': '正在检查更新…',
|
|
53
|
+
'update.up_to_date': '已是最新版本',
|
|
54
|
+
title: 'Context Lens 上下文镜头与 Token 守卫',
|
|
55
|
+
sub: 'AST 代码精简、日志过滤与 Token 预算监控',
|
|
56
|
+
mode: '压缩模式',
|
|
57
|
+
depth: 'AST 最大深度',
|
|
58
|
+
tracking: '追踪 Token 节省',
|
|
59
|
+
preview: '日志预览',
|
|
60
|
+
compress: '压缩',
|
|
61
|
+
saved: '已节省',
|
|
62
|
+
tokens: 'Token',
|
|
63
|
+
placeholder: '在此粘贴 Jest/Pytest 等测试日志…',
|
|
64
|
+
saving: '保存中…',
|
|
65
|
+
ready: '已就绪',
|
|
66
|
+
budget: '预算',
|
|
67
|
+
history: '近期操作',
|
|
68
|
+
lowBudget: 'Token 预算即将耗尽',
|
|
69
|
+
threshold: '自动压缩阈值 (字符数)',
|
|
70
|
+
autoCollapse: '预算偏低时在界面提示警告',
|
|
71
|
+
budgetAlertPercent: '预算警告阈值 (%)',
|
|
72
|
+
previewApprox: '服务端压缩预览',
|
|
73
|
+
noData: '暂无数据',
|
|
74
|
+
refresh: '刷新',
|
|
75
|
+
ops: '次操作',
|
|
76
|
+
active: '运行中',
|
|
77
|
+
readyState: '已就绪',
|
|
78
|
+
lowBudgetBadge: '预算紧张 ⚠',
|
|
79
|
+
focusPaths: '当前聚焦路径',
|
|
80
|
+
noFocus: '未设置聚焦路径',
|
|
81
|
+
clearFocus: '清除聚焦'
|
|
59
82
|
};
|
|
60
83
|
|
|
61
84
|
function ensureCss() {
|
|
@@ -99,12 +122,12 @@ window.__ModuleLoader__.load({
|
|
|
99
122
|
.cl-badge-ok {
|
|
100
123
|
border-color: var(--dsw-alias-state-success-primary, #10b981);
|
|
101
124
|
color: var(--dsw-alias-state-success-primary, #10b981);
|
|
102
|
-
background:
|
|
125
|
+
background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 8%, transparent);
|
|
103
126
|
}
|
|
104
127
|
.cl-badge-warn {
|
|
105
128
|
border-color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
106
129
|
color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
107
|
-
background:
|
|
130
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent);
|
|
108
131
|
}
|
|
109
132
|
.cl-stat-grid {
|
|
110
133
|
display: grid;
|
|
@@ -202,7 +225,7 @@ window.__ModuleLoader__.load({
|
|
|
202
225
|
.cl-banner-warn {
|
|
203
226
|
padding: 10px 14px;
|
|
204
227
|
border-radius: 8px;
|
|
205
|
-
background:
|
|
228
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 10%, transparent);
|
|
206
229
|
border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
|
|
207
230
|
color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
208
231
|
font-size: 12px;
|
|
@@ -220,10 +243,22 @@ window.__ModuleLoader__.load({
|
|
|
220
243
|
.cl-hist-item:last-child {
|
|
221
244
|
border-bottom: none;
|
|
222
245
|
}
|
|
246
|
+
.cl-tag {
|
|
247
|
+
display: inline-flex;
|
|
248
|
+
align-items: center;
|
|
249
|
+
gap: 4px;
|
|
250
|
+
padding: 2px 6px;
|
|
251
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
252
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
253
|
+
border-radius: 4px;
|
|
254
|
+
font-size: 11px;
|
|
255
|
+
font-family: monospace;
|
|
256
|
+
color: var(--dsw-alias-label-secondary);
|
|
257
|
+
}
|
|
223
258
|
.cl-error-box {
|
|
224
259
|
padding: 12px;
|
|
225
260
|
border-radius: 8px;
|
|
226
|
-
background:
|
|
261
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent);
|
|
227
262
|
border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
|
|
228
263
|
color: var(--dsw-alias-state-error-primary, #ef4444);
|
|
229
264
|
font-size: 12px;
|
|
@@ -232,6 +267,18 @@ window.__ModuleLoader__.load({
|
|
|
232
267
|
document.head.appendChild(style);
|
|
233
268
|
}
|
|
234
269
|
|
|
270
|
+
function makeT(dict, fallback) {
|
|
271
|
+
return function t(key, vars) {
|
|
272
|
+
let val = (dict && dict[key]) || (fallback && fallback[key]) || key;
|
|
273
|
+
if (vars && typeof val === 'string') {
|
|
274
|
+
for (const k of Object.keys(vars)) {
|
|
275
|
+
val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return val;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
235
282
|
function createErrorBoundary() {
|
|
236
283
|
if (!React || typeof React.Component !== 'function') {
|
|
237
284
|
return function NoopBoundary(props) { return (props && props.children) || null; };
|
|
@@ -270,13 +317,13 @@ window.__ModuleLoader__.load({
|
|
|
270
317
|
try {
|
|
271
318
|
if (ctx && ctx.locale && typeof ctx.locale.getSnapshot === 'function') {
|
|
272
319
|
const snap = ctx.locale.getSnapshot();
|
|
273
|
-
if (snap && snap.active && snap.active.startsWith('
|
|
320
|
+
if (snap && snap.active && snap.active.startsWith('zh')) return zh;
|
|
274
321
|
}
|
|
275
|
-
} catch {}
|
|
322
|
+
} catch (err) { void err; }
|
|
276
323
|
return en;
|
|
277
324
|
}
|
|
278
325
|
|
|
279
|
-
function StatusPanel({ stats, history, labels, onRefresh, title, showBadge, compact }) {
|
|
326
|
+
function StatusPanel({ stats, history, focus, sessionId, labels, onRefresh, onClearFocus, title, showBadge, compact }) {
|
|
280
327
|
ensureCss();
|
|
281
328
|
const L = labels || en;
|
|
282
329
|
const warn = !!(stats && stats.lowBudget);
|
|
@@ -351,6 +398,33 @@ window.__ModuleLoader__.load({
|
|
|
351
398
|
));
|
|
352
399
|
}
|
|
353
400
|
|
|
401
|
+
// Active focus paths section
|
|
402
|
+
const focusPaths = (focus && focus.paths) || [];
|
|
403
|
+
if (focusPaths.length > 0 || onClearFocus) {
|
|
404
|
+
children.push(React.createElement('div', {
|
|
405
|
+
key: 'focus',
|
|
406
|
+
style: { marginTop: 10, paddingTop: 8, borderTop: '1px solid var(--dsw-alias-border-l2)' }
|
|
407
|
+
},
|
|
408
|
+
React.createElement('div', {
|
|
409
|
+
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }
|
|
410
|
+
},
|
|
411
|
+
React.createElement('span', { style: { fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-secondary)' } }, L.focusPaths),
|
|
412
|
+
focusPaths.length > 0 && onClearFocus
|
|
413
|
+
? React.createElement('button', {
|
|
414
|
+
type: 'button',
|
|
415
|
+
onClick: onClearFocus,
|
|
416
|
+
style: { background: 'none', border: 'none', padding: 0, fontSize: 10, color: 'var(--dsw-alias-label-tertiary)', cursor: 'pointer', textDecoration: 'underline' }
|
|
417
|
+
}, L.clearFocus)
|
|
418
|
+
: null
|
|
419
|
+
),
|
|
420
|
+
focusPaths.length > 0
|
|
421
|
+
? React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 4 } },
|
|
422
|
+
...focusPaths.slice(0, 4).map((p, i) => React.createElement('span', { key: i, className: 'cl-tag' }, p))
|
|
423
|
+
)
|
|
424
|
+
: React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', fontStyle: 'italic' } }, L.noFocus)
|
|
425
|
+
));
|
|
426
|
+
}
|
|
427
|
+
|
|
354
428
|
if (history && history.length) {
|
|
355
429
|
children.push(React.createElement('div', {
|
|
356
430
|
key: 'hist',
|
|
@@ -386,20 +460,31 @@ window.__ModuleLoader__.load({
|
|
|
386
460
|
.then((r) => r.ok ? r.json() : null);
|
|
387
461
|
}
|
|
388
462
|
|
|
463
|
+
function clearLensFocus(sessionId) {
|
|
464
|
+
const q = sessionId ? '?sessionId=' + encodeURIComponent(sessionId) : '';
|
|
465
|
+
return fetch('/dsh-context-lens/clear-focus' + q, { method: 'POST', headers: { accept: 'application/json' } })
|
|
466
|
+
.then((r) => r.ok ? r.json() : null);
|
|
467
|
+
}
|
|
468
|
+
|
|
389
469
|
function LensTabInner({ ctx: _ctx, scope }) {
|
|
390
470
|
ensureCss();
|
|
391
471
|
const [stats, setStats] = React.useState(null);
|
|
392
472
|
const [history, setHistory] = React.useState([]);
|
|
473
|
+
const [focus, setFocus] = React.useState(null);
|
|
474
|
+
const [sessionId, setSessionId] = React.useState(null);
|
|
393
475
|
const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
|
|
394
476
|
const apply = (j) => {
|
|
395
477
|
if (j && j.stats) setStats(j.stats);
|
|
396
478
|
if (j && j.history) setHistory(j.history || []);
|
|
479
|
+
if (j && j.focus) setFocus(j.focus);
|
|
480
|
+
if (j && j.sessionId) setSessionId(j.sessionId);
|
|
397
481
|
};
|
|
398
482
|
const refresh = () => fetchLensStatus().then(apply).catch(() => {});
|
|
483
|
+
const handleClearFocus = () => clearLensFocus(sessionId).then(() => refresh()).catch(() => {});
|
|
399
484
|
React.useEffect(() => { refresh(); }, []);
|
|
400
485
|
return React.createElement('div', { style: { padding: 12 } },
|
|
401
486
|
React.createElement(StatusPanel, {
|
|
402
|
-
stats, history, labels: L, onRefresh: refresh, title: 'Context Lens', showBadge: false, compact: false
|
|
487
|
+
stats, history, focus, sessionId, labels: L, onRefresh: refresh, onClearFocus: handleClearFocus, title: 'Context Lens', showBadge: false, compact: false
|
|
403
488
|
})
|
|
404
489
|
);
|
|
405
490
|
}
|
|
@@ -414,6 +499,8 @@ window.__ModuleLoader__.load({
|
|
|
414
499
|
ensureCss();
|
|
415
500
|
const [stats, setStats] = React.useState(null);
|
|
416
501
|
const [history, setHistory] = React.useState([]);
|
|
502
|
+
const [focus, setFocus] = React.useState(null);
|
|
503
|
+
const [sessionId, setSessionId] = React.useState(null);
|
|
417
504
|
const [open, setOpen] = React.useState(false);
|
|
418
505
|
const ref = React.useRef(null);
|
|
419
506
|
const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
|
|
@@ -421,8 +508,12 @@ window.__ModuleLoader__.load({
|
|
|
421
508
|
const fetchStatus = () => fetchLensStatus().then((j) => {
|
|
422
509
|
if (j && j.stats) setStats(j.stats);
|
|
423
510
|
if (j && j.history) setHistory(j.history || []);
|
|
511
|
+
if (j && j.focus) setFocus(j.focus);
|
|
512
|
+
if (j && j.sessionId) setSessionId(j.sessionId);
|
|
424
513
|
}).catch(() => {});
|
|
425
514
|
|
|
515
|
+
const handleClearFocus = () => clearLensFocus(sessionId).then(() => fetchStatus()).catch(() => {});
|
|
516
|
+
|
|
426
517
|
React.useEffect(() => {
|
|
427
518
|
let alive = true;
|
|
428
519
|
fetchStatus();
|
|
@@ -469,13 +560,13 @@ window.__ModuleLoader__.load({
|
|
|
469
560
|
style: {
|
|
470
561
|
position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 280,
|
|
471
562
|
background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
472
|
-
borderRadius: 10, boxShadow: '
|
|
563
|
+
borderRadius: 10, boxShadow: 'var(--dsw-alias-modal-shadow, var(--dsw-alias-shadow-large, 0 4px 16px var(--dsw-alias-border-l1)))', padding: '12px 14px',
|
|
473
564
|
zIndex: 1000, fontSize: 12, color: 'var(--dsw-alias-label-primary)', cursor: 'default', textAlign: 'left'
|
|
474
565
|
},
|
|
475
566
|
onClick: (e) => e.stopPropagation()
|
|
476
567
|
},
|
|
477
568
|
React.createElement(StatusPanel, {
|
|
478
|
-
stats, history, labels: L, onRefresh: fetchStatus, title: 'Context Lens', showBadge: true, compact: true
|
|
569
|
+
stats, history, focus, sessionId, labels: L, onRefresh: fetchStatus, onClearFocus: handleClearFocus, title: 'Context Lens', showBadge: true, compact: true
|
|
479
570
|
})
|
|
480
571
|
) : null;
|
|
481
572
|
|
|
@@ -507,6 +598,7 @@ window.__ModuleLoader__.load({
|
|
|
507
598
|
astSkeletonMaxDepth: 3,
|
|
508
599
|
tokenSavingsTracking: true,
|
|
509
600
|
budgetLimit: 100000,
|
|
601
|
+
budgetAlertPercent: 90,
|
|
510
602
|
autoCollapse: true,
|
|
511
603
|
autoCompressThreshold: 4000
|
|
512
604
|
});
|
|
@@ -521,7 +613,7 @@ window.__ModuleLoader__.load({
|
|
|
521
613
|
const scopeRef = React.useRef(null);
|
|
522
614
|
if (!scopeRef.current && _ctx) {
|
|
523
615
|
try {
|
|
524
|
-
const s = (_ctx.get && _ctx.get('
|
|
616
|
+
const s = _ctx.settingsScope || (_ctx.get && _ctx.get('settingsScope'));
|
|
525
617
|
if (s && typeof s.bind === 'function') {
|
|
526
618
|
scopeRef.current = s.bind({ namespace: NS });
|
|
527
619
|
}
|
|
@@ -561,7 +653,7 @@ window.__ModuleLoader__.load({
|
|
|
561
653
|
unsub = scope.subscribe(() => {
|
|
562
654
|
if (!cancelled) loadSnap();
|
|
563
655
|
});
|
|
564
|
-
} catch (
|
|
656
|
+
} catch (_err) { void _err; }
|
|
565
657
|
}
|
|
566
658
|
|
|
567
659
|
return () => {
|
|
@@ -570,6 +662,69 @@ window.__ModuleLoader__.load({
|
|
|
570
662
|
};
|
|
571
663
|
}, [scope]);
|
|
572
664
|
|
|
665
|
+
const [updateState, setUpdateState] = React.useState({
|
|
666
|
+
checking: false,
|
|
667
|
+
updating: false,
|
|
668
|
+
currentVersion: '0.1.18',
|
|
669
|
+
latestVersion: '',
|
|
670
|
+
updateAvailable: false,
|
|
671
|
+
canAutoUpdate: true,
|
|
672
|
+
notice: '',
|
|
673
|
+
error: ''
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const checkUpdate = React.useCallback(async () => {
|
|
677
|
+
setUpdateState((s) => ({ ...s, checking: true, error: '' }));
|
|
678
|
+
try {
|
|
679
|
+
const res = await fetch('/api/dsh-context-lens/update');
|
|
680
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
681
|
+
const data = await res.json().catch(() => ({}));
|
|
682
|
+
setUpdateState((s) => ({
|
|
683
|
+
...s,
|
|
684
|
+
checking: false,
|
|
685
|
+
currentVersion: data.currentVersion || s.currentVersion,
|
|
686
|
+
latestVersion: data.latestVersion || '',
|
|
687
|
+
updateAvailable: !!data.updateAvailable,
|
|
688
|
+
canAutoUpdate: data.canAutoUpdate !== false,
|
|
689
|
+
}));
|
|
690
|
+
} catch (_) {
|
|
691
|
+
setUpdateState((s) => ({ ...s, checking: false }));
|
|
692
|
+
}
|
|
693
|
+
}, []);
|
|
694
|
+
|
|
695
|
+
React.useEffect(() => {
|
|
696
|
+
if (expanded) checkUpdate();
|
|
697
|
+
}, [expanded, checkUpdate]);
|
|
698
|
+
|
|
699
|
+
async function handleTriggerUpdate() {
|
|
700
|
+
if (updateState.updating) return;
|
|
701
|
+
setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }));
|
|
702
|
+
try {
|
|
703
|
+
const res = await fetch('/api/dsh-context-lens/update', {
|
|
704
|
+
method: 'POST',
|
|
705
|
+
headers: { 'x-dsh-plugin-update': '1', 'x-dsh-context-lens-update': '1' }
|
|
706
|
+
});
|
|
707
|
+
const data = await res.json().catch(() => ({}));
|
|
708
|
+
if (!res.ok || data.ok === false || data.error) {
|
|
709
|
+
throw new Error(data.error || ('HTTP ' + res.status));
|
|
710
|
+
}
|
|
711
|
+
const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion;
|
|
712
|
+
setUpdateState((s) => ({
|
|
713
|
+
...s,
|
|
714
|
+
updating: false,
|
|
715
|
+
updateAvailable: false,
|
|
716
|
+
currentVersion: newVer,
|
|
717
|
+
notice: tt('update.done', { version: newVer }),
|
|
718
|
+
}));
|
|
719
|
+
} catch (e) {
|
|
720
|
+
setUpdateState((s) => ({
|
|
721
|
+
...s,
|
|
722
|
+
updating: false,
|
|
723
|
+
error: tt('update.failed', { error: e && e.message || String(e) }),
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
573
728
|
React.useEffect(() => {
|
|
574
729
|
if (!expanded) return;
|
|
575
730
|
fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
|
|
@@ -580,7 +735,8 @@ window.__ModuleLoader__.load({
|
|
|
580
735
|
}).catch(() => {});
|
|
581
736
|
}, [expanded, draft.autoCollapse]);
|
|
582
737
|
|
|
583
|
-
const
|
|
738
|
+
const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
|
|
739
|
+
const tt = React.useMemo(() => (t ? t : makeT(L, en)), [t, L]);
|
|
584
740
|
|
|
585
741
|
let ChevronIcon = null;
|
|
586
742
|
try {
|
|
@@ -589,9 +745,29 @@ window.__ModuleLoader__.load({
|
|
|
589
745
|
} catch (e) { ChevronIcon = null; }
|
|
590
746
|
|
|
591
747
|
const Chevron = ChevronIcon ? function ChevronNode(p) {
|
|
592
|
-
return React.createElement(ChevronIcon, { className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''), style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-tertiary)', transition: 'transform .16s', transform: p.open ? 'rotate(180deg)' : 'none' } });
|
|
748
|
+
return React.createElement(ChevronIcon, { className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''), style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-tertiary)', transition: 'transform .16s ease', transform: p.open ? 'rotate(180deg)' : 'none' } });
|
|
593
749
|
} : function Fallback(p) {
|
|
594
|
-
return React.createElement('
|
|
750
|
+
return React.createElement('svg', {
|
|
751
|
+
className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''),
|
|
752
|
+
viewBox: '0 0 14 14',
|
|
753
|
+
width: 14,
|
|
754
|
+
height: 14,
|
|
755
|
+
style: {
|
|
756
|
+
marginLeft: 'auto',
|
|
757
|
+
color: 'var(--dsw-alias-label-tertiary)',
|
|
758
|
+
transform: p.open ? 'rotate(180deg)' : 'none',
|
|
759
|
+
transition: 'transform .16s ease',
|
|
760
|
+
display: 'inline-block',
|
|
761
|
+
flexShrink: 0
|
|
762
|
+
}
|
|
763
|
+
}, React.createElement('path', {
|
|
764
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
765
|
+
fill: 'none',
|
|
766
|
+
stroke: 'currentColor',
|
|
767
|
+
strokeWidth: '1.5',
|
|
768
|
+
strokeLinecap: 'round',
|
|
769
|
+
strokeLinejoin: 'round'
|
|
770
|
+
}));
|
|
595
771
|
};
|
|
596
772
|
|
|
597
773
|
async function onSave() {
|
|
@@ -645,6 +821,39 @@ window.__ModuleLoader__.load({
|
|
|
645
821
|
React.createElement(Chevron, { open: expanded })
|
|
646
822
|
),
|
|
647
823
|
expanded ? React.createElement('div', { className: 'cl-body', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 12 } },
|
|
824
|
+
React.createElement('div', {
|
|
825
|
+
style: {
|
|
826
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
827
|
+
padding: '6px 10px', marginBottom: 10, borderRadius: 8,
|
|
828
|
+
background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
832
|
+
React.createElement('span', { style: { fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } },
|
|
833
|
+
'v' + (updateState.currentVersion || '0.1.18')
|
|
834
|
+
),
|
|
835
|
+
updateState.checking
|
|
836
|
+
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } }, tt('update.checking'))
|
|
837
|
+
: updateState.updateAvailable
|
|
838
|
+
? React.createElement('span', { className: 'cl-badge cl-badge-warn' },
|
|
839
|
+
tt('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
840
|
+
)
|
|
841
|
+
: React.createElement('span', { className: 'cl-badge cl-badge-ok' },
|
|
842
|
+
'✓ ' + tt('update.up_to_date')
|
|
843
|
+
)
|
|
844
|
+
),
|
|
845
|
+
updateState.updateAvailable
|
|
846
|
+
? React.createElement('button', {
|
|
847
|
+
type: 'button',
|
|
848
|
+
className: 'cl-btn cl-btn-primary',
|
|
849
|
+
disabled: updateState.updating,
|
|
850
|
+
onClick: handleTriggerUpdate,
|
|
851
|
+
style: { padding: '3px 8px', fontSize: 11 }
|
|
852
|
+
}, updateState.updating ? tt('update.updating') : tt('update.btn'))
|
|
853
|
+
: null
|
|
854
|
+
),
|
|
855
|
+
updateState.notice ? React.createElement('div', { className: 'cl-banner-ok', style: { marginBottom: 10, padding: '8px 12px', borderRadius: 8, background: 'color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 8%, transparent)', border: '1px solid var(--dsw-alias-state-success-primary, #10b981)', color: 'var(--dsw-alias-state-success-primary, #10b981)', fontSize: 12 } }, '✓ ' + updateState.notice) : null,
|
|
856
|
+
updateState.error ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 10 } }, updateState.error) : null,
|
|
648
857
|
budgetWarn ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 12 } }, '⚠ ' + tt('lowBudget')) : null,
|
|
649
858
|
status === 'loading' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading…') :
|
|
650
859
|
status === 'unavailable' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Settings unavailable — plugin not registered on host yet') :
|
|
@@ -696,6 +905,14 @@ window.__ModuleLoader__.load({
|
|
|
696
905
|
onChange: (e) => setDraft((d) => ({ ...d, budgetLimit: parseInt(e.target.value, 10) || 100000 }))
|
|
697
906
|
})
|
|
698
907
|
),
|
|
908
|
+
React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
|
|
909
|
+
React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('budgetAlertPercent')),
|
|
910
|
+
React.createElement('input', {
|
|
911
|
+
className: 'cl-input', type: 'number', min: 50, max: 99, step: 1,
|
|
912
|
+
value: draft.budgetAlertPercent,
|
|
913
|
+
onChange: (e) => setDraft((d) => ({ ...d, budgetAlertPercent: parseInt(e.target.value, 10) || 90 }))
|
|
914
|
+
})
|
|
915
|
+
),
|
|
699
916
|
React.createElement('div', { className: 'cl-field', style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 0' } },
|
|
700
917
|
React.createElement('label', { style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } }, tt('threshold')),
|
|
701
918
|
React.createElement('input', {
|
|
@@ -741,9 +958,28 @@ window.__ModuleLoader__.load({
|
|
|
741
958
|
);
|
|
742
959
|
}
|
|
743
960
|
|
|
744
|
-
module.exports.inject = ['slots', 'locale'];
|
|
961
|
+
module.exports.inject = ['slots', 'locale', 'settingsScope'];
|
|
745
962
|
module.exports.apply = function apply(ctx) {
|
|
746
|
-
|
|
963
|
+
let hasEffect = false;
|
|
964
|
+
try {
|
|
965
|
+
hasEffect = typeof ctx.effect === 'function';
|
|
966
|
+
} catch (_) {
|
|
967
|
+
hasEffect = false;
|
|
968
|
+
}
|
|
969
|
+
if (hasEffect) {
|
|
970
|
+
ctx.effect(() => {
|
|
971
|
+
try {
|
|
972
|
+
const dispose = ctx.locale?.register ? ctx.locale.register(NS, { en, zh }) : undefined;
|
|
973
|
+
return () => {
|
|
974
|
+
if (typeof dispose === 'function') dispose();
|
|
975
|
+
};
|
|
976
|
+
} catch (e) {
|
|
977
|
+
console.warn('[dsh-context-lens] locale register failed', e && e.message || e);
|
|
978
|
+
}
|
|
979
|
+
}, 'dsh-context-lens: locale');
|
|
980
|
+
} else {
|
|
981
|
+
try { ctx.locale.register(NS, { en, zh }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
|
|
982
|
+
}
|
|
747
983
|
if (!ctx.slots) return;
|
|
748
984
|
let registered = false;
|
|
749
985
|
const doRegister = () => {
|
|
@@ -819,14 +1055,14 @@ window.__ModuleLoader__.load({
|
|
|
819
1055
|
try { registerPaneTab(); } catch (e2) {}
|
|
820
1056
|
}
|
|
821
1057
|
} else {
|
|
822
|
-
try { registerPaneTab(); } catch (
|
|
1058
|
+
try { registerPaneTab(); } catch (err) { void err; }
|
|
823
1059
|
}
|
|
824
1060
|
}
|
|
825
1061
|
} catch (e) {
|
|
826
1062
|
console.warn("[dsh-context-lens] native sidebar registration failed", e && e.message || e);
|
|
827
1063
|
}
|
|
828
1064
|
});
|
|
829
|
-
} catch (
|
|
1065
|
+
} catch (err) { void err; }
|
|
830
1066
|
}
|
|
831
1067
|
|
|
832
1068
|
if (typeof ctx.inject === 'function') {
|
|
@@ -851,7 +1087,7 @@ window.__ModuleLoader__.load({
|
|
|
851
1087
|
console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
|
|
852
1088
|
}
|
|
853
1089
|
});
|
|
854
|
-
} catch (
|
|
1090
|
+
} catch (err) { void err; }
|
|
855
1091
|
}
|
|
856
1092
|
|
|
857
1093
|
if (ctx.slots) {
|
|
@@ -875,7 +1111,7 @@ window.__ModuleLoader__.load({
|
|
|
875
1111
|
try { headerChipRegister(); } catch (e2) {}
|
|
876
1112
|
}
|
|
877
1113
|
} else {
|
|
878
|
-
try { headerChipRegister(); } catch (
|
|
1114
|
+
try { headerChipRegister(); } catch (err) { void err; }
|
|
879
1115
|
}
|
|
880
1116
|
}
|
|
881
1117
|
};
|
package/lib/http.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export function writeJson(res, code, body) {
|
|
2
|
+
try {
|
|
3
|
+
res.writeHead(code, {
|
|
4
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
5
|
+
'Cache-Control': 'no-store',
|
|
6
|
+
});
|
|
7
|
+
res.end(JSON.stringify(body));
|
|
8
|
+
} catch {
|
|
9
|
+
/* socket already closed */
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function readBody(req, maxBytes = 256 * 1024) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
req.on('data', (c) => {
|
|
18
|
+
size += c.length;
|
|
19
|
+
if (size > maxBytes) {
|
|
20
|
+
req.destroy();
|
|
21
|
+
reject(new Error('body too large'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(c);
|
|
25
|
+
});
|
|
26
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
27
|
+
req.on('error', reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function header(request, name) {
|
|
32
|
+
const value = request?.headers?.[name.toLowerCase()];
|
|
33
|
+
return Array.isArray(value) ? value[0] : value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isLoopbackAddress(value) {
|
|
37
|
+
const address = String(value || '').toLowerCase().replace(/^\[|\]$/g, '');
|
|
38
|
+
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
39
|
+
|| address.startsWith('127.')
|
|
40
|
+
|| address.startsWith('::ffff:127.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reject cross-site writes while allowing local, LAN, and reverse-proxy UIs. */
|
|
44
|
+
export function isTrustedRequest(request) {
|
|
45
|
+
const secFetchSite = header(request, 'sec-fetch-site');
|
|
46
|
+
if (secFetchSite === 'cross-site') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const host = header(request, 'x-forwarded-host') || header(request, 'host');
|
|
51
|
+
const origin = header(request, 'origin');
|
|
52
|
+
if (origin) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(origin);
|
|
55
|
+
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const referer = header(request, 'referer');
|
|
68
|
+
if (referer) {
|
|
69
|
+
try {
|
|
70
|
+
const url = new URL(referer);
|
|
71
|
+
if (host && url.host.toLowerCase() === host.toLowerCase()) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (isLoopbackAddress(url.hostname) && isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Requests without origin/referer (e.g. curl, loopback tests, or internal calls)
|
|
84
|
+
if (isLoopbackAddress(request?.socket?.remoteAddress)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (secFetchSite === 'same-origin' || secFetchSite === 'same-site') {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const UPDATE_HEADER = 'x-dsh-plugin-update';
|
|
96
|
+
export const ALT_UPDATE_HEADER = 'x-dsh-context-lens-update';
|
|
97
|
+
|
|
98
|
+
export function isTrustedUpdateRequest(request) {
|
|
99
|
+
const updHdr = header(request, UPDATE_HEADER) || header(request, ALT_UPDATE_HEADER);
|
|
100
|
+
if (updHdr !== '1') return false;
|
|
101
|
+
if (!isLoopbackAddress(request?.socket?.remoteAddress)) return false;
|
|
102
|
+
const site = header(request, 'sec-fetch-site');
|
|
103
|
+
if (site !== undefined && site !== 'same-origin') return false;
|
|
104
|
+
const origin = header(request, 'origin');
|
|
105
|
+
const host = header(request, 'host');
|
|
106
|
+
if (origin === undefined || host === undefined) return false;
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(origin);
|
|
109
|
+
return (url.protocol === 'http:' || url.protocol === 'https:')
|
|
110
|
+
&& isLoopbackAddress(url.hostname) && url.host === host;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|