dsh-vision-router 2.1.2 → 2.1.3
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/docs/releases/v2.1.3.md +34 -0
- package/index.js +79 -94
- package/lib/client-presentation-boundary-main.js +478 -2
- package/lib/client.js +4 -4
- package/lib/depth-guidance.js +4 -4
- package/lib/local-vision-stabilizer.js +1 -1
- package/lib/mixed-router.js +8 -8
- package/lib/runtime-i18n-boundary.js +27 -2
- package/lib/runtime-i18n.js +6 -6
- package/lib/settings-ia-client-prelude.js +1 -1
- package/lib/structured-bootstrap.js +2 -2
- package/lib/structured-flow-hardening.js +159 -158
- package/package.json +3 -3
|
@@ -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) {
|
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、回退与重试)共享的总时限;默认
|
|
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
|
|
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
|
|
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.',
|
package/lib/depth-guidance.js
CHANGED
|
@@ -49,15 +49,15 @@ function effectiveMaxCalls(explicit) {
|
|
|
49
49
|
|
|
50
50
|
const SCENE_GUIDANCE = Object.freeze({
|
|
51
51
|
zh: Object.assign(Object.create(null), {
|
|
52
|
-
code: '
|
|
52
|
+
code: '检测到代码内容。按用户问题决定证据:只有当结论依赖可执行或逐字代码时才需要逐字保真;仅问语言、结构或语义时可直接做针对性语义复核。',
|
|
53
53
|
document: '检测到文档内容。语义优先;仅当需要逐字引用(长文档/合同/表单)时才用 OCR。',
|
|
54
|
-
ui: '
|
|
54
|
+
ui: '检测到界面内容。按用户问题选择最小必要证据:语义复核、元素盘点或精确定位均可,不固定组合工具。',
|
|
55
55
|
chat: '检测到聊天截图。关注气泡顺序与关键信息提取。',
|
|
56
56
|
}),
|
|
57
57
|
en: Object.assign(Object.create(null), {
|
|
58
|
-
code: 'Code content detected.
|
|
58
|
+
code: 'Code content detected. Let the user question determine the evidence: require verbatim fidelity only when the conclusion depends on executable or text-exact code; language, structure, or semantic questions can use targeted semantic verification.',
|
|
59
59
|
document: 'Document content detected. Prefer semantic understanding; use OCR only when verbatim quotation is required, such as for long documents, contracts, or forms.',
|
|
60
|
-
ui: 'UI content detected.
|
|
60
|
+
ui: 'UI content detected. Choose the smallest evidence needed for the user question: semantic verification, element inventory, or precise localization; do not require a fixed tool combination.',
|
|
61
61
|
chat: 'Chat screenshot detected. Preserve message-bubble order and extract the important information.',
|
|
62
62
|
}),
|
|
63
63
|
})
|
package/lib/mixed-router.js
CHANGED
|
@@ -19,22 +19,22 @@ void UI_SIGNAL_TYPES
|
|
|
19
19
|
|
|
20
20
|
const BRANCH_GUIDANCE = Object.freeze({
|
|
21
21
|
zh: new Map([
|
|
22
|
-
['document:code', '
|
|
22
|
+
['document:code', '仅当任务依赖可执行或逐字代码时才做逐字转写;否则按语义问题查证'],
|
|
23
23
|
['document:form', '语义优先,逐字字段名/值确需引用时用 OCR'],
|
|
24
24
|
['document:table', '结构提取优先,数字/金额逐字(表格 OCR 专精场景)'],
|
|
25
25
|
['document:', '语义优先;仅当需要逐字引用(长文档/合同/表单)时才用 OCR'],
|
|
26
|
-
['ui:', '
|
|
27
|
-
['code:', '
|
|
26
|
+
['ui:', '按问题需要选择语义复核、元素盘点或精确定位,不固定工具组合'],
|
|
27
|
+
['code:', '仅当任务依赖可执行或逐字代码时才要求逐字保真;否则按语义问题查证'],
|
|
28
28
|
['table:', '结构提取优先,数字/金额逐字'],
|
|
29
29
|
['_default', '放行(模型自由选择识别方式)'],
|
|
30
30
|
]),
|
|
31
31
|
en: new Map([
|
|
32
|
-
['document:code', '
|
|
32
|
+
['document:code', 'use verbatim transcription only when the task depends on executable or text-exact code; otherwise verify the semantic question directly'],
|
|
33
33
|
['document:form', 'prefer semantic understanding; use OCR when exact field names or values must be quoted'],
|
|
34
34
|
['document:table', 'prefer structural extraction; preserve numbers and amounts exactly (table OCR is a specialized case)'],
|
|
35
35
|
['document:', 'prefer semantic understanding; use OCR only when verbatim quotation is required for long documents, contracts, or forms'],
|
|
36
|
-
['ui:', '
|
|
37
|
-
['code:', '
|
|
36
|
+
['ui:', 'choose semantic verification, element inventory, or precise localization according to the question; do not require a fixed tool combination'],
|
|
37
|
+
['code:', 'require verbatim fidelity only when the task depends on executable or text-exact code; otherwise verify the semantic question directly'],
|
|
38
38
|
['table:', 'prefer structural extraction and preserve numbers/amounts exactly'],
|
|
39
39
|
['_default', 'allow the model to choose the recognition method freely'],
|
|
40
40
|
]),
|
|
@@ -137,7 +137,7 @@ export function renderMixedGuidance(plan, _depth, locale) {
|
|
|
137
137
|
})
|
|
138
138
|
const kinds = branches.map((branch) => branch.kind).join(' + ')
|
|
139
139
|
const header = language === 'en'
|
|
140
|
-
? `Mixed content detected (${kinds}).
|
|
141
|
-
: `检测到混合内容(${kinds}
|
|
140
|
+
? `Mixed content detected (${kinds}). Focus on the branch or branches relevant to the user question. If the answer depends on more than one branch, verify those branches separately as needed; do not reuse one branch's recognition method blindly for another branch.`
|
|
141
|
+
: `检测到混合内容(${kinds})。只关注与用户问题相关的分支;如果答案确实依赖多个分支,再按需分别验证这些分支。分支之间不要盲目混用识别方式。`
|
|
142
142
|
return `${header}\n${lines.join('\n')}`
|
|
143
143
|
}
|