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.
@@ -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、回退与重试)共享的总时限;默认 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.',
@@ -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: '检测到界面内容。建议元素清单(detect)+ 关键元素定位(ground)。',
54
+ ui: '检测到界面内容。按用户问题选择最小必要证据:语义复核、元素盘点或精确定位均可,不固定组合工具。',
55
55
  chat: '检测到聊天截图。关注气泡顺序与关键信息提取。',
56
56
  }),
57
57
  en: Object.assign(Object.create(null), {
58
- code: 'Code content detected. Transcribe code verbatim; use region-by-region transcription plus semantic verification instead of summarizing it.',
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. Prefer an element inventory (detect) plus grounding of the important elements (ground).',
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
  })
@@ -76,7 +76,7 @@ export function installLocalVisionStabilizer(ctx, config = {}, core) {
76
76
  1000,
77
77
  Math.min(
78
78
  positive(value.timeoutMs, 120000),
79
- positive(value.visionTaskTimeoutMs, 45000),
79
+ positive(value.visionTaskTimeoutMs, 120000),
80
80
  ),
81
81
  )
82
82
 
@@ -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:', 'detect / ground 优先(元素清单与像素定位)'],
27
- ['code:', '逐字转写(可执行性例外)'],
26
+ ['ui:', '按问题需要选择语义复核、元素盘点或精确定位,不固定工具组合'],
27
+ ['code:', '仅当任务依赖可执行或逐字代码时才要求逐字保真;否则按语义问题查证'],
28
28
  ['table:', '结构提取优先,数字/金额逐字'],
29
29
  ['_default', '放行(模型自由选择识别方式)'],
30
30
  ]),
31
31
  en: new Map([
32
- ['document:code', 'transcribe verbatim (code executability requires text-exact evidence)'],
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:', 'prefer detect / ground for element inventory and pixel localization'],
37
- ['code:', 'transcribe verbatim (executability requires text-exact evidence)'],
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}). To avoid omissions or misclassification, verify each branch separately as needed before answering; do not reuse one branch's recognition method blindly for another branch.`
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
  }