@mengruo/dsh-vision-toolkit 0.1.5 → 0.1.6-beta.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 (48) hide show
  1. package/assets/1.mp4 +0 -0
  2. package/assets/skill/SKILL.md +37 -5
  3. package/docs/plan-per-tool-visibility.md +82 -0
  4. package/lib/client.js +53 -3
  5. package/lib/client.js.map +1 -1
  6. package/lib/config.js +17 -0
  7. package/lib/config.js.map +1 -1
  8. package/lib/exposure.js +35 -8
  9. package/lib/exposure.js.map +1 -1
  10. package/lib/index.js +1 -1
  11. package/lib/index.js.map +1 -1
  12. package/lib/paths.js +9 -0
  13. package/lib/paths.js.map +1 -1
  14. package/lib/runtime.js +175 -1
  15. package/lib/runtime.js.map +1 -1
  16. package/lib/tools.js +116 -4
  17. package/lib/tools.js.map +1 -1
  18. package/lib/types/client/index.d.ts +24 -1
  19. package/lib/types/client/index.d.ts.map +1 -1
  20. package/lib/types/config.d.ts +26 -0
  21. package/lib/types/config.d.ts.map +1 -1
  22. package/lib/types/exposure.d.ts +14 -2
  23. package/lib/types/exposure.d.ts.map +1 -1
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/lib/types/paths.d.ts +7 -0
  26. package/lib/types/paths.d.ts.map +1 -1
  27. package/lib/types/runtime.d.ts +38 -0
  28. package/lib/types/runtime.d.ts.map +1 -1
  29. package/lib/types/tools.d.ts +22 -1
  30. package/lib/types/tools.d.ts.map +1 -1
  31. package/lib/types/video.d.ts +78 -0
  32. package/lib/types/video.d.ts.map +1 -0
  33. package/lib/types/web.d.ts +1 -0
  34. package/lib/types/web.d.ts.map +1 -1
  35. package/lib/video.js +169 -0
  36. package/lib/video.js.map +1 -0
  37. package/lib/web.js +47 -4
  38. package/lib/web.js.map +1 -1
  39. package/package.json +2 -1
  40. package/src/client/index.tsx +72 -1
  41. package/src/config.ts +43 -0
  42. package/src/exposure.ts +34 -9
  43. package/src/index.ts +10 -5
  44. package/src/paths.ts +11 -0
  45. package/src/runtime.ts +196 -0
  46. package/src/tools.ts +144 -3
  47. package/src/video.ts +222 -0
  48. package/src/web.ts +53 -5
@@ -77,6 +77,8 @@ const en = {
77
77
  streamHint: 'Request a streamed (SSE) completion instead of one JSON response. Use it for endpoints with weak non-streaming support or that time out on long outputs.',
78
78
  uploadViaUrl: 'Transfer images via URL',
79
79
  uploadViaUrlHint: 'Upload each image to the configured object storage and send the model a URL instead of base64. Requires object storage (below); region crops fall back to base64.',
80
+ videoSupport: 'Enable video support',
81
+ videoSupportHint: 'OpenAI-compatible video understanding. This setting is hidden for Anthropic providers and is currently a placeholder with no effect yet.',
80
82
  language: 'Output language',
81
83
  limits: 'Timeout and concurrency',
82
84
  timeout: 'Request timeout (ms)',
@@ -131,8 +133,10 @@ const en = {
131
133
  runHealth: 'Run health check',
132
134
  testConnection: 'Test API connection',
133
135
  testModel: 'Test vision model',
136
+ testVideo: 'Test video call',
134
137
  testing: 'Checking…',
135
138
  testingModel: 'Testing model…',
139
+ testingVideo: 'Testing video…',
136
140
  connectionHint: 'The API connection test only queries GET /models. The vision model test sends the bundled diagnostic image and verifies one real multimodal request.',
137
141
  saveBeforeTesting: 'Save service changes before testing the connection.',
138
142
  advanced: 'Advanced settings',
@@ -141,6 +145,14 @@ const en = {
141
145
  hiddenVariants: 'Transparent variant routing',
142
146
  hiddenVariantsLabel: 'Keep the original model names and enable images automatically',
143
147
  hiddenVariantsHint: 'Text-only models keep one model-selector entry with the original name while the session runs on the image-capable variant. Pasted images, image history, and the built-in read_image tool keep working; disable to restore the explicit (Vision Toolkit) entries.',
148
+ toolVisibility: 'Tool visibility',
149
+ toolVisibilityHint: 'Which groups of vision tools an Agent sees. Applies to the next Agent conversation: an already-active Agent keeps its activation-time set until it is reloaded.',
150
+ toolVisibilityLocal: 'Local tools',
151
+ toolVisibilityLocalHint: 'trace / crop / pixel diff / foreground / colors / HTML screenshot / video info. Local processing, no on-line parallel limits.',
152
+ toolVisibilityOnline: 'Online image tools',
153
+ toolVisibilityOnlineHint: 'glance / ground / detect / long-screenshot OCR, plus the concurrency/status probe. On-line service calls share the session concurrency budget.',
154
+ toolVisibilityVideo: 'Video tools',
155
+ toolVisibilityVideoHint: 'video understanding (experimental). Calling it without an enabled video-capable service returns "video understanding unavailable".',
144
156
  pluginVersion: 'Plugin',
145
157
  upstreamVersion: 'Upstream',
146
158
  activeGeneration: 'Runtime generation',
@@ -312,6 +324,8 @@ const zh: Record<LocaleKey, string> = {
312
324
  streamHint: '以流式(SSE)方式请求补全,而非一次性返回 JSON。适合对非流式支持不佳、长输出易连接超时的端点。',
313
325
  uploadViaUrl: '通过 URL 传输图片',
314
326
  uploadViaUrlHint: '把每张图片上传到下方配置的对象存储,将 URL 传给模型而不是 base64。需要先配置对象存储;使用 region 裁剪时回退为 base64。',
327
+ videoSupport: '启用视频支持',
328
+ videoSupportHint: 'OpenAI 兼容接口的视频理解能力。Anthropic 服务不显示此项;当前为占位设置,尚未生效。',
315
329
  language: '结果语言',
316
330
  limits: '超时与并发限制',
317
331
  timeout: '单次请求超时(毫秒)',
@@ -366,8 +380,10 @@ const zh: Record<LocaleKey, string> = {
366
380
  runHealth: '检查本地环境',
367
381
  testConnection: '测试 API 连接',
368
382
  testModel: '测试视觉模型',
383
+ testVideo: '测试视频调用',
369
384
  testing: '检查中…',
370
385
  testingModel: '正在测试模型…',
386
+ testingVideo: '正在测试视频…',
371
387
  connectionHint: '“测试 API 连接”只请求 GET /models;“测试视觉模型”会发送插件自带的诊断图片,验证一次真实多模态调用。',
372
388
  saveBeforeTesting: '修改服务配置后,请先保存,再执行 API 或视觉模型测试。',
373
389
  advanced: '高级设置',
@@ -376,6 +392,14 @@ const zh: Record<LocaleKey, string> = {
376
392
  hiddenVariants: '透明变体路由',
377
393
  hiddenVariantsLabel: '保留原模型名并自动启用图片能力',
378
394
  hiddenVariantsHint: '文本模型在模型列表中只显示原名称,会话实际运行在支持图片的变体路由上:粘贴图片、历史图片和内置 read_image 工具均可正常使用。关闭后恢复显示显式的(Vision Toolkit)条目。',
395
+ toolVisibility: '工具可见性',
396
+ toolVisibilityHint: 'Agent 可见的视觉工具分组。仅在下一个 agent 对话中生效:已激活的 agent 保持激活时的工具集,直到重载。',
397
+ toolVisibilityLocal: '本地工具',
398
+ toolVisibilityLocalHint: 'trace / crop / 像素对比 / 前景提取 / 取色 / HTML 截图 / 视频信息。本地处理,不受联机并发限制。',
399
+ toolVisibilityOnline: '联机图片工具',
400
+ toolVisibilityOnlineHint: 'glance / ground / detect / 长截图 OCR,以及并发/状态查询。联机调用共享会话并发额度。',
401
+ toolVisibilityVideo: '视频工具',
402
+ toolVisibilityVideoHint: '视频理解(测试)。调用时若没有已启用且支持视频的服务,将返回“视频理解不可用”。',
379
403
  pluginVersion: '插件版本',
380
404
  upstreamVersion: '工具包版本',
381
405
  activeGeneration: '本次运行已应用',
@@ -569,6 +593,7 @@ interface ProviderValue {
569
593
  userAgent?: string
570
594
  stream?: boolean
571
595
  uploadViaUrl?: boolean
596
+ videoSupport?: boolean
572
597
  t1Seconds?: number
573
598
  t2Seconds?: number
574
599
  maxImageBytes?: number
@@ -587,6 +612,7 @@ interface SettingsValue {
587
612
  userAgent?: string
588
613
  stream?: boolean
589
614
  uploadViaUrl?: boolean
615
+ videoSupport?: boolean
590
616
  }
591
617
  providers?: ProviderValue[]
592
618
  language?: 'zh' | 'en'
@@ -612,6 +638,11 @@ interface SettingsValue {
612
638
  autoSwitch?: boolean
613
639
  hidden?: boolean
614
640
  }
641
+ toolVisibility?: {
642
+ local?: boolean
643
+ online?: boolean
644
+ video?: boolean
645
+ }
615
646
  }
616
647
 
617
648
  type PluginUpdateUnavailableReason =
@@ -1049,7 +1080,8 @@ interface SettingsState {
1049
1080
  update?: PluginUpdateCheck | undefined
1050
1081
  restart?: PluginUpdateResult | undefined
1051
1082
  storageTest?: { detail: string } | undefined
1052
- action?: 'save' | 'health' | 'connection' | 'model' | 'check-update' | 'apply-update' | 'test-storage' | undefined
1083
+ videoTest?: { detail: string } | undefined
1084
+ action?: 'save' | 'health' | 'connection' | 'model' | 'check-update' | 'apply-update' | 'test-storage' | 'test-video' | undefined
1053
1085
  message?: string | undefined
1054
1086
  error?: string | undefined
1055
1087
  }
@@ -1198,6 +1230,20 @@ export class VisionSettingsController {
1198
1230
  }
1199
1231
  }
1200
1232
 
1233
+ async runVideoTest(providerIndex?: number): Promise<void> {
1234
+ this.set({ ...this.state, action: 'test-video', error: undefined, message: undefined })
1235
+ try {
1236
+ const result = await apiRequest<{ detail: string }>({
1237
+ method: 'POST',
1238
+ headers: { 'Content-Type': 'application/json' },
1239
+ body: JSON.stringify({ action: 'test-video', ...(providerIndex === undefined ? {} : { providerIndex }) }),
1240
+ })
1241
+ this.set({ ...this.state, action: undefined, videoTest: result })
1242
+ } catch (error) {
1243
+ this.set({ ...this.state, action: undefined, error: error instanceof Error ? error.message : String(error) })
1244
+ }
1245
+ }
1246
+
1201
1247
  async checkUpdate(): Promise<void> {
1202
1248
  this.set({ ...this.state, action: 'check-update', error: undefined, message: undefined })
1203
1249
  try {
@@ -1248,6 +1294,7 @@ interface ProviderDraft {
1248
1294
  userAgent: string
1249
1295
  stream: boolean
1250
1296
  uploadViaUrl: boolean
1297
+ videoSupport: boolean
1251
1298
  t1Seconds: string
1252
1299
  t2Seconds: string
1253
1300
  maxImageBytes: string
@@ -1277,6 +1324,9 @@ interface Draft {
1277
1324
  variantEnabled: boolean
1278
1325
  variantProviders: string
1279
1326
  variantAutoSwitch: boolean
1327
+ toolVisibilityLocal: boolean
1328
+ toolVisibilityOnline: boolean
1329
+ toolVisibilityVideo: boolean
1280
1330
  }
1281
1331
 
1282
1332
  function randomProviderId(): string {
@@ -1313,6 +1363,7 @@ function emptyProviderDraft(defaults: ProviderDefaults): ProviderDraft {
1313
1363
  userAgent: DEFAULT_USER_AGENT,
1314
1364
  stream: false,
1315
1365
  uploadViaUrl: false,
1366
+ videoSupport: false,
1316
1367
  t1Seconds: '90',
1317
1368
  t2Seconds: '90',
1318
1369
  maxImageBytes: String(defaults.maxImageBytes),
@@ -1337,6 +1388,7 @@ function providerDraftOf(value: ProviderValue | undefined, fallback: ProviderVal
1337
1388
  userAgent: source?.userAgent ?? DEFAULT_USER_AGENT,
1338
1389
  stream: source?.stream === true,
1339
1390
  uploadViaUrl: source?.uploadViaUrl === true,
1391
+ videoSupport: source?.videoSupport === true,
1340
1392
  t1Seconds: String(source?.t1Seconds ?? 90),
1341
1393
  t2Seconds: String(source?.t2Seconds ?? 90),
1342
1394
  maxImageBytes: String(source?.maxImageBytes ?? defaults.maxImageBytes),
@@ -1377,6 +1429,9 @@ function draftOf(value: SettingsValue): Draft {
1377
1429
  variantEnabled: value.imageInputVariants?.enabled ?? true,
1378
1430
  variantProviders: (value.imageInputVariants?.providers ?? []).join('\n'),
1379
1431
  variantAutoSwitch: value.imageInputVariants?.autoSwitch ?? true,
1432
+ toolVisibilityLocal: value.toolVisibility?.local ?? true,
1433
+ toolVisibilityOnline: value.toolVisibility?.online ?? true,
1434
+ toolVisibilityVideo: value.toolVisibility?.video ?? false,
1380
1435
  }
1381
1436
  }
1382
1437
 
@@ -1409,6 +1464,7 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1409
1464
  ...(provider.userAgent.trim() === DEFAULT_USER_AGENT ? {} : { userAgent: provider.userAgent.trim() }),
1410
1465
  ...(provider.stream ? { stream: true } : {}),
1411
1466
  ...(provider.uploadViaUrl ? { uploadViaUrl: true } : {}),
1467
+ ...(provider.videoSupport ? { videoSupport: true } : {}),
1412
1468
  ...(provider.t1Seconds.trim().length === 0 ? {} : { t1Seconds: positiveInteger(provider.t1Seconds, t('t1'), t) }),
1413
1469
  ...(provider.t2Seconds.trim().length === 0 ? {} : { t2Seconds: positiveInteger(provider.t2Seconds, t('t2'), t) }),
1414
1470
  ...(provider.maxImageBytes.trim().length === 0 ? {} : { maxImageBytes: positiveInteger(provider.maxImageBytes, t('maxBytes'), t) }),
@@ -1427,6 +1483,7 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1427
1483
  userAgent: primary?.userAgent.trim() || DEFAULT_USER_AGENT,
1428
1484
  stream: primary?.stream === true,
1429
1485
  uploadViaUrl: primary?.uploadViaUrl === true,
1486
+ videoSupport: primary?.videoSupport === true,
1430
1487
  },
1431
1488
  providers,
1432
1489
  language: draft.language,
@@ -1461,6 +1518,11 @@ function valueOf(draft: Draft, t: Translate): SettingsValue {
1461
1518
  ...(draft.variantAutoSwitch ? {} : { autoSwitch: false }),
1462
1519
  hidden: draft.hiddenVariants,
1463
1520
  },
1521
+ toolVisibility: {
1522
+ local: draft.toolVisibilityLocal,
1523
+ online: draft.toolVisibilityOnline,
1524
+ video: draft.toolVisibilityVideo,
1525
+ },
1464
1526
  }
1465
1527
  }
1466
1528
 
@@ -1803,6 +1865,7 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1803
1865
  <Field label={t('userAgent')}><Input aria-label={t('userAgent')} value={selectedProvider.userAgent} onChange={(event) => { updateProvider(selectedIndex, 'userAgent', event.target.value) }} /></Field>
1804
1866
  <Field label={t('stream')} hint={t('streamHint')}><select aria-label={t('stream')} disabled={!snapshot.writable || busy} value={selectedProvider.stream ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'stream', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field>
1805
1867
  <Field label={t('uploadViaUrl')} hint={t('uploadViaUrlHint')}><select aria-label={t('uploadViaUrl')} disabled={!snapshot.writable || busy} value={selectedProvider.uploadViaUrl ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'uploadViaUrl', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field>
1868
+ {selectedProvider.protocol === 'openai' ? <Field label={t('videoSupport')} hint={t('videoSupportHint')}><select aria-label={t('videoSupport')} disabled={!snapshot.writable || busy} value={selectedProvider.videoSupport ? 'enabled' : 'disabled'} onChange={(event) => { updateProvider(selectedIndex, 'videoSupport', event.target.value === 'enabled') }}><option value="disabled">{t('disabled')}</option><option value="enabled">{t('enabled')}</option></select></Field> : null}
1806
1869
  <Field label={t('t1')} hint={t('t1Hint')}><Input aria-label={t('t1')} inputMode="numeric" value={selectedProvider.t1Seconds} onChange={(event) => { updateProvider(selectedIndex, 't1Seconds', event.target.value) }} /></Field>
1807
1870
  <Field label={t('t2')} hint={t('t2Hint')}><Input aria-label={t('t2')} inputMode="numeric" value={selectedProvider.t2Seconds} onChange={(event) => { updateProvider(selectedIndex, 't2Seconds', event.target.value) }} /></Field>
1808
1871
  <Field label={t('maxBytes')}><Input aria-label={t('maxBytes')} inputMode="numeric" value={selectedProvider.maxImageBytes} onChange={(event) => { updateProvider(selectedIndex, 'maxImageBytes', event.target.value) }} /></Field>
@@ -1814,12 +1877,14 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1814
1877
  <div className="dvt-provider-tests">
1815
1878
  <Button size="sm" variant="outline" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('connection', selectedIndex) }}>{state.action === 'connection' ? t('testing') : t('testConnection')}</Button>
1816
1879
  <Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runHealth('model', selectedIndex) }}>{state.action === 'model' ? t('testingModel') : t('testModel')}</Button>
1880
+ {selectedProvider.protocol === 'openai' ? <Button size="sm" variant="primary" disabled={busy || !snapshot.runtime.ready} onClick={() => { void controller.runVideoTest(selectedIndex) }}>{state.action === 'test-video' ? t('testingVideo') : t('testVideo')}</Button> : null}
1817
1881
  </div>
1818
1882
  {providerHealth === undefined ? null : <div className="dvt-provider-test-result">{(['service', 'model'] as const).map(name => {
1819
1883
  const check = providerHealth.checks[name]
1820
1884
  if (check === undefined) return null
1821
1885
  return <div key={name} data-status={check.status}><span>{t(HEALTH_NAME_KEYS[name] ?? 'health')}</span><strong>{t(HEALTH_STATUS_KEYS[check.status])}</strong><p>{healthDetail(name, check.detail, t)}</p></div>
1822
1886
  })}</div>}
1887
+ {state.videoTest === undefined ? null : <div className="dvt-provider-test-result"><div data-status="ok"><span>{t('videoSupport')}</span><strong>{t('statusOk')}</strong><p>{state.videoTest.detail}</p></div></div>}
1823
1888
  </div>
1824
1889
  )}
1825
1890
  </section>
@@ -1887,6 +1952,12 @@ function LoadedSettings({ controller, t }: SettingsInjected) {
1887
1952
 
1888
1953
  <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('imageInput')}</h3></div><label className="dvt-check"><input type="checkbox" checked={draft.hiddenVariants} disabled={!snapshot.writable || busy} onChange={(event) => { update('hiddenVariants', event.target.checked) }} /><span>{t('hiddenVariantsLabel')}</span><small>{t('hiddenVariantsHint')}</small></label></section>
1889
1954
 
1955
+ <section className="dvt-panel"><div className="dvt-panel-title"><div><h3>{t('toolVisibility')}</h3><p>{t('toolVisibilityHint')}</p></div></div>
1956
+ <label className="dvt-check"><input type="checkbox" checked={draft.toolVisibilityLocal} disabled={!snapshot.writable || busy} onChange={(event) => { update('toolVisibilityLocal', event.target.checked) }} /><span>{t('toolVisibilityLocal')}</span><small>{t('toolVisibilityLocalHint')}</small></label>
1957
+ <label className="dvt-check"><input type="checkbox" checked={draft.toolVisibilityOnline} disabled={!snapshot.writable || busy} onChange={(event) => { update('toolVisibilityOnline', event.target.checked) }} /><span>{t('toolVisibilityOnline')}</span><small>{t('toolVisibilityOnlineHint')}</small></label>
1958
+ <label className="dvt-check"><input type="checkbox" checked={draft.toolVisibilityVideo} disabled={!snapshot.writable || busy} onChange={(event) => { update('toolVisibilityVideo', event.target.checked) }} /><span>{t('toolVisibilityVideo')} <em>(测试)</em></span><small>{t('toolVisibilityVideoHint')}</small></label>
1959
+ </section>
1960
+
1890
1961
  <section className="dvt-panel"><div className="dvt-panel-title"><h3>{t('storage')}</h3></div><div className="dvt-form-grid">
1891
1962
  <Field label={t('storageDir')} hint={t('storageDirHint')}><Input aria-label={t('storageDir')} placeholder="/tmp/dsh-vision-toolkit" value={draft.storageDir} onChange={(event) => { update('storageDir', event.target.value) }} /></Field>
1892
1963
  <Field label={t('allowedDirs')} hint={t('allowedDirsHint')}><textarea rows={3} value={draft.allowedDirs} onChange={(event) => { update('allowedDirs', event.target.value) }} /></Field>
package/src/config.ts CHANGED
@@ -81,6 +81,8 @@ export interface VisionProviderConfig {
81
81
  stream?: boolean
82
82
  /** Whether to upload images to object storage and send the model a URL instead of base64 (default false). */
83
83
  uploadViaUrl?: boolean
84
+ /** OpenAI-compatible video understanding (default false); Anthropic providers ignore this placeholder flag. */
85
+ videoSupport?: boolean
84
86
  /** t1: per-request hedge threshold in seconds. A single request exceeding t1 keeps running while the next provider starts in parallel. */
85
87
  t1Seconds?: number
86
88
  /** t2: per-provider cumulative cutoff in seconds. Total accumulated request time reaching t2 terminates the provider. */
@@ -114,6 +116,8 @@ export interface VisionToolkitConfig {
114
116
  stream?: boolean
115
117
  /** Whether to upload images to object storage and send the model a URL instead of base64 (default false). */
116
118
  uploadViaUrl?: boolean
119
+ /** OpenAI-compatible video understanding (default false); Anthropic providers ignore this placeholder flag. */
120
+ videoSupport?: boolean
117
121
  }
118
122
  /** Ordered online vision providers; array order is the failover priority. */
119
123
  providers?: VisionProviderConfig[]
@@ -194,6 +198,21 @@ export interface VisionToolkitConfig {
194
198
  */
195
199
  hidden?: boolean
196
200
  }
201
+ /**
202
+ * Per-group tool visibility, applied when each Agent's visual tool set is
203
+ * materialized (a session-head snapshot: an already-active Agent keeps its
204
+ * activation-time set; a change only affects the next Agent's tool set).
205
+ * Tools are grouped into three buckets; a bucket that is off contributes none
206
+ * of its tools to an Agent's visible surface.
207
+ */
208
+ toolVisibility?: {
209
+ /** Local-processing tools (no on-line fan-out, no concurrency charge). Default true. */
210
+ local?: boolean
211
+ /** On-line image tools plus the concurrency/status probe. Default true. */
212
+ online?: boolean
213
+ /** Video-understanding tool (experimental). Default false. */
214
+ video?: boolean
215
+ }
197
216
  }
198
217
 
199
218
  /** Configuration schema with the documented P0 defaults. */
@@ -206,6 +225,8 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
206
225
  anthropicThinking: z.union(['omit', 'disabled', 'adaptive'] as const).default('omit'),
207
226
  userAgent: z.string().default(DEFAULT_VISION_USER_AGENT),
208
227
  stream: z.boolean().default(false),
228
+ uploadViaUrl: z.boolean().default(false),
229
+ videoSupport: z.boolean().default(false),
209
230
  }),
210
231
  providers: z.array(z.object({
211
232
  name: z.string(),
@@ -219,6 +240,7 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
219
240
  userAgent: z.string(),
220
241
  stream: z.boolean().default(false),
221
242
  uploadViaUrl: z.boolean().default(false),
243
+ videoSupport: z.boolean().default(false),
222
244
  t1Seconds: z.number(),
223
245
  t2Seconds: z.number(),
224
246
  maxImageBytes: z.number(),
@@ -253,6 +275,11 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
253
275
  autoSwitch: z.boolean().default(true),
254
276
  hidden: z.boolean().default(true),
255
277
  }),
278
+ toolVisibility: z.object({
279
+ local: z.boolean().default(true),
280
+ online: z.boolean().default(true),
281
+ video: z.boolean().default(false),
282
+ }),
256
283
  })
257
284
 
258
285
  /** One resolved online vision provider, with every default materialized. */
@@ -269,6 +296,7 @@ export interface ResolvedProvider {
269
296
  userAgent: string
270
297
  stream: boolean
271
298
  uploadViaUrl: boolean
299
+ videoSupport: boolean
272
300
  t1Seconds: number
273
301
  t2Seconds: number
274
302
  maxImageBytes: number
@@ -288,6 +316,7 @@ export interface ResolvedVisionToolkitConfig {
288
316
  userAgent: string
289
317
  stream: boolean
290
318
  uploadViaUrl: boolean
319
+ videoSupport: boolean
291
320
  }
292
321
  /** Ordered failover pool; array order is the priority, highest first. */
293
322
  providers: ResolvedProvider[]
@@ -318,6 +347,11 @@ export interface ResolvedVisionToolkitConfig {
318
347
  autoSwitch: boolean
319
348
  hidden: boolean
320
349
  }
350
+ toolVisibility: {
351
+ local: boolean
352
+ online: boolean
353
+ video: boolean
354
+ }
321
355
  }
322
356
 
323
357
  const MAX_TIMEOUT_SECONDS = 600
@@ -410,6 +444,7 @@ function resolveProvider(
410
444
  }
411
445
  const stream = input.stream === true
412
446
  const uploadViaUrl = input.uploadViaUrl === true
447
+ const videoSupport = input.videoSupport === true
413
448
  const t1Seconds = input.t1Seconds ?? 90
414
449
  if (!Number.isInteger(t1Seconds) || t1Seconds < 1 || t1Seconds > MAX_TIMEOUT_SECONDS) {
415
450
  throw new VisionToolkitError('config', `${label}.t1Seconds must be an integer between 1 and ${MAX_TIMEOUT_SECONDS}`)
@@ -448,6 +483,7 @@ function resolveProvider(
448
483
  userAgent,
449
484
  stream,
450
485
  uploadViaUrl,
486
+ videoSupport,
451
487
  t1Seconds,
452
488
  t2Seconds,
453
489
  maxImageBytes,
@@ -535,6 +571,7 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
535
571
  const variantProviders = (imageInputVariants.providers ?? [])
536
572
  .map(provider => provider.trim())
537
573
  .filter(provider => provider.length > 0)
574
+ const toolVisibility = config.toolVisibility ?? {}
538
575
  const providerDefaults: ProviderDefaults = { maxImageBytes, maxImagePixels, concurrency }
539
576
  const configuredProviders = config.providers ?? []
540
577
  if (configuredProviders.length > MAX_PROVIDERS) {
@@ -555,6 +592,7 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
555
592
  userAgent: primary.userAgent,
556
593
  stream: primary.stream,
557
594
  uploadViaUrl: primary.uploadViaUrl,
595
+ videoSupport: primary.videoSupport,
558
596
  },
559
597
  providers,
560
598
  language,
@@ -584,6 +622,11 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
584
622
  autoSwitch: imageInputVariants.autoSwitch ?? true,
585
623
  hidden: imageInputVariants.hidden ?? true,
586
624
  },
625
+ toolVisibility: {
626
+ local: toolVisibility.local ?? true,
627
+ online: toolVisibility.online ?? true,
628
+ video: toolVisibility.video ?? false,
629
+ },
587
630
  }
588
631
  }
589
632
 
package/src/exposure.ts CHANGED
@@ -12,7 +12,7 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
12
12
  import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
13
13
  import type { Context } from '@deepseek-ai/cordis'
14
14
  import { VISION_SKILLS_CONTENT, VISION_SKILLS_NAME } from './skill.ts'
15
- import { VISION_TOOL_NAMES } from './tools.ts'
15
+ import { VISION_TOOL_NAMES, type ToolVisibility } from './tools.ts'
16
16
 
17
17
  /** Small bootstrap tool retained only until the current Agent gains visual tools. */
18
18
  export const VISION_TOOLKIT_ACTIVATE = 'vision_toolkit_activate'
@@ -139,17 +139,21 @@ export class VisionToolExposure {
139
139
 
140
140
  /**
141
141
  * @param ctx - Plugin context with Tool and Agent registries.
142
- * @param createTools - Fresh definitions bound to the current runtime generation.
142
+ * @param createTools - Fresh definitions bound to the current runtime
143
+ * generation, filtered by a tool-visibility snapshot.
144
+ * @param resolveVisibility - Live resolver for the current tool-visibility
145
+ * snapshot; read once per activation (a session-head snapshot).
143
146
  */
144
147
  constructor(
145
148
  private readonly ctx: Context,
146
- private readonly createTools: () => ToolDefinition[],
149
+ private readonly createTools: (snapshot: ToolVisibility) => ToolDefinition[],
150
+ private readonly resolveVisibility: () => ToolVisibility = () => ({ local: true, online: true, video: false }),
147
151
  ) {
148
152
  this.activationTool = defineTool({
149
153
  name: VISION_TOOLKIT_ACTIVATE,
150
- description: `Activate the independent Vision Toolkit execution tools for this Agent: ${Object.values(VISION_TOOL_NAMES).join(', ')}. `
151
- + `Loading the ${VISION_SKILLS_NAME} Skill normally activates them automatically; call this once when the visual tools are still absent, then use them for image understanding, OCR, UI detection, and related tasks. `
152
- + 'It is safe to call before the Skill is loaded, and this activation tool disappears after success.',
154
+ description: `Report and (re)mount the Vision Toolkit execution tools for this Agent: the currently visible subset of ${Object.values(VISION_TOOL_NAMES).join(', ')} plus optional video understanding. `
155
+ + `The visual tool set is normally mounted automatically when the ${VISION_SKILLS_NAME} Skill loads; call this ONLY when the user explicitly asks you to refresh or reload the current session's vision tools, or to list which vision tools are currently available. `
156
+ + 'It restores the tool set to the latest Settings snapshot and returns the tool names actually mounted.',
153
157
  parameters: {},
154
158
  output: {
155
159
  schema: {
@@ -166,9 +170,12 @@ export class VisionToolExposure {
166
170
  if (exec.agent === undefined) {
167
171
  throw new Error(`${VISION_TOOLKIT_ACTIVATE}: an Agent Session is required`)
168
172
  }
169
- return Promise.resolve(this.activate(exec.agent))
173
+ // An already-active Agent is reloaded (re-reads the latest Settings
174
+ // snapshot) rather than returning the stale set, so the tool genuinely
175
+ // refreshes the visible tool list on an explicit user request.
176
+ return Promise.resolve(this.reload(exec.agent))
170
177
  },
171
- presentCall: () => ({ card: 'generic', title: 'Activate vision tools', kind: 'execute' }),
178
+ presentCall: () => ({ card: 'generic', title: 'Refresh or list vision tools', kind: 'execute' }),
172
179
  })
173
180
  }
174
181
 
@@ -222,7 +229,7 @@ export class VisionToolExposure {
222
229
  if (state === undefined) throw new Error(`dsh-vision-toolkit: Agent ${String(agent.id)} has no exposure state`)
223
230
  if (state.active) return { activated: false, tools: [...state.toolNames] }
224
231
 
225
- const definitions = this.createTools()
232
+ const definitions = this.createTools(this.resolveVisibility())
226
233
  const toolDisposers: Array<() => void> = []
227
234
  try {
228
235
  for (const definition of definitions) toolDisposers.push(agent.ctx.tools.register(definition))
@@ -241,6 +248,24 @@ export class VisionToolExposure {
241
248
  return { activated: true, tools: [...state.toolNames] }
242
249
  }
243
250
 
251
+ /**
252
+ * Explicit refresh/reload from the bootstrap tool. An inactive Agent is
253
+ * mounted; an active Agent is torn down and re-mounted from the *current*
254
+ * Settings snapshot, so the returned tool list reflects the latest
255
+ * `toolVisibility` instead of the stale activation-time set.
256
+ */
257
+ private reload(agent: Agent): VisionToolkitActivationResult {
258
+ this.attach(agent)
259
+ const state = this.states.get(agent)
260
+ if (state === undefined) return { activated: false, tools: [] }
261
+ if (!state.active) return this.activate(agent)
262
+ // Dispose the current tool generation (including the hide-restriction on
263
+ // the bootstrap gauntlet) and rebuild from the fresh snapshot.
264
+ this.disposeState(state)
265
+ this.states.set(agent, { active: false, toolDisposers: [], toolNames: [] })
266
+ return this.activate(agent)
267
+ }
268
+
244
269
  /** Whether the session is attached to the live SessionStore (production). */
245
270
  private isLiveSession(session: Session): boolean {
246
271
  return this.ctx.sessions.get(session.id) === session
package/src/index.ts CHANGED
@@ -83,11 +83,16 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
83
83
 
84
84
  const ensureOperational = (): void => {
85
85
  if (!manager.ready || operationalDisposers !== undefined) return
86
- const exposure = new VisionToolExposure(ctx, () => createVisionTools(
87
- () => manager.current(),
88
- value => artifacts.presentationMeta(value),
89
- lifecycle.signal,
90
- ))
86
+ const exposure = new VisionToolExposure(
87
+ ctx,
88
+ (visibility) => createVisionTools(
89
+ () => manager.current(),
90
+ value => artifacts.presentationMeta(value),
91
+ lifecycle.signal,
92
+ visibility,
93
+ ),
94
+ () => manager.currentConfig().toolVisibility,
95
+ )
91
96
  let activationTool: (() => void) | undefined
92
97
  let exposureDisposer: (() => void) | undefined
93
98
  let skill: (() => void) | undefined
package/src/paths.ts CHANGED
@@ -16,6 +16,12 @@ import { VisionToolkitError } from './errors.ts'
16
16
  /** Supported input image extensions (the upstream client's allowlist). */
17
17
  export const SUPPORTED_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp'] as const
18
18
 
19
+ /** Supported input video extensions probed with the bundled ffprobe binary. */
20
+ export const SUPPORTED_VIDEO_EXTENSIONS = [
21
+ '.mp4', '.mov', '.webm', '.mkv', '.avi', '.m4v',
22
+ '.mpg', '.mpeg', '.wmv', '.flv', '.ts', '.m2ts', '.3gp',
23
+ ] as const
24
+
19
25
  /** Resolved path policy for one tool invocation. */
20
26
  export interface PathPolicy {
21
27
  /** Real workspace root. */
@@ -399,6 +405,11 @@ export function resolveHtmlFile(raw: string, policy: PathPolicy): Promise<{ path
399
405
  return resolveAuthorizedFile(raw, policy, ['.html', '.htm'], 'HTML source')
400
406
  }
401
407
 
408
+ /** Validate one input video path against the video extension allowlist. */
409
+ export function resolveInputVideo(raw: string, policy: PathPolicy): Promise<{ path: string; bytes: number }> {
410
+ return resolveAuthorizedFile(raw, policy, SUPPORTED_VIDEO_EXTENSIONS, 'video')
411
+ }
412
+
402
413
  /**
403
414
  * Resolve an optional user-supplied output filename inside the plugin output
404
415
  * directory. Absolute paths, `..` segments, and wrong extensions are rejected.