@goodandready/dsh-context-lens 0.1.17 → 0.1.19

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/lib/client.js CHANGED
@@ -6,6 +6,13 @@ window.__ModuleLoader__.load({
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',
@@ -37,6 +44,13 @@ window.__ModuleLoader__.load({
37
44
  };
38
45
 
39
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': '已是最新版本',
40
54
  title: 'Context Lens 上下文镜头与 Token 守卫',
41
55
  sub: 'AST 代码精简、日志过滤与 Token 预算监控',
42
56
  mode: '压缩模式',
@@ -108,12 +122,12 @@ window.__ModuleLoader__.load({
108
122
  .cl-badge-ok {
109
123
  border-color: var(--dsw-alias-state-success-primary, #10b981);
110
124
  color: var(--dsw-alias-state-success-primary, #10b981);
111
- background: rgba(16, 185, 129, 0.08);
125
+ background: color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 8%, transparent);
112
126
  }
113
127
  .cl-badge-warn {
114
128
  border-color: var(--dsw-alias-state-error-primary, #ef4444);
115
129
  color: var(--dsw-alias-state-error-primary, #ef4444);
116
- background: rgba(239, 68, 68, 0.08);
130
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent);
117
131
  }
118
132
  .cl-stat-grid {
119
133
  display: grid;
@@ -211,7 +225,7 @@ window.__ModuleLoader__.load({
211
225
  .cl-banner-warn {
212
226
  padding: 10px 14px;
213
227
  border-radius: 8px;
214
- background: rgba(239, 68, 68, 0.1);
228
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 10%, transparent);
215
229
  border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
216
230
  color: var(--dsw-alias-state-error-primary, #ef4444);
217
231
  font-size: 12px;
@@ -244,7 +258,7 @@ window.__ModuleLoader__.load({
244
258
  .cl-error-box {
245
259
  padding: 12px;
246
260
  border-radius: 8px;
247
- background: rgba(239, 68, 68, 0.08);
261
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent);
248
262
  border: 1px solid var(--dsw-alias-state-error-primary, #ef4444);
249
263
  color: var(--dsw-alias-state-error-primary, #ef4444);
250
264
  font-size: 12px;
@@ -253,6 +267,18 @@ window.__ModuleLoader__.load({
253
267
  document.head.appendChild(style);
254
268
  }
255
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
+
256
282
  function createErrorBoundary() {
257
283
  if (!React || typeof React.Component !== 'function') {
258
284
  return function NoopBoundary(props) { return (props && props.children) || null; };
@@ -293,7 +319,9 @@ window.__ModuleLoader__.load({
293
319
  const snap = ctx.locale.getSnapshot();
294
320
  if (snap && snap.active && snap.active.startsWith('zh')) return zh;
295
321
  }
296
- } catch {}
322
+ } catch (err) {
323
+ /* intentional fallback: default to english on snapshot error */
324
+ }
297
325
  return en;
298
326
  }
299
327
 
@@ -534,7 +562,7 @@ window.__ModuleLoader__.load({
534
562
  style: {
535
563
  position: 'absolute', top: 'calc(100% + 6px)', right: 0, width: 280,
536
564
  background: 'var(--dsw-alias-bg-layer-3)', border: '1px solid var(--dsw-alias-border-l2)',
537
- borderRadius: 10, boxShadow: '0 8px 24px rgba(0, 0, 0, 0.28)', padding: '12px 14px',
565
+ borderRadius: 10, boxShadow: 'var(--dsw-alias-modal-shadow, var(--dsw-alias-shadow-large, 0 4px 16px var(--dsw-alias-border-l1)))', padding: '12px 14px',
538
566
  zIndex: 1000, fontSize: 12, color: 'var(--dsw-alias-label-primary)', cursor: 'default', textAlign: 'left'
539
567
  },
540
568
  onClick: (e) => e.stopPropagation()
@@ -587,7 +615,7 @@ window.__ModuleLoader__.load({
587
615
  const scopeRef = React.useRef(null);
588
616
  if (!scopeRef.current && _ctx) {
589
617
  try {
590
- const s = (_ctx.get && _ctx.get('lanSettings')) || _ctx.settingsScope;
618
+ const s = _ctx.settingsScope || (_ctx.get && _ctx.get('settingsScope'));
591
619
  if (s && typeof s.bind === 'function') {
592
620
  scopeRef.current = s.bind({ namespace: NS });
593
621
  }
@@ -627,7 +655,9 @@ window.__ModuleLoader__.load({
627
655
  unsub = scope.subscribe(() => {
628
656
  if (!cancelled) loadSnap();
629
657
  });
630
- } catch (_) {}
658
+ } catch (_err) {
659
+ console.warn('[dsh-context-lens] scope subscribe failed', _err && _err.message || _err);
660
+ }
631
661
  }
632
662
 
633
663
  return () => {
@@ -636,6 +666,69 @@ window.__ModuleLoader__.load({
636
666
  };
637
667
  }, [scope]);
638
668
 
669
+ const [updateState, setUpdateState] = React.useState({
670
+ checking: false,
671
+ updating: false,
672
+ currentVersion: '0.1.18',
673
+ latestVersion: '',
674
+ updateAvailable: false,
675
+ canAutoUpdate: true,
676
+ notice: '',
677
+ error: ''
678
+ });
679
+
680
+ const checkUpdate = React.useCallback(async () => {
681
+ setUpdateState((s) => ({ ...s, checking: true, error: '' }));
682
+ try {
683
+ const res = await fetch('/api/dsh-context-lens/update');
684
+ if (!res.ok) throw new Error('HTTP ' + res.status);
685
+ const data = await res.json().catch(() => ({}));
686
+ setUpdateState((s) => ({
687
+ ...s,
688
+ checking: false,
689
+ currentVersion: data.currentVersion || s.currentVersion,
690
+ latestVersion: data.latestVersion || '',
691
+ updateAvailable: !!data.updateAvailable,
692
+ canAutoUpdate: data.canAutoUpdate !== false,
693
+ }));
694
+ } catch (_) {
695
+ setUpdateState((s) => ({ ...s, checking: false }));
696
+ }
697
+ }, []);
698
+
699
+ React.useEffect(() => {
700
+ if (expanded) checkUpdate();
701
+ }, [expanded, checkUpdate]);
702
+
703
+ async function handleTriggerUpdate() {
704
+ if (updateState.updating) return;
705
+ setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }));
706
+ try {
707
+ const res = await fetch('/api/dsh-context-lens/update', {
708
+ method: 'POST',
709
+ headers: { 'x-dsh-plugin-update': '1', 'x-dsh-context-lens-update': '1' }
710
+ });
711
+ const data = await res.json().catch(() => ({}));
712
+ if (!res.ok || data.ok === false || data.error) {
713
+ throw new Error(data.error || ('HTTP ' + res.status));
714
+ }
715
+ const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion;
716
+ setUpdateState((s) => ({
717
+ ...s,
718
+ updating: false,
719
+ updateAvailable: false,
720
+ currentVersion: newVer,
721
+ notice: tt('update.done', { version: newVer }),
722
+ }));
723
+ } catch (e) {
724
+ setUpdateState((s) => ({
725
+ ...s,
726
+ updating: false,
727
+ error: tt('update.failed', { error: e && e.message || String(e) }),
728
+ }));
729
+ }
730
+ }
731
+
639
732
  React.useEffect(() => {
640
733
  if (!expanded) return;
641
734
  fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
@@ -646,7 +739,8 @@ window.__ModuleLoader__.load({
646
739
  }).catch(() => {});
647
740
  }, [expanded, draft.autoCollapse]);
648
741
 
649
- const tt = t || ((k) => (en[k] || k));
742
+ const L = React.useMemo(() => getActiveLocale(_ctx), [_ctx]);
743
+ const tt = React.useMemo(() => (t ? t : makeT(L, en)), [t, L]);
650
744
 
651
745
  let ChevronIcon = null;
652
746
  try {
@@ -655,9 +749,29 @@ window.__ModuleLoader__.load({
655
749
  } catch (e) { ChevronIcon = null; }
656
750
 
657
751
  const Chevron = ChevronIcon ? function ChevronNode(p) {
658
- 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' } });
752
+ 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' } });
659
753
  } : function Fallback(p) {
660
- return React.createElement('span', { className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''), style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-tertiary)' } }, '▼');
754
+ return React.createElement('svg', {
755
+ className: 'cl-chev' + (p.open ? ' cl-chev-open' : ''),
756
+ viewBox: '0 0 14 14',
757
+ width: 14,
758
+ height: 14,
759
+ style: {
760
+ marginLeft: 'auto',
761
+ color: 'var(--dsw-alias-label-tertiary)',
762
+ transform: p.open ? 'rotate(180deg)' : 'none',
763
+ transition: 'transform .16s ease',
764
+ display: 'inline-block',
765
+ flexShrink: 0
766
+ }
767
+ }, React.createElement('path', {
768
+ d: 'M3.5 5.25L7 8.75L10.5 5.25',
769
+ fill: 'none',
770
+ stroke: 'currentColor',
771
+ strokeWidth: '1.5',
772
+ strokeLinecap: 'round',
773
+ strokeLinejoin: 'round'
774
+ }));
661
775
  };
662
776
 
663
777
  async function onSave() {
@@ -711,6 +825,39 @@ window.__ModuleLoader__.load({
711
825
  React.createElement(Chevron, { open: expanded })
712
826
  ),
713
827
  expanded ? React.createElement('div', { className: 'cl-body', style: { borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 12 } },
828
+ React.createElement('div', {
829
+ style: {
830
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
831
+ padding: '6px 10px', marginBottom: 10, borderRadius: 8,
832
+ background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l2)', fontSize: 12
833
+ }
834
+ },
835
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
836
+ React.createElement('span', { style: { fontWeight: 500, color: 'var(--dsw-alias-label-primary)' } },
837
+ 'v' + (updateState.currentVersion || '0.1.18')
838
+ ),
839
+ updateState.checking
840
+ ? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 11 } }, tt('update.checking'))
841
+ : updateState.updateAvailable
842
+ ? React.createElement('span', { className: 'cl-badge cl-badge-warn' },
843
+ tt('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
844
+ )
845
+ : React.createElement('span', { className: 'cl-badge cl-badge-ok' },
846
+ '✓ ' + tt('update.up_to_date')
847
+ )
848
+ ),
849
+ updateState.updateAvailable
850
+ ? React.createElement('button', {
851
+ type: 'button',
852
+ className: 'cl-btn cl-btn-primary',
853
+ disabled: updateState.updating,
854
+ onClick: handleTriggerUpdate,
855
+ style: { padding: '3px 8px', fontSize: 11 }
856
+ }, updateState.updating ? tt('update.updating') : tt('update.btn'))
857
+ : null
858
+ ),
859
+ 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,
860
+ updateState.error ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 10 } }, updateState.error) : null,
714
861
  budgetWarn ? React.createElement('div', { className: 'cl-banner-warn', style: { marginBottom: 12 } }, '⚠ ' + tt('lowBudget')) : null,
715
862
  status === 'loading' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Loading…') :
716
863
  status === 'unavailable' ? React.createElement('div', { style: { padding: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Settings unavailable — plugin not registered on host yet') :
@@ -815,9 +962,28 @@ window.__ModuleLoader__.load({
815
962
  );
816
963
  }
817
964
 
818
- module.exports.inject = ['slots', 'locale'];
965
+ module.exports.inject = ['slots', 'locale', 'settingsScope'];
819
966
  module.exports.apply = function apply(ctx) {
820
- try { ctx.locale.register(NS, { en, zh }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
967
+ let hasEffect = false;
968
+ try {
969
+ hasEffect = typeof ctx.effect === 'function';
970
+ } catch (_) {
971
+ hasEffect = false;
972
+ }
973
+ if (hasEffect) {
974
+ ctx.effect(() => {
975
+ try {
976
+ const dispose = ctx.locale?.register ? ctx.locale.register(NS, { en, zh }) : undefined;
977
+ return () => {
978
+ if (typeof dispose === 'function') dispose();
979
+ };
980
+ } catch (e) {
981
+ console.warn('[dsh-context-lens] locale register failed', e && e.message || e);
982
+ }
983
+ }, 'dsh-context-lens: locale');
984
+ } else {
985
+ try { ctx.locale.register(NS, { en, zh }); } catch (e) { console.warn('[dsh-context-lens] locale register failed', e && e.message || e); }
986
+ }
821
987
  if (!ctx.slots) return;
822
988
  let registered = false;
823
989
  const doRegister = () => {
@@ -890,17 +1056,28 @@ window.__ModuleLoader__.load({
890
1056
  const ok = ctx.slots.inject("sidebar.right.pane.tab", registerPaneTab);
891
1057
  if (!ok) console.warn("[dsh-context-lens] sidebar.right.pane.tab inject returned false");
892
1058
  } catch (e) {
893
- try { registerPaneTab(); } catch (e2) {}
1059
+ console.warn("[dsh-context-lens] sidebar.right.pane.tab inject failed, falling back to direct register", e && e.message || e);
1060
+ try {
1061
+ registerPaneTab();
1062
+ } catch (e2) {
1063
+ console.warn("[dsh-context-lens] registerPaneTab direct register fallback failed", e2 && e2.message || e2);
1064
+ }
894
1065
  }
895
1066
  } else {
896
- try { registerPaneTab(); } catch (e) {}
1067
+ try {
1068
+ registerPaneTab();
1069
+ } catch (err) {
1070
+ console.warn("[dsh-context-lens] registerPaneTab direct register failed", err && err.message || err);
1071
+ }
897
1072
  }
898
1073
  }
899
1074
  } catch (e) {
900
1075
  console.warn("[dsh-context-lens] native sidebar registration failed", e && e.message || e);
901
1076
  }
902
1077
  });
903
- } catch (e) {}
1078
+ } catch (err) {
1079
+ console.warn("[dsh-context-lens] native sidebar inject failed", err && err.message || err);
1080
+ }
904
1081
  }
905
1082
 
906
1083
  if (typeof ctx.inject === 'function') {
@@ -925,7 +1102,9 @@ window.__ModuleLoader__.load({
925
1102
  console.warn('[dsh-context-lens] betterSidebar registerTab failed', e && e.message || e);
926
1103
  }
927
1104
  });
928
- } catch (e) {}
1105
+ } catch (err) {
1106
+ console.warn('[dsh-context-lens] betterSidebar inject failed', err && err.message || err);
1107
+ }
929
1108
  }
930
1109
 
931
1110
  if (ctx.slots) {
@@ -945,11 +1124,19 @@ window.__ModuleLoader__.load({
945
1124
  try {
946
1125
  ctx.slots.inject('conversation.session.header.utilities', headerChipRegister);
947
1126
  } catch (e) {
948
- console.warn('[dsh-context-lens] utilities inject failed', e && e.message || e);
949
- try { headerChipRegister(); } catch (e2) {}
1127
+ console.warn('[dsh-context-lens] utilities inject failed, falling back to direct register', e && e.message || e);
1128
+ try {
1129
+ headerChipRegister();
1130
+ } catch (e2) {
1131
+ console.warn('[dsh-context-lens] headerChipRegister fallback failed', e2 && e2.message || e2);
1132
+ }
950
1133
  }
951
1134
  } else {
952
- try { headerChipRegister(); } catch (e) {}
1135
+ try {
1136
+ headerChipRegister();
1137
+ } catch (err) {
1138
+ console.warn('[dsh-context-lens] headerChipRegister direct register failed', err && err.message || err);
1139
+ }
953
1140
  }
954
1141
  }
955
1142
  };
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
+ }