dsh-vision-router 2.1.2 → 2.1.4

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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/docs/architecture/compat-inventory.md +6 -6
  4. package/docs/architecture/dsh-compatibility-matrix.md +7 -6
  5. package/docs/architecture/dsh-support-window.md +36 -16
  6. package/docs/architecture/p3-compat-retirement.md +7 -5
  7. package/docs/architecture/p3-host-native-seams.md +3 -1
  8. package/docs/doctor.md +4 -1
  9. package/docs/releases/v2.1.3.md +34 -0
  10. package/docs/releases/v2.1.4.md +34 -0
  11. package/docs/v2-capability-routing.md +20 -11
  12. package/index.js +156 -129
  13. package/lib/catalog-corrections.js +2 -0
  14. package/lib/client-presentation-boundary-main.js +478 -2
  15. package/lib/client-presentation-boundary.js +121 -1
  16. package/lib/client.js +4 -4
  17. package/lib/depth-guidance.js +4 -4
  18. package/lib/doctor-cli-p0.js +3 -1
  19. package/lib/doctor-cli.js +4 -1
  20. package/lib/doctor-runtime.js +8 -3
  21. package/lib/dsh-support-window.js +15 -7
  22. package/lib/live-model-discovery.js +20 -13
  23. package/lib/local-vision-stabilizer.js +1 -1
  24. package/lib/mixed-router.js +8 -8
  25. package/lib/pi-ai-bridge-wire-compat.js +69 -10
  26. package/lib/runtime-i18n-boundary.js +27 -2
  27. package/lib/runtime-i18n.js +6 -6
  28. package/lib/session-affinity-runtime.js +104 -0
  29. package/lib/session-affinity.js +93 -0
  30. package/lib/settings-ia-client-prelude.js +1 -1
  31. package/lib/structured-bootstrap.js +2 -2
  32. package/lib/structured-flow-hardening.js +159 -158
  33. package/lib/tesseract-exec-compat.js +9 -36
  34. package/lib/vision-backend-runtime-policy.js +1 -0
  35. package/lib/vision-background-benchmark.js +79 -52
  36. package/lib/vision-background-failure-policy.js +1 -24
  37. package/lib/vision-background-stop-store.js +10 -13
  38. package/lib/vision-capability-benchmark-service.js +14 -2
  39. package/lib/vision-tool-runtime-boundary.js +20 -3
  40. package/lib/windows-desktop-capture.js +247 -0
  41. package/package.json +5 -5
  42. package/lib/windows-screenshot-dpi-compat.js +0 -148
@@ -488,6 +488,465 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
488
488
  }
489
489
  }
490
490
 
491
+ // #138 Windows clipboard compatibility: desktop clipboards may expose an
492
+ // image as BMP/empty MIME, or may declare a supported MIME that disagrees with
493
+ // the actual bytes (for example .png + image/png carrying JPEG bytes). DSH
494
+ // validates the declaration before/while creating the draft, and its local
495
+ // attachment store rejects declaration/content mismatches. Inspect image-like
496
+ // clipboard files by magic bytes before Lexical's CRITICAL paste handler sees
497
+ // them; canonical files keep the original File object and are never re-encoded.
498
+ function installClipboardImagePasteCompat(ctx) {
499
+ if (!ctx || typeof ctx.effect !== 'function') return;
500
+ ctx.effect(function() {
501
+ var doc = window.document;
502
+ if (!doc || typeof doc.addEventListener !== 'function') return function(){};
503
+ if (
504
+ typeof window.DataTransfer !== 'function' ||
505
+ typeof window.ClipboardEvent !== 'function' ||
506
+ typeof window.File !== 'function' ||
507
+ typeof WeakSet !== 'function'
508
+ ) return function(){};
509
+
510
+ var replayed = new WeakSet();
511
+ var supported = {
512
+ 'image/png': true,
513
+ 'image/jpeg': true,
514
+ 'image/webp': true,
515
+ 'image/gif': true
516
+ };
517
+ var bitmapTypes = {
518
+ 'image/bmp': true,
519
+ 'image/x-bmp': true,
520
+ 'image/x-ms-bmp': true
521
+ };
522
+
523
+ function mediaType(value) {
524
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
525
+ }
526
+
527
+ function imageLikeName(value) {
528
+ return typeof value === 'string' && /\.(?:png|jpe?g|webp|gif|bmp|dib)$/i.test(value.trim());
529
+ }
530
+
531
+ function needsInspection(file) {
532
+ if (!file) return false;
533
+ var type = mediaType(file.type);
534
+ return type === '' || type.indexOf('image/') === 0 || imageLikeName(file.name);
535
+ }
536
+
537
+ var normalizedBitmapFiles = new WeakSet();
538
+ var maxExactDedupeBytes = 8 * 1024 * 1024;
539
+ var maxVisualDedupeSourceBytes = 16 * 1024 * 1024;
540
+ var maxVisualDedupeDimension = 4096;
541
+ var maxVisualDedupePixels = 9000000;
542
+ var visualDedupeTile = 256;
543
+
544
+ function supportedImage(file) {
545
+ return !!file && supported[mediaType(file.type)] === true;
546
+ }
547
+
548
+ function syntheticClipboardName(name) {
549
+ var value = typeof name === 'string' ? name.trim() : '';
550
+ if (value === '') return true;
551
+ return /^(?:image|clipboard)(?:[ _-]?\d+)?\.(?:png|jpe?g|webp|gif)$/i.test(value);
552
+ }
553
+
554
+ function syntheticClipboardFile(file) {
555
+ return !!file && (normalizedBitmapFiles.has(file) || syntheticClipboardName(file.name));
556
+ }
557
+
558
+ function hasDedupeSignal(files) {
559
+ var images = files.filter(supportedImage);
560
+ for (var i = 0; i < images.length; i += 1) {
561
+ for (var j = i + 1; j < images.length; j += 1) {
562
+ var left = images[i];
563
+ var right = images[j];
564
+ if (
565
+ (Number.isFinite(left.size) && left.size > 0 && left.size === right.size) ||
566
+ (mediaType(left.type) === mediaType(right.type) &&
567
+ (syntheticClipboardFile(left) || syntheticClipboardFile(right)))
568
+ ) return true;
569
+ }
570
+ }
571
+ return false;
572
+ }
573
+
574
+ function bytesOf(file) {
575
+ if (!file || typeof file.arrayBuffer !== 'function') return Promise.resolve(undefined);
576
+ try {
577
+ return Promise.resolve(file.arrayBuffer()).then(function(buffer) {
578
+ return new Uint8Array(buffer);
579
+ }, function(){ return undefined; });
580
+ } catch (_) {
581
+ return Promise.resolve(undefined);
582
+ }
583
+ }
584
+
585
+ function sameBytes(left, right) {
586
+ if (
587
+ !left || !right ||
588
+ !Number.isFinite(left.size) || left.size <= 0 || left.size !== right.size ||
589
+ left.size > maxExactDedupeBytes
590
+ ) return Promise.resolve(false);
591
+ return Promise.all([bytesOf(left), bytesOf(right)]).then(function(values) {
592
+ var a = values[0];
593
+ var b = values[1];
594
+ if (!a || !b || a.length !== b.length) return false;
595
+ for (var i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
596
+ return true;
597
+ }, function(){ return false; });
598
+ }
599
+
600
+ function visualPairAllowed(left, right) {
601
+ if (!left || !right) return false;
602
+ if (mediaType(left.type) !== 'image/png' || mediaType(right.type) !== 'image/png') return false;
603
+ if (!(syntheticClipboardFile(left) || syntheticClipboardFile(right))) return false;
604
+ return Number.isFinite(left.size) && left.size > 0 && left.size <= maxVisualDedupeSourceBytes &&
605
+ Number.isFinite(right.size) && right.size > 0 && right.size <= maxVisualDedupeSourceBytes;
606
+ }
607
+
608
+ function closeBitmap(bitmap) {
609
+ try { if (bitmap && typeof bitmap.close === 'function') bitmap.close(); } catch (_) {}
610
+ }
611
+
612
+ function visualInfoAllowed(leftInfo, rightInfo) {
613
+ if (!leftInfo || !rightInfo || leftInfo.type !== 'image/png' || rightInfo.type !== 'image/png') return false;
614
+ var width = leftInfo.width;
615
+ var height = leftInfo.height;
616
+ return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 &&
617
+ width === rightInfo.width && height === rightInfo.height &&
618
+ width <= maxVisualDedupeDimension && height <= maxVisualDedupeDimension &&
619
+ width * height <= maxVisualDedupePixels;
620
+ }
621
+
622
+ function bitmapPixelsMatch(left, right) {
623
+ if (!visualPairAllowed(left, right) || typeof window.createImageBitmap !== 'function') return Promise.resolve(false);
624
+ return Promise.all([headType(left), headType(right)]).then(function(infos) {
625
+ if (!visualInfoAllowed(infos[0], infos[1])) return false;
626
+ return Promise.resolve(window.createImageBitmap(left)).then(function(leftBitmap) {
627
+ return Promise.resolve(window.createImageBitmap(right)).then(function(rightBitmap) {
628
+ try {
629
+ var width = leftBitmap && leftBitmap.width;
630
+ var height = leftBitmap && leftBitmap.height;
631
+ if (
632
+ !Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 ||
633
+ width !== rightBitmap.width || height !== rightBitmap.height ||
634
+ width > maxVisualDedupeDimension || height > maxVisualDedupeDimension ||
635
+ width * height > maxVisualDedupePixels
636
+ ) return false;
637
+ var canvas = doc.createElement('canvas');
638
+ var context = canvas.getContext && canvas.getContext('2d', { willReadFrequently: true });
639
+ if (!context || typeof context.drawImage !== 'function' || typeof context.getImageData !== 'function') return false;
640
+ for (var y = 0; y < height; y += visualDedupeTile) {
641
+ for (var x = 0; x < width; x += visualDedupeTile) {
642
+ var tileWidth = Math.min(visualDedupeTile, width - x);
643
+ var tileHeight = Math.min(visualDedupeTile, height - y);
644
+ canvas.width = tileWidth;
645
+ canvas.height = tileHeight;
646
+ context.drawImage(leftBitmap, x, y, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight);
647
+ var leftPixels = context.getImageData(0, 0, tileWidth, tileHeight).data;
648
+ context.drawImage(rightBitmap, x, y, tileWidth, tileHeight, 0, 0, tileWidth, tileHeight);
649
+ var rightPixels = context.getImageData(0, 0, tileWidth, tileHeight).data;
650
+ if (!leftPixels || !rightPixels || leftPixels.length !== rightPixels.length) return false;
651
+ for (var i = 0; i < leftPixels.length; i += 1) {
652
+ if (leftPixels[i] !== rightPixels[i]) return false;
653
+ }
654
+ }
655
+ }
656
+ return true;
657
+ } catch (_) {
658
+ return false;
659
+ } finally {
660
+ closeBitmap(leftBitmap);
661
+ closeBitmap(rightBitmap);
662
+ }
663
+ }, function() {
664
+ closeBitmap(leftBitmap);
665
+ return false;
666
+ });
667
+ }, function(){ return false; });
668
+ }, function(){ return false; });
669
+ }
670
+
671
+ function duplicatePair(left, right) {
672
+ if (!supportedImage(left) || !supportedImage(right)) return Promise.resolve(false);
673
+ return sameBytes(left, right).then(function(exact) {
674
+ return exact ? true : bitmapPixelsMatch(left, right);
675
+ }, function(){ return false; });
676
+ }
677
+
678
+ function preferredDuplicate(left, right) {
679
+ if (syntheticClipboardFile(left) && !syntheticClipboardFile(right)) return right;
680
+ return left;
681
+ }
682
+
683
+ function dedupeBatch(files) {
684
+ if (!hasDedupeSignal(files)) return Promise.resolve(files);
685
+ var kept = [];
686
+ var chain = Promise.resolve();
687
+ files.forEach(function(file) {
688
+ chain = chain.then(function() {
689
+ var index = 0;
690
+ function compareNext() {
691
+ if (index >= kept.length) {
692
+ kept.push(file);
693
+ return Promise.resolve();
694
+ }
695
+ var current = index;
696
+ index += 1;
697
+ return duplicatePair(kept[current], file).then(function(duplicate) {
698
+ if (!duplicate) return compareNext();
699
+ kept[current] = preferredDuplicate(kept[current], file);
700
+ return undefined;
701
+ }, function(){ return compareNext(); });
702
+ }
703
+ return compareNext();
704
+ });
705
+ });
706
+ return chain.then(function(){ return kept; }, function(){ return files; });
707
+ }
708
+
709
+ function composerEditable(target) {
710
+ var element = target && target.nodeType === 1
711
+ ? target
712
+ : target && target.parentElement;
713
+ if (!element || typeof element.closest !== 'function') return null;
714
+ var editable = element.closest('[data-composer-input]');
715
+ if (!editable || typeof editable.closest !== 'function') return null;
716
+ return editable.closest('[data-composer-card]') ? editable : null;
717
+ }
718
+
719
+ function snapshotText(data) {
720
+ var entries = [];
721
+ var types = data && data.types ? Array.from(data.types) : [];
722
+ types.forEach(function(type) {
723
+ if (type === 'Files') return;
724
+ try {
725
+ var value = data.getData(type);
726
+ if (typeof value === 'string' && value !== '') entries.push([type, value]);
727
+ } catch (_) {}
728
+ });
729
+ return entries;
730
+ }
731
+
732
+ var maxBitmapSourceBytes = 64 * 1024 * 1024;
733
+ var maxBitmapDimension = 10000;
734
+ var maxBitmapPixels = 100000000;
735
+
736
+ function sniffImageInfo(bytes) {
737
+ if (!bytes || bytes.length < 2) return undefined;
738
+ if (
739
+ bytes.length >= 8 &&
740
+ bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
741
+ bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a
742
+ ) {
743
+ if (bytes.length < 24 || typeof DataView !== 'function') return { type: 'image/png' };
744
+ try {
745
+ var pngView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
746
+ return {
747
+ type: 'image/png',
748
+ width: pngView.getUint32(16, false),
749
+ height: pngView.getUint32(20, false)
750
+ };
751
+ } catch (_) {
752
+ return { type: 'image/png' };
753
+ }
754
+ }
755
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return { type: 'image/jpeg' };
756
+ if (
757
+ bytes.length >= 6 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 &&
758
+ bytes[3] === 0x38 && (bytes[4] === 0x37 || bytes[4] === 0x39) && bytes[5] === 0x61
759
+ ) return { type: 'image/gif' };
760
+ if (
761
+ bytes.length >= 12 && bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
762
+ bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50
763
+ ) return { type: 'image/webp' };
764
+ if (bytes[0] === 0x42 && bytes[1] === 0x4d) {
765
+ if (bytes.length < 26 || typeof DataView !== 'function') return { type: 'image/bmp' };
766
+ try {
767
+ var view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
768
+ var dibSize = bytes.length >= 18 ? view.getUint32(14, true) : 0;
769
+ if (dibSize < 40) return { type: 'image/bmp' };
770
+ return {
771
+ type: 'image/bmp',
772
+ width: Math.abs(view.getInt32(18, true)),
773
+ height: Math.abs(view.getInt32(22, true))
774
+ };
775
+ } catch (_) {
776
+ return { type: 'image/bmp' };
777
+ }
778
+ }
779
+ return undefined;
780
+ }
781
+
782
+ function headType(file) {
783
+ try {
784
+ var blob = typeof file.slice === 'function' ? file.slice(0, 32) : file;
785
+ if (!blob || typeof blob.arrayBuffer !== 'function') return Promise.resolve(undefined);
786
+ return Promise.resolve(blob.arrayBuffer()).then(function(buffer) {
787
+ return sniffImageInfo(new Uint8Array(buffer));
788
+ }, function(){ return undefined; });
789
+ } catch (_) {
790
+ return Promise.resolve(undefined);
791
+ }
792
+ }
793
+
794
+ function fileNameFor(type, original) {
795
+ var name = typeof original === 'string' ? original : '';
796
+ var suffix = type === 'image/png' ? '.png'
797
+ : type === 'image/jpeg' ? '.jpg'
798
+ : type === 'image/gif' ? '.gif'
799
+ : type === 'image/webp' ? '.webp'
800
+ : '';
801
+ if (!name) return suffix ? 'clipboard' + suffix : 'clipboard-image';
802
+ if (!suffix) return name;
803
+ return /\.(?:png|jpe?g|webp|gif|bmp|dib)$/i.test(name)
804
+ ? name.replace(/\.(?:png|jpe?g|webp|gif|bmp|dib)$/i, suffix)
805
+ : name;
806
+ }
807
+
808
+ function retypeFile(file, type) {
809
+ return new window.File([file], fileNameFor(type, file && file.name), {
810
+ type: type,
811
+ lastModified: file && Number.isFinite(file.lastModified) ? file.lastModified : Date.now()
812
+ });
813
+ }
814
+
815
+ function bitmapDecodeAllowed(file, info) {
816
+ if (!file || !info) return false;
817
+ if (!Number.isFinite(file.size) || file.size <= 0 || file.size > maxBitmapSourceBytes) return false;
818
+ if (!Number.isFinite(info.width) || !Number.isFinite(info.height) || info.width <= 0 || info.height <= 0) return false;
819
+ if (info.width > maxBitmapDimension || info.height > maxBitmapDimension) return false;
820
+ return info.width * info.height <= maxBitmapPixels;
821
+ }
822
+
823
+ function bitmapToPng(file) {
824
+ if (typeof window.createImageBitmap !== 'function') return Promise.reject(new Error('createImageBitmap unavailable'));
825
+ return Promise.resolve(window.createImageBitmap(file)).then(function(bitmap) {
826
+ return new Promise(function(resolve, reject) {
827
+ var close = function() {
828
+ try { if (bitmap && typeof bitmap.close === 'function') bitmap.close(); } catch (_) {}
829
+ };
830
+ try {
831
+ var canvas = doc.createElement('canvas');
832
+ canvas.width = bitmap.width;
833
+ canvas.height = bitmap.height;
834
+ var context = canvas.getContext && canvas.getContext('2d');
835
+ if (!context || typeof context.drawImage !== 'function' || typeof canvas.toBlob !== 'function') {
836
+ close();
837
+ reject(new Error('canvas PNG conversion unavailable'));
838
+ return;
839
+ }
840
+ context.drawImage(bitmap, 0, 0);
841
+ canvas.toBlob(function(blob) {
842
+ close();
843
+ if (!blob) {
844
+ reject(new Error('canvas PNG conversion failed'));
845
+ return;
846
+ }
847
+ try {
848
+ var normalized = new window.File([blob], fileNameFor('image/png', file && file.name), {
849
+ type: 'image/png',
850
+ lastModified: file && Number.isFinite(file.lastModified) ? file.lastModified : Date.now()
851
+ });
852
+ normalizedBitmapFiles.add(normalized);
853
+ resolve(normalized);
854
+ } catch (error) {
855
+ reject(error);
856
+ }
857
+ }, 'image/png');
858
+ } catch (error) {
859
+ close();
860
+ reject(error);
861
+ }
862
+ });
863
+ });
864
+ }
865
+
866
+ function normalizeFile(file) {
867
+ return headType(file).then(function(info) {
868
+ var detected = info && info.type;
869
+ if (detected && supported[detected]) {
870
+ return mediaType(file && file.type) === detected ? file : retypeFile(file, detected);
871
+ }
872
+ if (detected === 'image/bmp' && bitmapDecodeAllowed(file, info)) return bitmapToPng(file);
873
+ return file;
874
+ }, function(){ return file; });
875
+ }
876
+
877
+ function replayEvent(files, textEntries) {
878
+ try {
879
+ var transfer = new window.DataTransfer();
880
+ if (!transfer.items || typeof transfer.items.add !== 'function') return null;
881
+ files.forEach(function(file){ transfer.items.add(file); });
882
+ textEntries.forEach(function(entry){ transfer.setData(entry[0], entry[1]); });
883
+ var event = new window.ClipboardEvent('paste', {
884
+ clipboardData: transfer,
885
+ bubbles: true,
886
+ cancelable: true,
887
+ composed: true
888
+ });
889
+ return event && event.clipboardData === transfer ? event : null;
890
+ } catch (_) {
891
+ return null;
892
+ }
893
+ }
894
+
895
+ function dispatchReplay(target, preferred, fallback) {
896
+ var event = preferred || fallback;
897
+ if (!event || !target || typeof target.dispatchEvent !== 'function') return;
898
+ replayed.add(event);
899
+ try {
900
+ target.dispatchEvent(event);
901
+ } catch (_) {
902
+ if (fallback && fallback !== event) {
903
+ replayed.add(fallback);
904
+ try { target.dispatchEvent(fallback); } catch (_) {}
905
+ }
906
+ }
907
+ }
908
+
909
+ function onPaste(event) {
910
+ if (!event || replayed.has(event)) return;
911
+ var target = composerEditable(event.target);
912
+ if (!target) return;
913
+ var data = event.clipboardData;
914
+ if (!data || !data.items) return;
915
+ var files = Array.from(data.items)
916
+ .filter(function(item){ return item && item.kind === 'file'; })
917
+ .map(function(item){ try { return item.getAsFile(); } catch (_) { return null; } })
918
+ .filter(function(file){ return !!file; });
919
+ if (!files.some(needsInspection) && !hasDedupeSignal(files)) return;
920
+
921
+ // Snapshot every string flavor while the trusted paste event still owns
922
+ // a readable clipboard data store. Build a known-good fallback replay
923
+ // before canceling the original event so conversion failure never eats
924
+ // the user's text or files.
925
+ var textEntries = snapshotText(data);
926
+ var fallback = replayEvent(files, textEntries);
927
+ if (!fallback) return;
928
+
929
+ if (typeof event.preventDefault === 'function') event.preventDefault();
930
+ if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
931
+
932
+ Promise.all(files.map(function(file){
933
+ return needsInspection(file)
934
+ ? normalizeFile(file).catch(function(){ return file; })
935
+ : Promise.resolve(file);
936
+ })).then(function(normalized) {
937
+ return dedupeBatch(normalized).then(function(deduped) {
938
+ dispatchReplay(target, replayEvent(deduped, textEntries), fallback);
939
+ });
940
+ }, function() {
941
+ dispatchReplay(target, fallback, null);
942
+ });
943
+ }
944
+
945
+ doc.addEventListener('paste', onPaste, true);
946
+ return function(){ doc.removeEventListener('paste', onPaste, true); };
947
+ }, 'vision-router: clipboard image paste normalization');
948
+ }
949
+
491
950
  function installVisionModeToggle(ctx, React, primitives) {
492
951
  if (!ctx || typeof ctx.inject !== 'function' || !React) return;
493
952
  var zh = {
@@ -550,7 +1009,19 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
550
1009
 
551
1010
  var settings = bindVisionModeSettings(ctx);
552
1011
  var unavailableSettingsState = { value: undefined };
553
- ctx.inject(['slots', 'modelDirectories'], function(scope) {
1012
+ ctx.inject(['slots', 'modelDirectories', 'sessions', 'remote'], function(scope) {
1013
+ // Cold directoryFor calls use the caller's Cordis context, including its
1014
+ // remote.session declaration. Probe only once modelDirectories is ready;
1015
+ // older Hosts must not acquire a hard dependency on the alpha namespace.
1016
+ var session;
1017
+ try {
1018
+ session = typeof scope.get === 'function' ? scope.get('remote.session') : undefined;
1019
+ } catch (_) {}
1020
+ if (session) scope.inject(['remote.session'], installToggle);
1021
+ else installToggle(scope);
1022
+ });
1023
+
1024
+ function installToggle(scope) {
554
1025
  var models;
555
1026
  try {
556
1027
  models = scope.modelDirectories || (typeof scope.get === 'function' ? scope.get('modelDirectories') : undefined);
@@ -724,7 +1195,7 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
724
1195
  }, VisionModeToggle);
725
1196
  });
726
1197
  }, 'vision-router: composer vision mode toggle');
727
- });
1198
+ }
728
1199
  }
729
1200
 
730
1201
  function decorateVisionRouterPlugin(plugin, React, primitives) {
@@ -736,6 +1207,11 @@ export const CLIENT_PRESENTATION_PRELUDE = String.raw`(function(){
736
1207
  var args = Array.prototype.slice.call(arguments);
737
1208
  args[0] = decoratedCtx;
738
1209
  var result = originalApply.apply(this, args);
1210
+ try {
1211
+ installClipboardImagePasteCompat(decoratedCtx);
1212
+ } catch (error) {
1213
+ try { console.warn('vision-router: failed to install clipboard image paste compatibility', error); } catch (_) {}
1214
+ }
739
1215
  try {
740
1216
  installVisionModeToggle(decoratedCtx, React, primitives);
741
1217
  } catch (error) {
@@ -52,6 +52,109 @@ const VISION_TOGGLE_FIXED_SVG = String.raw` React.createElement('svg',
52
52
  }))
53
53
  : null`
54
54
 
55
+ const BITMAP_LIMITS_BASE = String.raw` var maxBitmapSourceBytes = 64 * 1024 * 1024;
56
+ var maxBitmapDimension = 10000;
57
+ var maxBitmapPixels = 100000000;`
58
+
59
+ const BITMAP_LIMITS_HARDENED = String.raw` var maxBitmapSourceBytes = 64 * 1024 * 1024;
60
+ var maxBitmapDimension = 10000;
61
+ var maxBitmapPixels = 100000000;
62
+ // Source bytes are not a safe proxy for browser decode cost. A low-bit-depth
63
+ // BMP can be only a few MiB on the clipboard and still expand to hundreds
64
+ // of MiB once createImageBitmap/canvas materializes RGBA pixels.
65
+ var maxBitmapDecodedBytes = 128 * 1024 * 1024;
66
+ var maxBitmapBatchDecodedBytes = 256 * 1024 * 1024;`
67
+
68
+ const BITMAP_DECODE_ALLOWED_BASE = String.raw` function bitmapDecodeAllowed(file, info) {
69
+ if (!file || !info) return false;
70
+ if (!Number.isFinite(file.size) || file.size <= 0 || file.size > maxBitmapSourceBytes) return false;
71
+ if (!Number.isFinite(info.width) || !Number.isFinite(info.height) || info.width <= 0 || info.height <= 0) return false;
72
+ if (info.width > maxBitmapDimension || info.height > maxBitmapDimension) return false;
73
+ return info.width * info.height <= maxBitmapPixels;
74
+ }`
75
+
76
+ const BITMAP_DECODE_ALLOWED_HARDENED = String.raw` function bitmapDecodeAllowed(file, info) {
77
+ if (!file || !info) return false;
78
+ if (!Number.isFinite(file.size) || file.size <= 0 || file.size > maxBitmapSourceBytes) return false;
79
+ if (!Number.isFinite(info.width) || !Number.isFinite(info.height) || info.width <= 0 || info.height <= 0) return false;
80
+ if (info.width > maxBitmapDimension || info.height > maxBitmapDimension) return false;
81
+ var pixels = info.width * info.height;
82
+ if (!Number.isFinite(pixels) || pixels > maxBitmapPixels) return false;
83
+ return pixels * 4 <= maxBitmapDecodedBytes;
84
+ }`
85
+
86
+ const BITMAP_NORMALIZE_BASE = String.raw` function normalizeFile(file) {
87
+ return headType(file).then(function(info) {
88
+ var detected = info && info.type;
89
+ if (detected && supported[detected]) {
90
+ return mediaType(file && file.type) === detected ? file : retypeFile(file, detected);
91
+ }
92
+ if (detected === 'image/bmp' && bitmapDecodeAllowed(file, info)) return bitmapToPng(file);
93
+ return file;
94
+ }, function(){ return file; });
95
+ }`
96
+
97
+ const BITMAP_NORMALIZE_HARDENED = String.raw` function normalizeFile(file, decodeBudget) {
98
+ return headType(file).then(function(info) {
99
+ var detected = info && info.type;
100
+ if (detected && supported[detected]) {
101
+ return mediaType(file && file.type) === detected ? file : retypeFile(file, detected);
102
+ }
103
+ if (detected === 'image/bmp' && bitmapDecodeAllowed(file, info)) {
104
+ var decodedBytes = info.width * info.height * 4;
105
+ if (decodeBudget && decodedBytes > decodeBudget.remaining) return file;
106
+ if (decodeBudget) decodeBudget.remaining -= decodedBytes;
107
+ return bitmapToPng(file);
108
+ }
109
+ return file;
110
+ }, function(){ return file; });
111
+ }`
112
+
113
+ const BITMAP_BATCH_BASE = String.raw` Promise.all(files.map(function(file){
114
+ return needsInspection(file)
115
+ ? normalizeFile(file).catch(function(){ return file; })
116
+ : Promise.resolve(file);
117
+ })).then(function(normalized) {
118
+ return dedupeBatch(normalized).then(function(deduped) {
119
+ dispatchReplay(target, replayEvent(deduped, textEntries), fallback);
120
+ });
121
+ }, function() {
122
+ dispatchReplay(target, fallback, null);
123
+ });`
124
+
125
+ const BITMAP_BATCH_HARDENED = String.raw` // Decode BMPs serially under one paste-scoped RGBA budget. This keeps peak
126
+ // browser memory bounded before the Host attachment governor gets a turn.
127
+ var decodeBudget = { remaining: maxBitmapBatchDecodedBytes };
128
+ var normalized = [];
129
+ var normalizeChain = Promise.resolve();
130
+ files.forEach(function(file){
131
+ normalizeChain = normalizeChain.then(function(){
132
+ if (!needsInspection(file)) {
133
+ normalized.push(file);
134
+ return undefined;
135
+ }
136
+ return normalizeFile(file, decodeBudget).then(function(value){
137
+ normalized.push(value);
138
+ }, function(){
139
+ normalized.push(file);
140
+ });
141
+ });
142
+ });
143
+ normalizeChain.then(function() {
144
+ return dedupeBatch(normalized).then(function(deduped) {
145
+ dispatchReplay(target, replayEvent(deduped, textEntries), fallback);
146
+ });
147
+ }, function() {
148
+ dispatchReplay(target, fallback, null);
149
+ });`
150
+
151
+ function replaceAllRequired(source, anchor, replacement, label) {
152
+ const parts = source.split(anchor)
153
+ const count = parts.length - 1
154
+ if (count === 0) throw new Error(`${label} transform anchor missing`)
155
+ return parts.join(replacement)
156
+ }
157
+
55
158
  export function transformVisionModeToggleIcons(source = BASE_CLIENT_PRESENTATION_PRELUDE) {
56
159
  const input = String(source)
57
160
  if (!input.includes(VISION_TOGGLE_TEXT_GLYPH)) {
@@ -60,7 +163,24 @@ export function transformVisionModeToggleIcons(source = BASE_CLIENT_PRESENTATION
60
163
  return input.replace(VISION_TOGGLE_TEXT_GLYPH, VISION_TOGGLE_FIXED_SVG)
61
164
  }
62
165
 
63
- export const CLIENT_PRESENTATION_PRELUDE = transformVisionModeToggleIcons()
166
+ export function transformBitmapDecodeBudget(source = BASE_CLIENT_PRESENTATION_PRELUDE) {
167
+ let output = String(source)
168
+ // The legacy prelude intentionally contains more than one clipboard owner.
169
+ // Harden every exact owner, not merely the first string match, so one stale
170
+ // path cannot retain unbounded parallel bitmap decoding.
171
+ output = replaceAllRequired(output, BITMAP_LIMITS_BASE, BITMAP_LIMITS_HARDENED, 'bitmap limits')
172
+ output = replaceAllRequired(
173
+ output,
174
+ BITMAP_DECODE_ALLOWED_BASE,
175
+ BITMAP_DECODE_ALLOWED_HARDENED,
176
+ 'bitmap decoded-byte admission',
177
+ )
178
+ output = replaceAllRequired(output, BITMAP_NORMALIZE_BASE, BITMAP_NORMALIZE_HARDENED, 'bitmap normalization')
179
+ output = replaceAllRequired(output, BITMAP_BATCH_BASE, BITMAP_BATCH_HARDENED, 'bitmap batch serialization')
180
+ return output
181
+ }
182
+
183
+ export const CLIENT_PRESENTATION_PRELUDE = transformBitmapDecodeBudget(transformVisionModeToggleIcons())
64
184
 
65
185
  export {
66
186
  resolveVisionModePair,
package/lib/client.js CHANGED
@@ -218,7 +218,7 @@ window.__ModuleLoader__.load({
218
218
  '用于兼容旧版行为,一般无需开启;开启后只使用上方识图模型链中的后端。',
219
219
  hintReverseRouting: '仅在「整轮交给视觉模型」开启时生效;纯文字消息继续交给聊天模型处理。',
220
220
  hintTool: '允许聊天模型按需查看、定位、裁剪和比较图片。推荐保持开启。',
221
- hintStructuredVisionBootstrap: '默认关闭。开启后,每个图片任务会先做一次不读取具体任务目标的结构化预识别,再由聊天模型根据原问题至少追加一次验证或深挖识图调用(1+x,x≥1)。启用的 Ollama / LM Studio 会和其他视觉后端一样参与这条识图链,不需要另开「即时识图」。准确性更高,但会增加至少一次视觉调用;若后续需要 OCR,自动模式会优先使用视觉模型。需保持「识图工具」开启。',
221
+ hintStructuredVisionBootstrap: '默认关闭。开启后,每个图片任务会先做一次不读取具体任务目标的结构化预识别,再由聊天模型根据原问题至少追加一次验证或深挖识图调用(1+x,x≥1)。启用的 Ollama / LM Studio 会和其他视觉后端一样参与这条识图链,不需要另开「即时识图」。准确性更高,但会增加至少一次视觉调用;若后续需要 OCR,engine=auto 仍会先尝试本地 Tesseract,失败或空结果再回退视觉模型;结构化模式不会改变这一顺序。需保持「识图工具」开启。',
222
222
  hintAutoWrapProviders: '自动为已启用的聊天模型创建带「+ 自动识图」的版本。原模型不受影响,推荐保持开启。',
223
223
  hintRewriteImages: '避免把无法读取的原始图片直接发送给纯文字模型。推荐保持开启。',
224
224
  hintDownscale: '超过像素预算的图片先缩放再送视觉模型,降低延迟与成本;默认开启。',
@@ -260,7 +260,7 @@ window.__ModuleLoader__.load({
260
260
  depthCapValueLabel: '最多深挖次数',
261
261
  depthCapInvalid: '请输入 1–100 之间的整数。',
262
262
  numHintTimeoutMs: '单个视觉请求超时;默认 120000。',
263
- numHintVisionTaskTimeoutMs: '一次识图任务(含全部 provider、回退与重试)共享的总时限;默认 45000。认证失败/限流会立即熔断对应后端。',
263
+ numHintVisionTaskTimeoutMs: '一次识图任务(含全部 provider、回退与重试)共享的总时限;默认 120000。认证失败/限流会立即熔断对应后端。',
264
264
  numHintOcrTimeoutMs: '一次 OCR 任务的总时限;本地 tesseract 最多用 12 秒,视觉模型回退只用剩余部分;默认 30000。',
265
265
  numHintDownscaleMaxPixels: '约 8MP = 8000000;超过的图先缩放再送视觉模型。',
266
266
  numHintCacheTtlSeconds: '视觉答案缓存有效期;0 = 永久;默认 3600。',
@@ -493,7 +493,7 @@ window.__ModuleLoader__.load({
493
493
  'only the vision models configured above participate.',
494
494
  hintReverseRouting: 'Only applies when whole-turn vision routing is enabled; plain text messages continue to use the chat model.',
495
495
  hintTool: 'Lets the chat model inspect, locate, crop, and compare images as needed. Recommended on.',
496
- hintStructuredVisionBootstrap: 'Off by default. Each image task first gets one task-independent structured visual baseline with no task goal passed into the pre-scan, then the chat model must make at least one evidence or deepening vision call for the original request (1+x, x>=1). Enabled Ollama / LM Studio backends participate in this same vision chain; there is no separate instant-recognition switch. This improves evidence quality but adds at least one visual call. If OCR is needed, auto mode prefers vision-model OCR. Keep Vision tools enabled.',
496
+ hintStructuredVisionBootstrap: 'Off by default. Each image task first gets one task-independent structured visual baseline with no task goal passed into the pre-scan, then the chat model must make at least one evidence or deepening vision call for the original request (1+x, x>=1). Enabled Ollama / LM Studio backends participate in this same vision chain; there is no separate instant-recognition switch. This improves evidence quality but adds at least one visual call. If OCR is needed, engine=auto still tries local Tesseract first and falls back to the vision model only when local OCR fails or returns no text; structured mode does not change this order. Keep Vision tools enabled.',
497
497
  hintAutoWrapProviders: 'Automatically creates a “+ Auto Vision” version of enabled chat models. Original model groups stay unchanged. Recommended on.',
498
498
  hintRewriteImages: 'Prevents raw image content from being sent to a text-only model that cannot read it. Recommended on.',
499
499
  hintDownscale: 'Images beyond the pixel budget are resized before the vision call, cutting latency and cost; on by default.',
@@ -537,7 +537,7 @@ window.__ModuleLoader__.load({
537
537
  depthCapValueLabel: 'Maximum deep-dive calls',
538
538
  depthCapInvalid: 'Enter an integer from 1 to 100.',
539
539
  numHintTimeoutMs: 'Per vision-call deadline; default 120000.',
540
- numHintVisionTaskTimeoutMs: 'One vision task (all providers, fallbacks and retries) shares this wall-clock budget; default 45000. Auth failures and rate limits trip their backend immediately.',
540
+ numHintVisionTaskTimeoutMs: 'One vision task (all providers, fallbacks and retries) shares this wall-clock budget; default 120000. Auth failures and rate limits trip their backend immediately.',
541
541
  numHintOcrTimeoutMs: 'Total budget for one OCR task: tesseract gets at most 12s, the vision fallback only the remainder; default 30000.',
542
542
  numHintDownscaleMaxPixels: 'About 8MP = 8000000; larger images are resized before the vision call.',
543
543
  numHintCacheTtlSeconds: 'Vision answer cache lifetime; 0 = forever; default 3600.',