dsh-vision-router 2.0.0 → 2.1.0

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 (92) hide show
  1. package/README.md +33 -16
  2. package/README.zh.md +34 -17
  3. package/cordis.patch.yml +19 -9
  4. package/docs/architecture/2x-contract-ledger.md +165 -0
  5. package/docs/architecture/compat-inventory.md +121 -0
  6. package/docs/architecture/dsh-compatibility-matrix.md +57 -0
  7. package/docs/architecture/dsh-support-window.md +50 -0
  8. package/docs/architecture/final-closure-audit.md +148 -0
  9. package/docs/architecture/p2-jobs-feasibility.md +94 -0
  10. package/docs/architecture/p3-compat-retirement.md +46 -0
  11. package/docs/architecture/p3-host-native-seams.md +43 -0
  12. package/docs/architecture/p3-native-recovery-evaluation.md +65 -0
  13. package/docs/architecture/runtime-boundaries.md +124 -0
  14. package/docs/releases/v2.0.1.md +17 -0
  15. package/docs/releases/v2.1.0-support-window.md +29 -0
  16. package/docs/releases/v2.1.0.md +83 -0
  17. package/entry.js +13 -307
  18. package/index.js +85 -352
  19. package/lib/abort-signal-compat.js +191 -0
  20. package/lib/adapter-update-coalescer.js +113 -29
  21. package/lib/adversarial-hardening.js +24 -9
  22. package/lib/artifact-boundary.js +16 -175
  23. package/lib/artifact-io.js +220 -0
  24. package/lib/artifact-retention.js +305 -53
  25. package/lib/catalog-corrections.js +6 -1
  26. package/lib/client-host-compat-prelude.js +249 -0
  27. package/lib/client-presentation-boundary.js +61 -3
  28. package/lib/client.js +71 -72
  29. package/lib/core-vision-surface.js +124 -0
  30. package/lib/depth-guidance.js +114 -90
  31. package/lib/doctor-cli-p0.js +168 -0
  32. package/lib/doctor-cli.js +7 -0
  33. package/lib/doctor-vision-limits.js +72 -0
  34. package/lib/dsh-contract-compat.js +207 -39
  35. package/lib/dsh-host-capabilities.js +107 -0
  36. package/lib/dsh-support-window.js +48 -0
  37. package/lib/http-compat.js +22 -6
  38. package/lib/image-resource-governor.js +35 -10
  39. package/lib/legacy-core-vision-policy-bridge.js +25 -226
  40. package/lib/legacy-global-proxy-boundary.js +127 -0
  41. package/lib/live-model-client-prelude.js +0 -12
  42. package/lib/mixed-router.js +81 -81
  43. package/lib/public-entry.js +42 -4
  44. package/lib/runtime-composition.js +348 -0
  45. package/lib/runtime-i18n-boundary.js +683 -0
  46. package/lib/runtime-i18n-core-scope.js +56 -0
  47. package/lib/runtime-i18n-core.js +34 -0
  48. package/lib/runtime-i18n.js +121 -0
  49. package/lib/session-surface-policy.js +97 -0
  50. package/lib/session-vision-index.js +282 -0
  51. package/lib/session-vision-runtime.js +48 -0
  52. package/lib/settings-factory-lifecycle.js +144 -0
  53. package/lib/settings-ia-client-prelude.js +74 -2
  54. package/lib/settings-limit-client-prelude.js +110 -0
  55. package/lib/settings-native-card-layout.js +297 -0
  56. package/lib/settings-number-contract.js +29 -0
  57. package/lib/structured-flow-hardening.js +207 -76
  58. package/lib/tesseract-exec-compat.js +58 -32
  59. package/lib/update-check.js +17 -5
  60. package/lib/v2-settings-ia-integration.js +9 -4
  61. package/lib/vision-artifact-store.js +77 -0
  62. package/lib/vision-backend-runtime-policy.js +6 -2
  63. package/lib/vision-background-benchmark.js +130 -41
  64. package/lib/vision-background-failure-policy.js +70 -0
  65. package/lib/vision-background-stop-store.js +54 -9
  66. package/lib/vision-capability-benchmark-client.js +57 -66
  67. package/lib/vision-capability-benchmark-presentation.js +97 -0
  68. package/lib/vision-capability-benchmark-service.js +3 -3
  69. package/lib/vision-execution-order-apply.js +61 -0
  70. package/lib/vision-execution-order-plan.js +64 -0
  71. package/lib/vision-execution-order.js +38 -0
  72. package/lib/vision-limit-diagnostics.js +202 -0
  73. package/lib/vision-product-presentation.js +284 -0
  74. package/lib/vision-provider-transport.js +197 -0
  75. package/lib/vision-resilience.js +87 -45
  76. package/lib/vision-routing-evidence.js +366 -0
  77. package/lib/vision-routing-runtime.js +278 -0
  78. package/lib/vision-tool-runtime-boundary.js +27 -21
  79. package/lib/vision-turn-budget-client-prelude.js +9 -9
  80. package/lib/web/benchmark-panel.js +6 -0
  81. package/lib/web/diagnostics-panel.js +44 -0
  82. package/lib/web/index.js +60 -0
  83. package/lib/web/model-picker.js +22 -0
  84. package/lib/web/onboarding.js +6 -0
  85. package/lib/web/product-state.js +59 -0
  86. package/lib/web/remote-settings-client.js +8 -0
  87. package/lib/web/routing-section.js +6 -0
  88. package/lib/web/settings-controller.js +6 -0
  89. package/lib/web-capability-boundary.js +65 -9
  90. package/lib/windows-screenshot-dpi-compat.js +148 -0
  91. package/package.json +14 -6
  92. package/lib/vision-capability-shadow.js +0 -576
@@ -0,0 +1,124 @@
1
+ import {
2
+ currentSessionSurfacePolicy,
3
+ resolveSessionSurfacePolicy,
4
+ } from './session-surface-policy.js'
5
+
6
+ const CORE_SURFACE_KEYS = Object.freeze([
7
+ 'tool',
8
+ 'rewriteImages',
9
+ 'instantDescribe',
10
+ 'autoActivateOnImage',
11
+ 'structuredVisionBootstrap',
12
+ ])
13
+
14
+ function isObject(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
16
+ }
17
+
18
+ function own(object, key) {
19
+ return Object.prototype.hasOwnProperty.call(object, key)
20
+ }
21
+
22
+ function coreFlags(values) {
23
+ return {
24
+ toolAvailable: values.tool !== false,
25
+ rewriteEnabled: values.rewriteImages !== false,
26
+ instantDescribe: values.instantDescribe === true,
27
+ autoActivateOnImage: values.autoActivateOnImage !== false,
28
+ structuredBootstrap: values.structuredVisionBootstrap === true,
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Explicit core-facing projection of the SessionSurfacePolicy.
34
+ *
35
+ * The returned object carries only the five config values that the historical
36
+ * Settings/config projection could override plus explicit booleans consumed by
37
+ * Core and the already-resolved image ownership facts. It deliberately cannot
38
+ * masquerade as a Settings service or a full plugin config object.
39
+ */
40
+ export function projectCoreVisionSurface(config = {}, sessionPolicy) {
41
+ const value = isObject(config) ? config : {}
42
+ const policy = sessionPolicy && typeof sessionPolicy === 'object'
43
+ ? sessionPolicy
44
+ : resolveSessionSurfacePolicy({ config: value })
45
+ const overrides = isObject(policy.legacyConfigOverrides)
46
+ ? policy.legacyConfigOverrides
47
+ : {}
48
+ const projected = {}
49
+
50
+ for (const key of CORE_SURFACE_KEYS) {
51
+ projected[key] = own(overrides, key) ? overrides[key] : value[key]
52
+ }
53
+
54
+ return Object.freeze({
55
+ ownership: policy.ownership,
56
+ preserveRawImages: policy.preserveRawImages === true,
57
+ rewriteCurrentImages: policy.rewriteCurrentImages === true,
58
+ ...coreFlags(projected),
59
+ values: Object.freeze(projected),
60
+ })
61
+ }
62
+
63
+ /** Resolve an explicit core surface from an explicit ownership-policy input. */
64
+ export function resolveCoreVisionSurface({
65
+ visionPolicy,
66
+ config = {},
67
+ schemaBootstrapping = false,
68
+ } = {}) {
69
+ const value = isObject(config) ? config : {}
70
+ return projectCoreVisionSurface(
71
+ value,
72
+ resolveSessionSurfacePolicy({
73
+ visionPolicy,
74
+ config: value,
75
+ schemaBootstrapping,
76
+ }),
77
+ )
78
+ }
79
+
80
+ /** Resolve the current turn-local surface without exposing a Settings facade. */
81
+ export function currentCoreVisionSurface(config = {}, options = {}) {
82
+ const value = isObject(config) ? config : {}
83
+ return projectCoreVisionSurface(
84
+ value,
85
+ currentSessionSurfacePolicy(value, {
86
+ schemaBootstrapping: options?.schemaBootstrapping === true,
87
+ }),
88
+ )
89
+ }
90
+
91
+ /**
92
+ * One explicit production seam for Core's five policy-derived switches.
93
+ *
94
+ * The config source stays live, but schema bootstrap is an internal lifecycle
95
+ * bit rather than a fake Settings/config value. Direct Core callers can keep
96
+ * their two-argument compatibility path; composition passes this runtime as
97
+ * the authoritative production surface.
98
+ */
99
+ export function createCoreVisionSurfaceRuntime({ config = {} } = {}) {
100
+ let schemaBootstrapping = true
101
+
102
+ const current = () => {
103
+ let source = config
104
+ if (typeof config === 'function') {
105
+ try {
106
+ source = config()
107
+ } catch {
108
+ source = {}
109
+ }
110
+ }
111
+ return currentCoreVisionSurface(isObject(source) ? source : {}, {
112
+ schemaBootstrapping,
113
+ })
114
+ }
115
+
116
+ return Object.freeze({
117
+ current,
118
+ finishSchemaBootstrap() {
119
+ schemaBootstrapping = false
120
+ },
121
+ })
122
+ }
123
+
124
+ export const CORE_VISION_SURFACE_KEYS = CORE_SURFACE_KEYS
@@ -1,116 +1,140 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { runtimeLanguageFor } from './runtime-i18n.js'
2
3
 
3
4
  // 看图深度与场景引导(depth-guidance)。
4
5
  //
5
- // 移植自 dsh-vision 的「模板 + 路由」两层结构(prompts.py / vision_client.py):
6
- // - 模板层:按大类各写一条引导(zoom_* 的等价物),彼此独立、无组合
7
- // - 档位层:fast/standard/deep/custom 只决定"深挖多少"(步骤/上限),不参与提示词组合
8
- // 因此这里是"模板集合"而非"提示词矩阵"——工作量 = 引导表 + 拼接函数。
6
+ // 深度档位只决定模型如何查证,不再隐式决定调用次数:
7
+ // - fast:整体优先,减少不必要的重复识别;
8
+ // - standard:围绕用户问题按需查证;
9
+ // - deep:主动做局部检查与交叉验证。
9
10
  //
10
- // 拼接规则(运行时):
11
- // guidance = 场景引导(按 visual_kind 查表)
12
- // + general 时内容引导(按内容大类查表,纯提示词兜底)
13
- // + 档位句(fast 附"档位不足"提示,搬 dsh-vision 回答节思想)
14
- // 语言模型的追问 question 由模型自己生成(现状已自由),本模块不生成。
11
+ // 调用次数是独立的可选安全阀:visionDepthMaxCalls=0/空值表示不限,
12
+ // 1-100 表示本轮成功深挖证据调用的硬上限。bootstrap 预识别不计入。
13
+ // 旧配置 visionDepth=custom 继续兼容,按 standard 引导解释;正数
14
+ // visionDepthMaxCalls 仍会作为独立次数上限生效。
15
15
 
16
16
  const MAX_OVERRIDE_CHARS = 2000
17
17
  const DEPTH_RUNTIME_CONTEXT = new AsyncLocalStorage()
18
18
 
19
- // index.js predates the custom tier and still normalizes an unknown depth to
20
- // standard before it calls the helpers below. Keep the public core untouched:
21
- // the entry-layer structured guard exposes the live settings through this
22
- // async-local scope, so both the old inner quota and the outer hardening guard
23
- // see one authoritative fast=1 / standard=2 / deep=4 / custom=N policy.
24
19
  export function runWithDepthConfig(config, execute) {
25
20
  if (typeof execute !== 'function') return undefined
26
21
  const value = config && typeof config === 'object' ? config : {}
27
22
  return DEPTH_RUNTIME_CONTEXT.run(value, execute)
28
23
  }
29
24
 
30
- function activeDepth(depth, customMax) {
25
+ export function currentDepthRuntimeLocale() {
26
+ const value = DEPTH_RUNTIME_CONTEXT.getStore()?.__visionRouterLocale
27
+ return typeof value === 'string' && value.trim() !== '' ? value : undefined
28
+ }
29
+
30
+ function strategyDepth(depth) {
31
31
  const runtime = DEPTH_RUNTIME_CONTEXT.getStore()
32
- if (runtime && runtime.visionDepth === 'custom') {
33
- return { depth: 'custom', customMax: runtime.visionDepthMaxCalls }
34
- }
35
- return { depth, customMax }
32
+ const candidate = runtime?.visionDepth ?? depth
33
+ return candidate === 'fast' || candidate === 'deep' ? candidate : 'standard'
36
34
  }
37
35
 
38
- function normalizeCustomMax(value) {
36
+ function normalizeMaxCalls(value) {
39
37
  const number = Number(value)
40
- if (!Number.isFinite(number) || number <= 0) return 0
38
+ if (!Number.isFinite(number) || number <= 0) return undefined
41
39
  return Math.min(100, Math.max(1, Math.floor(number)))
42
40
  }
43
41
 
44
- // 用 null-prototype 表避免异常 kind(如 constructor / __proto__)命中
45
- // Object.prototype 上的继承属性。
46
- const SCENE_GUIDANCE = Object.assign(Object.create(null), {
47
- code: '检测到代码内容。代码必须逐字转写,建议分区域转写 + 语义确认,避免概括。',
48
- document: '检测到文档内容。语义优先;仅当需要逐字引用(长文档/合同/表单)时才用 OCR。',
49
- ui: '检测到界面内容。建议元素清单(detect)+ 关键元素定位(ground)。',
50
- chat: '检测到聊天截图。关注气泡顺序与关键信息提取。',
51
- // general 无固定场景引导——由 content_kind 内容引导接管(bootstrap 判出)
52
- // mixed / unknown 无场景引导(mixed 走分支引导,unknown 放行)
42
+ function effectiveMaxCalls(explicit) {
43
+ const runtime = DEPTH_RUNTIME_CONTEXT.getStore()
44
+ if (runtime && Object.prototype.hasOwnProperty.call(runtime, 'visionDepthMaxCalls')) {
45
+ return normalizeMaxCalls(runtime.visionDepthMaxCalls)
46
+ }
47
+ return normalizeMaxCalls(explicit)
48
+ }
49
+
50
+ const SCENE_GUIDANCE = Object.freeze({
51
+ zh: Object.assign(Object.create(null), {
52
+ code: '检测到代码内容。代码必须逐字转写,建议分区域转写 + 语义确认,避免概括。',
53
+ document: '检测到文档内容。语义优先;仅当需要逐字引用(长文档/合同/表单)时才用 OCR。',
54
+ ui: '检测到界面内容。建议元素清单(detect)+ 关键元素定位(ground)。',
55
+ chat: '检测到聊天截图。关注气泡顺序与关键信息提取。',
56
+ }),
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.',
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).',
61
+ chat: 'Chat screenshot detected. Preserve message-bubble order and extract the important information.',
62
+ }),
63
+ })
64
+
65
+ const CONTENT_GUIDANCE = Object.freeze({
66
+ zh: Object.assign(Object.create(null), {
67
+ person: '图中主体为人物:关注身份/表情/数量/姿态/关系/穿着。',
68
+ animal: '图中主体为动物:关注物种/数量/状态/环境。',
69
+ plant: '图中主体为植物:关注种类/生长状态/环境。',
70
+ food: '图中主体为食物:关注菜品/食材/卖相/就餐场景。',
71
+ vehicle: '图中主体为交通工具:关注品牌/型号/颜色/新旧/牌照。',
72
+ machine: '图中主体为机器/设备:关注类型/用途/状态/铭牌。',
73
+ architecture: '图中主体为建筑:关注类型/风格/细节/年代。',
74
+ object: '图中主体为物品:关注名称/材质/用途/品牌/型号。',
75
+ scene: '图中主体为场景:关注场景类型/主体/前景背景/光线/天气。',
76
+ meme: '图中为表情包/梗图:关注模板、文字与表达含义。',
77
+ }),
78
+ en: Object.assign(Object.create(null), {
79
+ person: 'The main subject is a person or people: focus on identity, expression, count, posture, relationships, and clothing.',
80
+ animal: 'The main subject is an animal or animals: focus on species, count, condition, and environment.',
81
+ plant: 'The main subject is a plant or plants: focus on type, growth condition, and environment.',
82
+ food: 'The main subject is food: focus on the dish, ingredients, appearance, and dining context.',
83
+ vehicle: 'The main subject is a vehicle: focus on make, model, color, condition, and license plate when visible.',
84
+ machine: 'The main subject is a machine or device: focus on type, purpose, condition, and nameplate information.',
85
+ architecture: 'The main subject is architecture: focus on type, style, details, and period when inferable from visible evidence.',
86
+ object: 'The main subject is an object: focus on its name, material, purpose, brand, and model when visible.',
87
+ scene: 'The main subject is a scene: focus on scene type, subjects, foreground/background, lighting, and weather.',
88
+ meme: 'The image is a meme/reaction image: focus on the template, text, and intended meaning.',
89
+ }),
53
90
  })
54
91
 
55
- // ── 内容引导表(按 content_kind 大类;bootstrap 判出,纯提示词方向,无出口绑定)──
56
- // 能力上限由用户配置的后端链决定,本表只负责"问对方向"。
57
- const CONTENT_GUIDANCE = Object.assign(Object.create(null), {
58
- person: '图中主体为人物:关注身份/表情/数量/姿态/关系/穿着。',
59
- animal: '图中主体为动物:关注物种/数量/状态/环境。',
60
- plant: '图中主体为植物:关注种类/生长状态/环境。',
61
- food: '图中主体为食物:关注菜品/食材/卖相/就餐场景。',
62
- vehicle: '图中主体为交通工具:关注品牌/型号/颜色/新旧/牌照。',
63
- machine: '图中主体为机器/设备:关注类型/用途/状态/铭牌。',
64
- architecture: '图中主体为建筑:关注类型/风格/细节/年代。',
65
- object: '图中主体为物品:关注名称/材质/用途/品牌/型号。',
66
- scene: '图中主体为场景:关注场景类型/主体/前景背景/光线/天气。',
67
- meme: '图中为表情包/梗图:关注模板、文字与表达含义。',
92
+ const GENERAL_FALLBACK_GUIDANCE = Object.freeze({
93
+ zh: '检测到未能明确归类的图。请先判断图中主体类别(人物/动物/植物/食物/交通工具/机器/建筑/物品/场景/表情包),再按该主体方向深挖(深挖时由你自行生成针对性问题)。',
94
+ en: 'The image could not be classified confidently. First identify the main subject category (person, animal, plant, food, vehicle, machine, architecture, object, scene, or meme), then deepen the inspection in that direction and generate targeted questions as needed.',
68
95
  })
69
96
 
70
- // general 且 content_kind 未知时的通用兜底(要求模型自行判断主体方向)。
71
- const GENERAL_FALLBACK_GUIDANCE =
72
- '检测到未能明确归类的图。请先判断图中主体类别(人物/动物/植物/食物/交通工具/机器/建筑/物品/场景/表情包),' +
73
- '再按该主体方向深挖(深挖时由你自行生成针对性问题)。'
97
+ const DEPTH_COPY = Object.freeze({
98
+ zh: Object.freeze({
99
+ fast: '本轮看图策略为快速:先做整体判断,优先使用已有视觉基线;只有关键证据不确定,或用户明确要求精确定位/逐字信息时,再调用必要的视觉工具。避免重复泛化识别。',
100
+ standard: '本轮看图策略为标准:围绕用户问题按需查证关键区域;需要时使用定位、裁剪、OCR、比对等工具补充或验证证据。证据充分后直接作答,不必为了流程继续调用。',
101
+ deep: '本轮看图策略为细致:主动检查关键局部并做交叉验证;对重要文字、位置、细节或差异,按需组合定位、裁剪、OCR、比对等工具,重要结论尽量由独立证据相互印证后再作答。',
102
+ }),
103
+ en: Object.freeze({
104
+ fast: 'Vision strategy is Quick for this turn: start from the whole-image judgment and reuse the existing visual baseline. Call an additional vision tool only when key evidence is uncertain or the user explicitly needs precise localization or verbatim text. Avoid repeating generic recognition.',
105
+ standard: "Vision strategy is Standard for this turn: verify the key regions required by the user's question as needed. Use grounding, cropping, OCR, comparison, or other tools when they can add or verify evidence. Answer once the evidence is sufficient; do not keep calling tools just to satisfy a workflow.",
106
+ deep: 'Vision strategy is Thorough for this turn: proactively inspect important regions and cross-check key claims. Combine grounding, cropping, OCR, comparison, and other precision tools as needed for important text, positions, details, or differences, and prefer independent evidence to corroborate important conclusions before answering.',
107
+ }),
108
+ })
74
109
 
75
- // ── 档位句 ────────────────────────────────────────────────────────────────
76
- const DEPTH_COPY = {
77
- fast: '本轮深度档位为 fast:仅做初步判断 + 1 次深挖即可;若你的问题需要深度定向识别,请告知用户升级档位。',
78
- standard: '本轮深度档位为 standard:深挖 1-2 次即可;第 2 次之后会停止新的证据调用。',
79
- deep: '本轮深度档位为 deep:可做 2-4 次充分深挖(定位→裁剪→比对→OCR 等)后再作答。',
110
+ function languageFor(locale) {
111
+ return runtimeLanguageFor(locale ?? currentDepthRuntimeLocale(), 'zh')
80
112
  }
81
113
 
82
114
  /**
83
- * 深度硬上限:fast=1、standard=2、deep=4。bootstrap 那 1 遍不计入。
84
- * custom=N(1-100)时 N 是本轮成功深挖调用的硬上限;custom=0/空值
85
- * 表示不设置次数上限。保存过的 custom 数值在切回内置档位后保留但不生效。
115
+ * Optional independent call cap. The depth strategy never supplies a cap.
116
+ * The depth parameter is retained for API/backward compatibility.
86
117
  */
87
- export function depthLimitFor(depth, customMax) {
88
- const active = activeDepth(depth, customMax)
89
- if (active.depth === 'custom') {
90
- const custom = normalizeCustomMax(active.customMax)
91
- return custom > 0 ? custom : undefined
92
- }
93
- if (active.depth === 'fast') return 1
94
- if (active.depth === 'deep') return 4
95
- return 2
118
+ export function depthLimitFor(_depth, maxCalls) {
119
+ return effectiveMaxCalls(maxCalls)
96
120
  }
97
121
 
98
- /** 档位句,与 depthLimitFor 的实际硬限制保持一致。 */
99
- export function depthCopyFor(depth, customMax) {
100
- const active = activeDepth(depth, customMax)
101
- if (active.depth === 'custom') {
102
- const custom = normalizeCustomMax(active.customMax)
103
- return custom > 0
104
- ? `本轮已自定义深挖上限为 ${custom} 次;达到上限后请基于已有证据作答。`
105
- : '本轮已选择自定义深挖:不设置次数上限;请只在能新增或验证证据时继续调用视觉工具。'
106
- }
107
- return DEPTH_COPY[active.depth === 'fast' || active.depth === 'deep' ? active.depth : 'standard'] || ''
122
+ /** Strategy guidance plus an optional, separately configured call-cap note. */
123
+ export function depthCopyFor(depth, maxCalls, locale) {
124
+ const language = languageFor(locale)
125
+ const strategy = strategyDepth(depth)
126
+ const strategyCopy = DEPTH_COPY[language][strategy] || DEPTH_COPY[language].standard
127
+ const cap = effectiveMaxCalls(maxCalls)
128
+ if (cap === undefined) return strategyCopy
129
+ const capCopy = language === 'en'
130
+ ? `A separate deep-dive call cap is enabled for this turn: at most ${cap} successful evidence calls. The bootstrap pre-scan does not count, and failed or empty-evidence calls do not consume the cap.`
131
+ : `本轮另启用了深挖次数上限:最多 ${cap} 次成功证据调用;bootstrap 预识别不计入,失败或空证据调用不占次数。`
132
+ return `${strategyCopy}\n${capCopy}`
108
133
  }
109
134
 
110
135
  /** 查表:覆盖表(guidanceOverrides)优先,其次内置表,最后 fallback。 */
111
136
  function resolveGuidance(kind, overrides, table, fallback = '') {
112
137
  if (Array.isArray(overrides)) {
113
- // 后写覆盖前写:重复 kind 的导入/编辑结果确定为 last-wins。
114
138
  for (let i = overrides.length - 1; i >= 0; i--) {
115
139
  const entry = overrides[i]
116
140
  if (entry && entry.kind === kind && typeof entry.text === 'string' && entry.text.trim() !== '') {
@@ -122,34 +146,34 @@ function resolveGuidance(kind, overrides, table, fallback = '') {
122
146
  }
123
147
 
124
148
  /** 场景引导:按 visual_kind 查表(覆盖优先);无则空串(不硬套)。 */
125
- export function sceneGuidanceFor(visualKind, overrides) {
126
- return resolveGuidance(visualKind, overrides, SCENE_GUIDANCE)
149
+ export function sceneGuidanceFor(visualKind, overrides, locale) {
150
+ return resolveGuidance(visualKind, overrides, SCENE_GUIDANCE[languageFor(locale)])
127
151
  }
128
152
 
129
- /** 内容引导:按内容大类查表(覆盖优先);无则空串。 */
130
- export function contentGuidanceFor(contentKind, overrides) {
131
- return resolveGuidance(contentKind, overrides, CONTENT_GUIDANCE)
153
+ /** 内容引导:按 content_kind 大类查表(覆盖优先);无则空串。 */
154
+ export function contentGuidanceFor(contentKind, overrides, locale) {
155
+ return resolveGuidance(contentKind, overrides, CONTENT_GUIDANCE[languageFor(locale)])
132
156
  }
133
157
 
134
158
  /**
135
- * 拼接一条完整的深挖引导(场景/内容 + 档位)。
159
+ * 拼接一条完整的深挖引导(场景/内容 + 策略 + 可选独立调用上限)。
136
160
  * general 时用 content_kind 内容引导(精确方向);content_kind 未知 → 通用兜底句。
137
161
  * guidanceOverrides:可配置的引导覆盖表(默认 undefined = 内置表)。
138
- * @param {object} opts - { visualKind, contentKind, depth, guidanceOverrides, customMax }
139
- * @returns {string} 空串表示无引导(放行)
162
+ * locale:显式传入优先;否则跟随当前 Host runtime locale;缺失时保留历史 zh 默认。
140
163
  */
141
- export function renderDepthGuidance({ visualKind, contentKind, depth, guidanceOverrides, customMax } = {}) {
142
- const depthCopy = depthCopyFor(depth, customMax)
164
+ export function renderDepthGuidance({ visualKind, contentKind, depth, guidanceOverrides, customMax, maxCalls, locale } = {}) {
165
+ const language = languageFor(locale)
166
+ const callCap = maxCalls === undefined ? customMax : maxCalls
167
+ const depthCopy = depthCopyFor(depth, callCap, language)
143
168
  if (!depthCopy) return ''
144
- const scene = sceneGuidanceFor(visualKind, guidanceOverrides)
145
- const content = contentGuidanceFor(contentKind, guidanceOverrides)
169
+ const scene = sceneGuidanceFor(visualKind, guidanceOverrides, language)
170
+ const content = contentGuidanceFor(contentKind, guidanceOverrides, language)
146
171
  const parts = []
147
172
  if (scene) {
148
173
  parts.push(scene)
149
174
  } else if (visualKind === 'general') {
150
- // general(媒介无引导):bootstrap 判出的 content_kind 给精确方向;未知则兜底
151
- parts.push(content || GENERAL_FALLBACK_GUIDANCE)
175
+ parts.push(content || GENERAL_FALLBACK_GUIDANCE[language])
152
176
  }
153
177
  parts.push(depthCopy)
154
- return parts.join('')
178
+ return parts.join('\n')
155
179
  }
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { realpathSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { run as runBaseDoctor } from './doctor-cli.js'
6
+ import {
7
+ formatDshHostCapability,
8
+ normalizeDshHostCapabilities,
9
+ } from './dsh-host-capabilities.js'
10
+ import {
11
+ DSH_SUPPORT_WINDOW,
12
+ formatDshSupportWindowLines,
13
+ supportWindowUpgradeAdvice,
14
+ } from './dsh-support-window.js'
15
+
16
+ const HOST_CAPABILITIES_PATH = '/_dsh/vision-router/host-capabilities'
17
+
18
+ function commandOf(argv) {
19
+ const first = argv.find((value) => typeof value === 'string' && !value.startsWith('-'))
20
+ return first && ['doctor', 'repair', 'repair-sessions'].includes(first) ? first : 'doctor'
21
+ }
22
+
23
+ function runtimeOptions(argv, env) {
24
+ let enabled = true
25
+ let baseUrl = env.DSH_WEB_URL || 'http://127.0.0.1:3080'
26
+ for (let index = 0; index < argv.length; index += 1) {
27
+ const value = argv[index]
28
+ if (value === '--no-runtime') enabled = false
29
+ if (value === '--runtime-url' && argv[index + 1]) {
30
+ baseUrl = argv[index + 1]
31
+ index += 1
32
+ } else if (typeof value === 'string' && value.startsWith('--runtime-url=')) {
33
+ baseUrl = value.slice('--runtime-url='.length)
34
+ }
35
+ }
36
+ return { enabled, baseUrl }
37
+ }
38
+
39
+ function unknownSnapshot() {
40
+ return normalizeDshHostCapabilities({})
41
+ }
42
+
43
+ export async function probeDoctorHostCapabilities({ baseUrl, fetchImpl = globalThis.fetch } = {}) {
44
+ try {
45
+ const normalized = new URL(baseUrl || 'http://127.0.0.1:3080')
46
+ normalized.pathname = HOST_CAPABILITIES_PATH
47
+ normalized.search = ''
48
+ normalized.hash = ''
49
+ if (typeof fetchImpl !== 'function') {
50
+ return { ok: false, source: 'runtime-unavailable', capabilities: unknownSnapshot() }
51
+ }
52
+ const response = await fetchImpl(normalized, {
53
+ method: 'GET',
54
+ headers: { accept: 'application/json' },
55
+ signal: AbortSignal.timeout(1500),
56
+ })
57
+ if (!response.ok) {
58
+ return {
59
+ ok: false,
60
+ source: 'runtime-route-unavailable',
61
+ status: response.status,
62
+ capabilities: unknownSnapshot(),
63
+ }
64
+ }
65
+ const body = await response.json()
66
+ return {
67
+ ok: true,
68
+ source: 'live-runtime',
69
+ capabilities: normalizeDshHostCapabilities(body?.capabilities),
70
+ }
71
+ } catch (error) {
72
+ return {
73
+ ok: false,
74
+ source: 'runtime-unavailable',
75
+ error: error instanceof Error ? error.message : String(error),
76
+ capabilities: unknownSnapshot(),
77
+ }
78
+ }
79
+ }
80
+
81
+ function capabilityLines(probe) {
82
+ const c = probe.capabilities
83
+ return [
84
+ 'DSH Host capabilities:',
85
+ ` batch attachments: ${formatDshHostCapability(c.batchAttachments)}`,
86
+ ` max image dimension: ${formatDshHostCapability(c.maxImageDimension)}`,
87
+ ` adapter registration: ${formatDshHostCapability(c.adapterRegistration)}`,
88
+ ` registration replace: ${formatDshHostCapability(c.registrationReplace)}`,
89
+ ` jobs: ${formatDshHostCapability(c.jobs)}`,
90
+ ` surface replacement: ${formatDshHostCapability(c.surfaceReplacement)}`,
91
+ ` settings live namespace: ${formatDshHostCapability(c.settingsLiveNamespace)}`,
92
+ ` settings web exposure: ${formatDshHostCapability(c.settingsWebExposure)}`,
93
+ ` prepareCall: ${formatDshHostCapability(c.prepareCall)}`,
94
+ ` tool registration: ${formatDshHostCapability(c.toolRegistration)}`,
95
+ ` tool execution: ${formatDshHostCapability(c.toolExecution)}`,
96
+ ` source: ${probe.source}`,
97
+ ...formatDshSupportWindowLines(c),
98
+ ]
99
+ }
100
+
101
+ function isJsonRequest(argv) {
102
+ return argv.includes('--json')
103
+ }
104
+
105
+ function isHelpRequest(argv) {
106
+ return argv.includes('--help') || argv.includes('-h')
107
+ }
108
+
109
+ /**
110
+ * P0 Doctor wrapper. Existing health semantics and exit codes stay owned by the
111
+ * established Doctor implementation; Host-capability diagnostics and P3's
112
+ * support-window policy are appended as advisory data and therefore can never
113
+ * turn a healthy runtime unhealthy.
114
+ */
115
+ export async function run(argv = process.argv.slice(2), io = console, env = process.env) {
116
+ if (commandOf(argv) !== 'doctor' || isHelpRequest(argv)) {
117
+ return runBaseDoctor(argv, io, env)
118
+ }
119
+
120
+ const runtime = runtimeOptions(argv, env)
121
+ const probe = runtime.enabled
122
+ ? await probeDoctorHostCapabilities({ baseUrl: runtime.baseUrl })
123
+ : { ok: false, source: 'runtime-probe-disabled', capabilities: unknownSnapshot() }
124
+
125
+ if (!isJsonRequest(argv)) {
126
+ const code = await runBaseDoctor(argv, io, env)
127
+ for (const line of capabilityLines(probe)) io.log(line)
128
+ return code
129
+ }
130
+
131
+ const logs = []
132
+ const errors = []
133
+ const capture = {
134
+ log(...values) { logs.push(values.map(String).join(' ')) },
135
+ error(...values) { errors.push(values.map(String).join(' ')) },
136
+ }
137
+ const code = await runBaseDoctor(argv, capture, env)
138
+ for (const line of errors) io.error(line)
139
+
140
+ if (logs.length === 1) {
141
+ try {
142
+ const report = JSON.parse(logs[0])
143
+ report.hostCapabilities = probe.capabilities
144
+ report.hostCapabilitiesSource = probe.source
145
+ report.hostSupportWindow = DSH_SUPPORT_WINDOW
146
+ report.hostSupportAdvice = supportWindowUpgradeAdvice(probe.capabilities)
147
+ io.log(JSON.stringify(report, null, 2))
148
+ return code
149
+ } catch {
150
+ // Fall through to the untouched output below.
151
+ }
152
+ }
153
+ for (const line of logs) io.log(line)
154
+ return code
155
+ }
156
+
157
+ export function isCliEntry(entry, moduleUrl = import.meta.url) {
158
+ if (typeof entry !== 'string' || entry.trim() === '') return false
159
+ const real = (value) => {
160
+ try { return realpathSync(value.startsWith('file:') ? fileURLToPath(value) : value) } catch { return value }
161
+ }
162
+ return real(entry) === real(moduleUrl)
163
+ }
164
+
165
+ if (isCliEntry(process.argv[1])) {
166
+ const code = await run()
167
+ if (Number.isInteger(code) && code !== 0) process.exitCode = code
168
+ }
package/lib/doctor-cli.js CHANGED
@@ -4,6 +4,7 @@ import { realpathSync } from 'node:fs'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { doctorProfiles, resolveDshHome } from './doctor.js'
6
6
  import { inspectPlatform, probeRuntime, supportReport } from './doctor-runtime.js'
7
+ import { inspectDoctorVisionLimits, formatDoctorVisionLimits } from './doctor-vision-limits.js'
7
8
  import { repairLegacySessionLogs } from './legacy-session-repair.js'
8
9
 
9
10
  function usage() {
@@ -206,6 +207,7 @@ export async function run(argv = process.argv.slice(2), io = console, env = proc
206
207
  .filter((item) => item.installation?.applicable && item.profileDir)
207
208
  .map((item) => item.profileDir)
208
209
  const platform = inspectPlatform({ profileDirs: applicableDirs })
210
+ const visionLimits = inspectDoctorVisionLimits(profileReport.log?.file)
209
211
  let sessions
210
212
  if (options.sessions) sessions = repairLegacySessionLogs({ dshHome, fix: false })
211
213
 
@@ -215,6 +217,7 @@ export async function run(argv = process.argv.slice(2), io = console, env = proc
215
217
 
216
218
  if (options.json) {
217
219
  const report = supportReport({ profileReport, runtime, platform, sessions })
220
+ report.visionLimits = visionLimits
218
221
  report.ok = overallOk
219
222
  io.log(JSON.stringify(report, null, 2))
220
223
  return overallOk ? 0 : 1
@@ -231,6 +234,10 @@ export async function run(argv = process.argv.slice(2), io = console, env = proc
231
234
  if (profileReport.log?.historicalSettingsSaveFailures?.length > 0) {
232
235
  io.log(`! Historical settings save failures exist (${profileReport.log.historicalSettingsSaveFailures.length}); none are attributed to the latest plugin start.`)
233
236
  }
237
+ for (const line of formatDoctorVisionLimits(visionLimits)) {
238
+ if (line.startsWith('WARN:')) io.log(`! ${line.slice('WARN:'.length).trim()}`)
239
+ else io.log(`✓ ${line}`)
240
+ }
234
241
 
235
242
  if (runtime) {
236
243
  if (!runtime.reachable) io.log(`– Runtime probe: DSH not reachable at ${runtime.baseUrl}; offline checks still completed.`)