@anionex/dsh-vision-toolkit 0.1.12 → 0.1.14

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 (64) hide show
  1. package/README.i18n.yaml +3 -3
  2. package/README.md +207 -346
  3. package/README.zh.md +212 -360
  4. package/docs/requirements-traceability/README.i18n.yaml +2 -2
  5. package/docs/requirements-traceability/README.md +1 -1
  6. package/docs/requirements-traceability/README.zh.md +1 -1
  7. package/lib/client.js +173 -7
  8. package/lib/client.js.map +1 -1
  9. package/lib/config.js +4 -0
  10. package/lib/config.js.map +1 -1
  11. package/lib/defaults.js +1 -1
  12. package/lib/defaults.js.map +1 -1
  13. package/lib/plugin-update.js +1011 -0
  14. package/lib/plugin-update.js.map +1 -0
  15. package/lib/runtime-install.js +61 -1
  16. package/lib/runtime-install.js.map +1 -1
  17. package/lib/runtime.js +20 -3
  18. package/lib/runtime.js.map +1 -1
  19. package/lib/skill.js +7 -3
  20. package/lib/skill.js.map +1 -1
  21. package/lib/tools.js +3 -2
  22. package/lib/tools.js.map +1 -1
  23. package/lib/types/client/index.d.ts +55 -1
  24. package/lib/types/client/index.d.ts.map +1 -1
  25. package/lib/types/config.d.ts.map +1 -1
  26. package/lib/types/defaults.d.ts +1 -1
  27. package/lib/types/defaults.d.ts.map +1 -1
  28. package/lib/types/plugin-update.d.ts +111 -0
  29. package/lib/types/plugin-update.d.ts.map +1 -0
  30. package/lib/types/runtime-install.d.ts +11 -0
  31. package/lib/types/runtime-install.d.ts.map +1 -1
  32. package/lib/types/runtime.d.ts +3 -0
  33. package/lib/types/runtime.d.ts.map +1 -1
  34. package/lib/types/skill.d.ts +1 -1
  35. package/lib/types/skill.d.ts.map +1 -1
  36. package/lib/types/tools.d.ts.map +1 -1
  37. package/lib/types/upstream.d.ts +1 -0
  38. package/lib/types/upstream.d.ts.map +1 -1
  39. package/lib/types/web.d.ts +13 -1
  40. package/lib/types/web.d.ts.map +1 -1
  41. package/lib/upstream.js +20 -8
  42. package/lib/upstream.js.map +1 -1
  43. package/lib/web.js +46 -7
  44. package/lib/web.js.map +1 -1
  45. package/package.json +12 -14
  46. package/src/client/index.tsx +229 -6
  47. package/src/config.ts +4 -0
  48. package/src/defaults.ts +1 -1
  49. package/src/plugin-update.ts +1142 -0
  50. package/src/runtime-install.ts +77 -1
  51. package/src/runtime.ts +23 -3
  52. package/src/skill.ts +7 -3
  53. package/src/tools.ts +4 -2
  54. package/src/upstream.ts +21 -8
  55. package/src/web.ts +69 -3
  56. package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +3 -3
  57. package/vendor/agent-vision-toolkit/bin/crop +0 -0
  58. package/vendor/agent-vision-toolkit/bin/detect +0 -0
  59. package/vendor/agent-vision-toolkit/bin/glance +0 -0
  60. package/vendor/agent-vision-toolkit/bin/ground +0 -0
  61. package/vendor/agent-vision-toolkit/bin/trace +0 -0
  62. package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/html_shot.py +323 -11
  63. package/vendor/agent-vision-toolkit/skills/vision-tools/scripts/long_screenshot_ocr.py +0 -0
  64. /package/assets/{hero.png → hero-v2.png} +0 -0
@@ -300,6 +300,69 @@ function venvPython(root: string): string {
300
300
  return process.platform === 'win32' ? join(root, 'Scripts', 'python.exe') : join(root, 'bin', 'python')
301
301
  }
302
302
 
303
+ /**
304
+ * Rewrite a staged venv's `pyvenv.cfg` `home`/`executable` to point at a given
305
+ * base directory (the app execution alias directory for the Microsoft Store
306
+ * Python). Pure helper so the transformation is testable cross-platform.
307
+ */
308
+ export function rewriteVenvConfig(cfg: string, homeDir: string): string {
309
+ return cfg
310
+ .replace(/^home = .*$/m, `home = ${homeDir}`)
311
+ .replace(/^executable = .*$/m, `executable = ${homeDir}\\python.exe`)
312
+ }
313
+
314
+ /**
315
+ * Build the Microsoft Store probe environment while preserving Python-variable
316
+ * tombstones; only the user-directory variables must fall back to the host.
317
+ */
318
+ export function storePythonProbeEnvironment(installEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
319
+ return Object.fromEntries(
320
+ Object.entries(installEnv).filter(([key]) => key !== 'HOME' && key !== 'USERPROFILE' && key !== 'LOCALAPPDATA'),
321
+ )
322
+ }
323
+
324
+ /**
325
+ * Windows-only workaround for the Microsoft Store Python. `python -m venv`
326
+ * records `home = C:\Program Files\WindowsApps\...` in the new venv, but the
327
+ * venv launcher (venvlauncher.exe) cannot CreateProcess that `python.exe`
328
+ * directly — the AppModel package execution restriction denies it (error 5) —
329
+ * so the venv's pip bootstrap exits 101. Rewrite the staged venv's pyvenv.cfg
330
+ * to the app execution alias directory, whose `python.exe` is launchable
331
+ * through Store activation.
332
+ */
333
+ async function rewriteStorePythonVenvHome(
334
+ ctx: Context,
335
+ bootstrap: { command: RuntimeCommand },
336
+ staging: string,
337
+ installEnv: NodeJS.ProcessEnv,
338
+ cwd: string,
339
+ ): Promise<void> {
340
+ if (process.platform !== 'win32') return
341
+ // The HOME/USERPROFILE/LOCALAPPDATA overrides make the Store alias resolve to
342
+ // the real Program Files\WindowsApps path; without them sys.executable points
343
+ // back at the alias directory that venvlauncher can launch.
344
+ const probeEnv = storePythonProbeEnvironment(installEnv)
345
+ const probe = await runCollected(
346
+ ctx,
347
+ [bootstrap.command.program, ...bootstrap.command.prefix, '-c', 'import os,sys; print(os.path.dirname(sys.executable))'],
348
+ cwd,
349
+ { env: probeEnv },
350
+ )
351
+ if (probe.exitCode !== 0 || probe.timedOut) return
352
+ const aliasDir = probe.stdout.trim().split(/\r?\n/)[0]
353
+ if (aliasDir === undefined || aliasDir.length === 0) return
354
+ const cfgPath = join(staging, 'pyvenv.cfg')
355
+ let cfg: string
356
+ try {
357
+ cfg = await readFile(cfgPath, 'utf8')
358
+ } catch {
359
+ return
360
+ }
361
+ // Only touch the known-broken layout; other interpreters are left untouched.
362
+ if (!/^home = .*Program Files\\WindowsApps.*$/m.test(cfg)) return
363
+ await writeFile(cfgPath, rewriteVenvConfig(cfg, aliasDir))
364
+ }
365
+
303
366
  async function dependencyVersions(
304
367
  ctx: Context,
305
368
  python: RuntimeCommand,
@@ -543,13 +606,26 @@ async function prepareManaged(
543
606
  if (!created) {
544
607
  const create = await runCollected(
545
608
  ctx,
546
- [bootstrap.command.program, ...bootstrap.command.prefix, '-m', 'venv', staging],
609
+ [bootstrap.command.program, ...bootstrap.command.prefix, '-m', 'venv', '--without-pip', staging],
547
610
  stateRoot,
548
611
  { timeoutMs: PREPARE_TIMEOUT_MS, env: installEnv },
549
612
  )
550
613
  if (create.exitCode !== 0 || create.timedOut) {
551
614
  throw new VisionToolkitError('runtime', `Python failed to create the managed runtime: ${create.stderr.trim()}`)
552
615
  }
616
+ // `python -m venv` normally bootstraps pip internally; with the Microsoft
617
+ // Store Python that step exits 101 (see rewriteStorePythonVenvHome), so pip
618
+ // is installed explicitly after the venv configuration is repaired.
619
+ await rewriteStorePythonVenvHome(ctx, bootstrap, staging, installEnv, stateRoot)
620
+ const pipBootstrap = await runCollected(
621
+ ctx,
622
+ [venvPython(staging), '-m', 'ensurepip', '--upgrade', '--default-pip'],
623
+ stateRoot,
624
+ { timeoutMs: PREPARE_TIMEOUT_MS, env: installEnv },
625
+ )
626
+ if (pipBootstrap.exitCode !== 0 || pipBootstrap.timedOut) {
627
+ throw new VisionToolkitError('runtime', `Python failed to bootstrap pip in the managed runtime: ${pipBootstrap.stderr.trim()}`)
628
+ }
553
629
  const install = await runCollected(
554
630
  ctx,
555
631
  [venvPython(staging), '-m', 'pip', 'install', '--disable-pip-version-check', '--no-input', '-r', REQUIREMENTS_PATH],
package/src/runtime.ts CHANGED
@@ -411,6 +411,7 @@ export interface HtmlScreenshotRequest {
411
411
  height?: number
412
412
  scale?: number
413
413
  waitMs?: number
414
+ fullPage?: boolean
414
415
  output?: string
415
416
  }
416
417
 
@@ -421,6 +422,8 @@ export interface HtmlScreenshotResult {
421
422
  viewport: { width: number; height: number; scale: number }
422
423
  width: number
423
424
  height: number
425
+ /** Full document height in CSS pixels; present only for full-page captures. */
426
+ pageHeight?: number
424
427
  artifact: ArtifactDescriptor
425
428
  }
426
429
 
@@ -1631,6 +1634,7 @@ export class VisionToolkitRuntime {
1631
1634
  const height = integerInRange(request.height, 800, 1, 8192, 'html_screenshot.height')
1632
1635
  const scale = integerInRange(request.scale, 1, 1, 4, 'html_screenshot.scale')
1633
1636
  const waitMs = integerInRange(request.waitMs, 0, 0, 120000, 'html_screenshot.waitMs')
1637
+ const fullPage = request.fullPage === true
1634
1638
  const outputPixels = width * height * scale * scale
1635
1639
  if (!Number.isSafeInteger(outputPixels) || outputPixels > this.config.maxImagePixels) {
1636
1640
  throw new VisionToolkitError('capacity', `HTML screenshot would create ${outputPixels} pixels, exceeding maxImagePixels ${this.config.maxImagePixels}`)
@@ -1657,16 +1661,31 @@ export class VisionToolkitRuntime {
1657
1661
  String(scale),
1658
1662
  '--wait-ms',
1659
1663
  String(waitMs),
1664
+ ...(fullPage ? ['--full-page', '--max-pixels', String(this.config.maxImagePixels)] : []),
1660
1665
  ], operation)
1661
1666
  const parsed = parseHtmlScreenshotOutput(result.stdout)
1662
1667
  const expectedWidth = width * scale
1663
- const expectedHeight = height * scale
1664
- if (parsed.outputPath !== staged || parsed.width !== expectedWidth || parsed.height !== expectedHeight) {
1668
+ if (parsed.outputPath !== staged || parsed.width !== expectedWidth) {
1669
+ throw new VisionToolkitError('output', 'html_screenshot: upstream summary does not match the requested output')
1670
+ }
1671
+ if (fullPage !== (parsed.pageHeight !== undefined)) {
1672
+ throw new VisionToolkitError('output', 'html_screenshot: upstream full-page metadata does not match the request')
1673
+ }
1674
+ const pageHeight = parsed.pageHeight
1675
+ const expectedHeight = pageHeight === undefined ? height * scale : pageHeight * scale
1676
+ if (pageHeight !== undefined && (!Number.isInteger(pageHeight) || pageHeight <= 0)) {
1677
+ throw new VisionToolkitError('output', 'html_screenshot: upstream reported an invalid page height')
1678
+ }
1679
+ const outputPixels = expectedWidth * expectedHeight
1680
+ if (!Number.isSafeInteger(outputPixels) || outputPixels > this.config.maxImagePixels) {
1681
+ throw new VisionToolkitError('capacity', `HTML screenshot would create ${outputPixels} pixels, exceeding maxImagePixels ${this.config.maxImagePixels}`)
1682
+ }
1683
+ if (parsed.height !== expectedHeight) {
1665
1684
  throw new VisionToolkitError('output', 'html_screenshot: upstream summary does not match the requested output')
1666
1685
  }
1667
1686
  const generated = await this.probeGeneratedImage(staged, operation, 'html_screenshot')
1668
1687
  if (generated.format !== 'png' || generated.width !== expectedWidth || generated.height !== expectedHeight) {
1669
- throw new VisionToolkitError('output', 'html_screenshot: generated PNG dimensions do not match the viewport')
1688
+ throw new VisionToolkitError('output', 'html_screenshot: generated PNG dimensions do not match the reported output')
1670
1689
  }
1671
1690
  await commitStagedOutput(staged, finalPath, policy)
1672
1691
  const artifact = await describeArtifact(finalPath, policy, {
@@ -1682,6 +1701,7 @@ export class VisionToolkitRuntime {
1682
1701
  viewport: { width, height, scale },
1683
1702
  width: expectedWidth,
1684
1703
  height: expectedHeight,
1704
+ ...(pageHeight === undefined ? {} : { pageHeight }),
1685
1705
  artifact,
1686
1706
  }
1687
1707
  } finally {
package/src/skill.ts CHANGED
@@ -45,7 +45,7 @@ never enter the model tool set, before or after Skill activation.
45
45
  | Split and merge a tall screenshot OCR | vision_long_screenshot_ocr |
46
46
  | Extract an icon/logo on transparency | vision_extract_foreground |
47
47
  | Measure a palette or choose among exact colors | vision_dominant_colors |
48
- | Render a local HTML implementation to PNG | vision_html_screenshot |
48
+ | Render a local HTML implementation to PNG (viewport or full document) | vision_html_screenshot |
49
49
 
50
50
  vision_glance, vision_ground, vision_detect, and non-split long OCR send the
51
51
  validated image bytes to the external vision service configured by the user.
@@ -86,7 +86,7 @@ be opened or downloaded by the UI and passed to later tools.
86
86
  - vision_long_screenshot_ocr -> merged Markdown, manifest JSON, boundary audit,
87
87
  chunk PNGs, and OCR sidecars
88
88
  - vision_extract_foreground -> transparent PNG
89
- - vision_html_screenshot -> PNG
89
+ - vision_html_screenshot -> PNG (fullPage=true also reports the CSS pageHeight)
90
90
 
91
91
  ## Reliable workflows
92
92
 
@@ -109,7 +109,8 @@ request. Reuse the same runName with resume=true to retain matching sidecars.
109
109
  2. vision_dominant_colors for measured colors.
110
110
  3. vision_extract_foreground or vision_trace for reusable assets.
111
111
  4. Implement a local HTML file.
112
- 5. vision_html_screenshot with the target viewport.
112
+ 5. vision_html_screenshot with the target viewport; use fullPage=true when the
113
+ complete document is needed without guessing taller viewports.
113
114
  6. vision_pixel_diff against the reference.
114
115
  7. Inspect the worst region and iterate until the measured differences are
115
116
  acceptable.
@@ -127,6 +128,9 @@ vision_glance with ocr=true when the text content matters.
127
128
  invent nested or absolute output paths.
128
129
  - vision_html_screenshot accepts local .html/.htm files only, not URLs or data
129
130
  URIs.
131
+ - vision_html_screenshot keeps the requested viewport for layout; with
132
+ fullPage=true, it captures the complete document and returns pageHeight
133
+ in CSS pixels.
130
134
  - Disabling or unloading the plugin cancels its active visual operations before
131
135
  unregistering the native tools and skill.
132
136
  - Do not infer image contents after a tool error. Fix the path, limits,
package/src/tools.ts CHANGED
@@ -540,7 +540,7 @@ export function createVisionTools(
540
540
  + WORKSPACE_NOTE,
541
541
  parameters: {
542
542
  source: { type: 'string', required: true, description: 'Local HTML path only.' }, width: { type: 'integer' }, height: { type: 'integer' },
543
- scale: { type: 'integer' }, waitMs: { type: 'integer' }, output: { type: 'string', description: 'Artifact filename; .png only.' },
543
+ scale: { type: 'integer' }, waitMs: { type: 'integer' }, fullPage: { type: 'boolean', description: 'Capture the full document height while preserving the requested viewport.' }, output: { type: 'string', description: 'Artifact filename; .png only.' },
544
544
  timeoutMs: { type: 'integer', description: TIMEOUT_NOTE },
545
545
  },
546
546
  output: {
@@ -548,7 +548,7 @@ export function createVisionTools(
548
548
  type: 'object', additionalProperties: false, properties: {
549
549
  sourcePath: { type: 'string', required: true }, sourceBytes: { type: 'integer', required: true },
550
550
  viewport: { type: 'object', additionalProperties: false, required: true, properties: { width: { type: 'integer', required: true }, height: { type: 'integer', required: true }, scale: { type: 'integer', required: true } } },
551
- width: { type: 'integer', required: true }, height: { type: 'integer', required: true }, artifact: requiredArtifactSchema,
551
+ width: { type: 'integer', required: true }, height: { type: 'integer', required: true }, pageHeight: { type: 'integer' }, artifact: requiredArtifactSchema,
552
552
  },
553
553
  },
554
554
  render: renderJson,
@@ -559,6 +559,7 @@ export function createVisionTools(
559
559
  source: args.source,
560
560
  ...(args.width === undefined ? {} : { width: args.width }), ...(args.height === undefined ? {} : { height: args.height }),
561
561
  ...(args.scale === undefined ? {} : { scale: args.scale }), ...(args.waitMs === undefined ? {} : { waitMs: args.waitMs }),
562
+ ...(args.fullPage === undefined ? {} : { fullPage: args.fullPage }),
562
563
  ...(args.output === undefined ? {} : { output: args.output }),
563
564
  }
564
565
  return runtimeFrom(source).htmlScreenshot(request, callOptions(exec, args.timeoutMs, lifecycleSignal))
@@ -663,6 +664,7 @@ interface HtmlArgs {
663
664
  height?: number
664
665
  scale?: number
665
666
  waitMs?: number
667
+ fullPage?: boolean
666
668
  output?: string
667
669
  timeoutMs?: number
668
670
  }
package/src/upstream.ts CHANGED
@@ -165,6 +165,7 @@ export interface HtmlScreenshotOutput {
165
165
  outputPath: string
166
166
  width: number
167
167
  height: number
168
+ pageHeight?: number
168
169
  }
169
170
 
170
171
  const BOX_SUFFIX = /x1:\s*(\d+),\s*y1:\s*(\d+),\s*x2:\s*(\d+),\s*y2:\s*(\d+)\s*$/
@@ -448,9 +449,14 @@ export function parseDominantColorsOutput(stdout: string): DominantColorsOutput
448
449
 
449
450
  /** Parse the local Chrome screenshot summary. */
450
451
  export function parseHtmlScreenshotOutput(stdout: string): HtmlScreenshotOutput {
451
- const wrote = /^wrote\s+(.+?)\s+\((\d+)x(\d+)\)\s*$/.exec(stdout.trim())
452
+ const wrote = /^wrote\s+(.+?)\s+\((\d+)x(\d+)(?:;\s*pageHeight=(\d+))?\)\s*$/.exec(stdout.trim())
452
453
  if (wrote === null) throw new VisionToolkitError('output', 'html_screenshot: upstream did not report a written PNG')
453
- return { outputPath: wrote[1] ?? '', width: Number(wrote[2]), height: Number(wrote[3]) }
454
+ return {
455
+ outputPath: wrote[1] ?? '',
456
+ width: Number(wrote[2]),
457
+ height: Number(wrote[3]),
458
+ ...(wrote[4] === undefined ? {} : { pageHeight: Number(wrote[4]) }),
459
+ }
454
460
  }
455
461
 
456
462
  const REQUIRED_TOOLS = ['glance', 'ground', 'detect', 'crop', 'trace'] as const
@@ -527,20 +533,24 @@ const VISION_MODEL_GUARD = [
527
533
  ].join('\n')
528
534
 
529
535
  const HTML_SCREENSHOT_GUARD = [
530
- 'import runpy,subprocess,sys,tempfile',
536
+ 'import os,runpy,subprocess,sys,tempfile',
531
537
  'script=sys.argv[1]',
532
538
  'sys.argv=[script,*sys.argv[2:]]',
533
- 'original_run=subprocess.run',
539
+ 'original_popen=subprocess.Popen',
534
540
  'with tempfile.TemporaryDirectory(prefix="dsh-vision-chrome-") as profile:',
535
- ' def guarded_run(command,*args,**kwargs):',
541
+ ' original_profile=os.environ.get("DSH_VISION_CHROME_PROFILE")',
542
+ ' os.environ["DSH_VISION_CHROME_PROFILE"]=profile',
543
+ ' def guarded_popen(command,*args,**kwargs):',
536
544
  ' command=list(command)',
537
545
  ' command[1:1]=["--use-mock-keychain",f"--user-data-dir={profile}","--incognito","--disable-background-networking","--proxy-server=http://127.0.0.1:9","--proxy-bypass-list=<-loopback>"]',
538
- ' return original_run(command,*args,**kwargs)',
539
- ' subprocess.run=guarded_run',
546
+ ' return original_popen(command,*args,**kwargs)',
547
+ ' subprocess.Popen=guarded_popen',
540
548
  ' try:',
541
549
  ' runpy.run_path(script,run_name="__main__")',
542
550
  ' finally:',
543
- ' subprocess.run=original_run',
551
+ ' subprocess.Popen=original_popen',
552
+ ' if original_profile is None: os.environ.pop("DSH_VISION_CHROME_PROFILE",None)',
553
+ ' else: os.environ["DSH_VISION_CHROME_PROFILE"]=original_profile',
544
554
  ].join('\n')
545
555
 
546
556
  const LONG_OCR_PINNED_GLANCE = [
@@ -853,6 +863,9 @@ export class UpstreamAdapter {
853
863
  if (/Missing config VISION_/i.test(result.stderr)) {
854
864
  return new VisionToolkitError('config', message)
855
865
  }
866
+ if (/maxImagePixels|exceed(?:s|ing).*pixels/i.test(result.stderr)) {
867
+ return new VisionToolkitError('capacity', message)
868
+ }
856
869
  if (/not found|only PNG|unsupported|cannot open|empty region|must be|expects|invalid colour|needs at least/i.test(result.stderr)) {
857
870
  return new VisionToolkitError('input', message)
858
871
  }
package/src/web.ts CHANGED
@@ -28,6 +28,13 @@ import {
28
28
  type VisionToolkitConfig,
29
29
  } from './config.ts'
30
30
  import type { VisionToolkitHealthResult } from './runtime.ts'
31
+ import {
32
+ PluginUpdateError,
33
+ VisionToolkitPluginUpdateService,
34
+ type PluginUpdateCapability,
35
+ type PluginUpdateCheck,
36
+ type PluginUpdateResult,
37
+ } from './plugin-update.ts'
31
38
  import {
32
39
  VisionToolkitRuntimeManager,
33
40
  type PreparedRuntimeGeneration,
@@ -62,6 +69,7 @@ export interface VisionToolkitSettingsSnapshot {
62
69
  upstreamRepository: string
63
70
  upstreamVersion: string
64
71
  upstreamCommit: string
72
+ update: PluginUpdateCapability
65
73
  }
66
74
  artifactRouteAvailable: boolean
67
75
  }
@@ -85,7 +93,16 @@ interface CredentialRequest {
85
93
  value: string
86
94
  }
87
95
 
88
- type SettingsRequest = SaveRequest | HealthRequest | CredentialRequest
96
+ interface CheckUpdateRequest {
97
+ action: 'check-update'
98
+ }
99
+
100
+ interface ApplyUpdateRequest {
101
+ action: 'apply-update'
102
+ expectedVersion: string
103
+ }
104
+
105
+ type SettingsRequest = SaveRequest | HealthRequest | CredentialRequest | CheckUpdateRequest | ApplyUpdateRequest
89
106
 
90
107
  interface JsonError {
91
108
  ok: false
@@ -109,6 +126,14 @@ export interface WebRuntimeManager {
109
126
  status(): RuntimeManagerStatus
110
127
  }
111
128
 
129
+ /** Minimal self-update face used by the Web route and its tests. */
130
+ export interface WebPluginUpdater {
131
+ configureWebServer?(host: string, port: number): void
132
+ capability(): Promise<PluginUpdateCapability>
133
+ check(): Promise<PluginUpdateCheck>
134
+ installAndRestart(expectedVersion: string): Promise<PluginUpdateResult>
135
+ }
136
+
112
137
  /** Callback invoked when a Settings save makes the first runtime available. */
113
138
  export type RuntimeActivated = () => void
114
139
 
@@ -195,6 +220,13 @@ function parseRequest(value: unknown): SettingsRequest {
195
220
  value: secret,
196
221
  }
197
222
  }
223
+ if (value.action === 'check-update') return { action: 'check-update' }
224
+ if (value.action === 'apply-update') {
225
+ if (typeof value.expectedVersion !== 'string' || value.expectedVersion.trim().length === 0) {
226
+ throw new TypeError('apply-update.expectedVersion must be a non-empty string')
227
+ }
228
+ return { action: 'apply-update', expectedVersion: value.expectedVersion.trim() }
229
+ }
198
230
  throw new TypeError(`unsupported action: ${value.action}`)
199
231
  }
200
232
 
@@ -205,12 +237,24 @@ function publicMessage(error: unknown): string {
205
237
 
206
238
  /** Same-origin Settings and health handler. */
207
239
  export class VisionToolkitWebBackend {
240
+ private readonly updater: WebPluginUpdater
241
+
208
242
  constructor(
209
243
  private readonly ctx: Context,
210
244
  private readonly manager: WebRuntimeManager,
211
245
  private readonly artifacts: ArtifactAccessController,
212
246
  private readonly onRuntimeActivated: RuntimeActivated,
213
- ) {}
247
+ updater?: WebPluginUpdater,
248
+ ) {
249
+ this.updater = updater ?? new VisionToolkitPluginUpdateService(ctx, PLUGIN_VERSION, {
250
+ runtimeReady: () => this.manager.status().ready,
251
+ })
252
+ }
253
+
254
+ /** Supply the active listener address before the Settings route becomes reachable. */
255
+ configureWebServer(host: string, port: number): void {
256
+ this.updater.configureWebServer?.(host, port)
257
+ }
214
258
 
215
259
  private async credential(config: ResolvedVisionToolkitConfig): Promise<CredentialInfo> {
216
260
  if (isBuiltInFreeVisionProvider(config.provider)) {
@@ -225,6 +269,7 @@ export class VisionToolkitWebBackend {
225
269
  const value = descriptor.value as VisionToolkitConfig
226
270
  const resolved = resolveConfig(value)
227
271
  const credential = await this.credential(resolved)
272
+ const update = await this.updater.capability()
228
273
  return {
229
274
  schemaVersion: 1,
230
275
  writable: this.ctx.settings.writable,
@@ -247,6 +292,7 @@ export class VisionToolkitWebBackend {
247
292
  upstreamRepository: UPSTREAM_REPOSITORY,
248
293
  upstreamVersion: UPSTREAM_VERSION,
249
294
  upstreamCommit: UPSTREAM_COMMIT,
295
+ update,
250
296
  },
251
297
  artifactRouteAvailable: this.artifacts.routeAvailable,
252
298
  }
@@ -350,20 +396,39 @@ export class VisionToolkitWebBackend {
350
396
  case 'credential':
351
397
  responseJson(res, 200, { ok: true, value: await this.saveCredential(parsed) })
352
398
  break
399
+ case 'check-update':
400
+ responseJson(res, 200, { ok: true, value: await this.updater.check() })
401
+ break
402
+ case 'apply-update':
403
+ responseJson(res, 200, { ok: true, value: await this.updater.installAndRestart(parsed.expectedVersion) })
404
+ break
353
405
  }
354
406
  } catch (error) {
355
407
  const settingsConflict = error instanceof SettingsConflictError
356
408
  const credentialConflict = error instanceof CredentialReferenceConflictError
409
+ const updateError = error instanceof PluginUpdateError
357
410
  const code = settingsConflict
358
411
  ? 'settings-conflict'
359
412
  : credentialConflict
360
413
  ? 'credential-conflict'
414
+ : updateError
415
+ ? error.code
361
416
  : parsed.action === 'health'
362
417
  ? 'health-failed'
363
418
  : parsed.action === 'credential'
364
419
  ? 'credential-rejected'
365
420
  : 'settings-rejected'
366
- const status = settingsConflict || credentialConflict ? 409 : parsed.action === 'health' ? 503 : 400
421
+ const updateConflict = updateError && ['update-in-progress', 'update-stale', 'update-unavailable', 'already-current'].includes(error.code)
422
+ const updateGateway = updateError && error.code === 'update-check-failed'
423
+ const status = settingsConflict || credentialConflict || updateConflict
424
+ ? 409
425
+ : parsed.action === 'health'
426
+ ? 503
427
+ : updateGateway
428
+ ? 502
429
+ : updateError
430
+ ? 500
431
+ : 400
367
432
  this.ctx.logger.warn('dsh-vision-toolkit Web action=%s failed: %s', parsed.action, publicMessage(error))
368
433
  requestError(res, status, code, publicMessage(error))
369
434
  }
@@ -454,6 +519,7 @@ export function installVisionToolkitWeb(
454
519
  ): void {
455
520
  ctx.inject(['webServer'], (webCtx) => {
456
521
  webCtx.effect(() => {
522
+ backend.configureWebServer(webCtx.webServer.host, webCtx.webServer.port)
457
523
  const detach = artifacts.attachRoute()
458
524
  const disposeArtifact = webCtx.webServer.register({
459
525
  kind: 'prefix',
@@ -3,7 +3,7 @@
3
3
  "repository": "https://github.com/Anionex/agent-vision-toolkit",
4
4
  "version": "v0.1.0+snapshot.bc9803d",
5
5
  "commit": "bc9803d7d6300c864d17460ecbb33540b26638e0",
6
- "contentSha256": "6936f8fba3ed40f54267245e717603c2622649ce4bd7ccf3c1b23f39047b1f3f",
6
+ "contentSha256": "aee26acedf76b083addc31acfc8817eb2a2044d144b495f4437adc9e20d18750",
7
7
  "files": [
8
8
  {
9
9
  "path": "CHANGELOG.md",
@@ -67,8 +67,8 @@
67
67
  },
68
68
  {
69
69
  "path": "skills/vision-tools/scripts/html_shot.py",
70
- "bytes": 4317,
71
- "sha256": "22f796dc6afaa791bbd9acc9bf87bc4914c4157e726f0132f7465f0e3d4d2894"
70
+ "bytes": 17573,
71
+ "sha256": "242e62e5d8bc02c4a707c57fe782cefb7f55abc0652792c1881a3f5d36c9302f"
72
72
  },
73
73
  {
74
74
  "path": "skills/vision-tools/scripts/long_screenshot_ocr.py",
File without changes
File without changes
File without changes
File without changes
File without changes