@mengruo/dsh-vision-toolkit 0.0.1 → 0.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 (51) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +17 -42
  3. package/README.zh.md +16 -41
  4. package/assets/logo_eapi_dark.png +0 -0
  5. package/docs/aihubmix-gemini-vision.i18n.yaml +2 -2
  6. package/docs/aihubmix-gemini-vision.md +2 -2
  7. package/docs/aihubmix-gemini-vision.zh.md +2 -2
  8. package/lib/client.js +246 -68
  9. package/lib/client.js.map +1 -1
  10. package/lib/config.js +143 -24
  11. package/lib/config.js.map +1 -1
  12. package/lib/evidence-cache.js +14 -0
  13. package/lib/evidence-cache.js.map +1 -1
  14. package/lib/image-input-variants.js +39 -27
  15. package/lib/image-input-variants.js.map +1 -1
  16. package/lib/runtime-install.js +204 -19
  17. package/lib/runtime-install.js.map +1 -1
  18. package/lib/runtime.js +310 -154
  19. package/lib/runtime.js.map +1 -1
  20. package/lib/tools.js +1 -0
  21. package/lib/tools.js.map +1 -1
  22. package/lib/types/client/index.d.ts +45 -3
  23. package/lib/types/client/index.d.ts.map +1 -1
  24. package/lib/types/config.d.ts +53 -0
  25. package/lib/types/config.d.ts.map +1 -1
  26. package/lib/types/evidence-cache.d.ts.map +1 -1
  27. package/lib/types/image-input-variants.d.ts +1 -6
  28. package/lib/types/image-input-variants.d.ts.map +1 -1
  29. package/lib/types/runtime-install.d.ts +21 -0
  30. package/lib/types/runtime-install.d.ts.map +1 -1
  31. package/lib/types/runtime.d.ts +45 -10
  32. package/lib/types/runtime.d.ts.map +1 -1
  33. package/lib/types/tools.d.ts.map +1 -1
  34. package/lib/types/upstream.d.ts +3 -0
  35. package/lib/types/upstream.d.ts.map +1 -1
  36. package/lib/types/web.d.ts +8 -0
  37. package/lib/types/web.d.ts.map +1 -1
  38. package/lib/upstream.js +11 -6
  39. package/lib/upstream.js.map +1 -1
  40. package/lib/web.js +50 -7
  41. package/lib/web.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/client/index.tsx +331 -88
  44. package/src/config.ts +210 -28
  45. package/src/evidence-cache.ts +14 -0
  46. package/src/image-input-variants.ts +39 -27
  47. package/src/runtime-install.ts +231 -20
  48. package/src/runtime.ts +333 -180
  49. package/src/tools.ts +1 -0
  50. package/src/upstream.ts +15 -8
  51. package/src/web.ts +67 -8
package/src/tools.ts CHANGED
@@ -97,6 +97,7 @@ const imageInfoSchema = {
97
97
  width: { type: 'integer', required: true },
98
98
  height: { type: 'integer', required: true },
99
99
  format: { type: 'string', required: true },
100
+ hasAlpha: { type: 'boolean', required: true, description: 'Whether the analyzed image carries an alpha (transparency) channel.' },
100
101
  originalPath: { type: 'string', required: true, description: 'Original image path before automatic compression.' },
101
102
  },
102
103
  } as const satisfies ValueSchemaSpec
package/src/upstream.ts CHANGED
@@ -176,6 +176,8 @@ export interface CompressedImageInfo {
176
176
  height: number
177
177
  format: 'png' | 'jpeg' | 'gif' | 'webp'
178
178
  mode: string
179
+ /** True when the saved image carries an alpha (transparency) channel. */
180
+ hasAlpha: boolean
179
181
  lossy: boolean
180
182
  resized: boolean
181
183
  candidate: string
@@ -614,7 +616,7 @@ const COMPRESS_IMAGE_SCRIPT = [
614
616
  ' if icc is not None: kwargs["icc_profile"]=icc',
615
617
  ' im.save(path,format=fmt,**kwargs)',
616
618
  ' with Image.open(path) as saved:',
617
- ' return os.path.getsize(path),(saved.format or "unknown").lower(),saved.mode',
619
+ ' return os.path.getsize(path),(saved.format or "unknown").lower(),saved.mode,saved.mode in ("RGBA","LA") or (saved.mode=="P" and "transparency" in saved.info)',
618
620
  'def has_alpha(im):',
619
621
  ' return im.mode in ("RGBA","LA") or (im.mode=="P" and "transparency" in im.info)',
620
622
  'def flatten(im):',
@@ -672,11 +674,11 @@ const COMPRESS_IMAGE_SCRIPT = [
672
674
  ' candidates=[]',
673
675
  ' for label,fmt,lossy,saver in candidates:',
674
676
  ' try:',
675
- ' size,saved_fmt,mode=saver()',
677
+ ' size,saved_fmt,mode,has_alpha=saver()',
676
678
  ' except Exception:',
677
679
  ' continue',
678
680
  ' if ok(w,h,size):',
679
- ' print(json.dumps({"ok":True,"bytes":size,"width":w,"height":h,"format":fmt,"mode":mode,"lossy":lossy,"resized":step>0,"candidate":label,"source_animated":meta["animated"]}))',
681
+ ' print(json.dumps({"ok":True,"bytes":size,"width":w,"height":h,"format":fmt,"mode":mode,"has_alpha":has_alpha,"lossy":lossy,"resized":step>0,"candidate":label,"source_animated":meta["animated"]}))',
680
682
  ' sys.exit(0)',
681
683
  ' if best is None or size<best[0]:',
682
684
  ' best=(size,label,fmt,lossy,step>0)',
@@ -799,13 +801,15 @@ export class UpstreamAdapter {
799
801
  async probeImageSize(
800
802
  imagePath: string,
801
803
  options: { signal: AbortSignal },
802
- ): Promise<{ width: number; height: number; format: string; mode: string }> {
804
+ ): Promise<{ width: number; height: number; format: string; mode: string; hasAlpha: boolean }> {
803
805
  if (this.prepared === undefined) await this.prepare()
804
806
  const prepared = this.requirePrepared()
805
807
  const script = [
806
808
  'import json,sys',
807
809
  'from PIL import Image',
808
- 'with Image.open(sys.argv[1]) as im: print(json.dumps({"width":im.width,"height":im.height,"format":str(im.format or "unknown").lower(),"mode":str(im.mode)}))',
810
+ 'with Image.open(sys.argv[1]) as im:',
811
+ ' has_alpha = im.mode in ("RGBA","LA") or (im.mode=="P" and "transparency" in im.info)',
812
+ ' print(json.dumps({"width":im.width,"height":im.height,"format":str(im.format or "unknown").lower(),"mode":str(im.mode),"has_alpha":has_alpha}))',
809
813
  ].join('\n')
810
814
  let handle: SubprocessHandle
811
815
  try {
@@ -829,18 +833,19 @@ export class UpstreamAdapter {
829
833
  throw new VisionToolkitError('input', `cannot decode image: ${outcome.stderr.trim() || 'unsupported or corrupt file'}`)
830
834
  }
831
835
  try {
832
- const parsed = JSON.parse(outcome.stdout) as { width?: unknown; height?: unknown; format?: unknown; mode?: unknown }
836
+ const parsed = JSON.parse(outcome.stdout) as { width?: unknown; height?: unknown; format?: unknown; mode?: unknown; has_alpha?: unknown }
833
837
  if (
834
838
  typeof parsed.width !== 'number'
835
839
  || typeof parsed.height !== 'number'
836
840
  || typeof parsed.format !== 'string'
837
841
  || typeof parsed.mode !== 'string'
842
+ || typeof parsed.has_alpha !== 'boolean'
838
843
  || !Number.isInteger(parsed.width)
839
844
  || !Number.isInteger(parsed.height)
840
845
  || parsed.width <= 0
841
846
  || parsed.height <= 0
842
847
  ) throw new Error('invalid dimensions')
843
- return { width: parsed.width, height: parsed.height, format: parsed.format, mode: parsed.mode }
848
+ return { width: parsed.width, height: parsed.height, format: parsed.format, mode: parsed.mode, hasAlpha: parsed.has_alpha }
844
849
  } catch (error) {
845
850
  throw new VisionToolkitError('output', 'cannot read image dimensions: unexpected Python output', { cause: error })
846
851
  }
@@ -884,7 +889,7 @@ export class UpstreamAdapter {
884
889
  const detail = typeof record.error === 'string' ? record.error : 'compression failed'
885
890
  throw new VisionToolkitError('capacity', `cannot compress image under ${maxBytes} bytes: ${detail}`)
886
891
  }
887
- const { bytes, width, height, format, mode, lossy, resized, candidate, source_animated } = record
892
+ const { bytes, width, height, format, mode, has_alpha, lossy, resized, candidate, source_animated } = record
888
893
  if (
889
894
  typeof bytes !== 'number' || !Number.isInteger(bytes) || bytes < 1 || bytes > maxBytes
890
895
  || typeof width !== 'number' || !Number.isInteger(width) || width < 1
@@ -892,6 +897,7 @@ export class UpstreamAdapter {
892
897
  || width * height > maxPixels
893
898
  || typeof format !== 'string' || !COMPRESSED_FORMATS.has(format)
894
899
  || typeof mode !== 'string' || mode.length === 0
900
+ || typeof has_alpha !== 'boolean'
895
901
  || typeof lossy !== 'boolean'
896
902
  || typeof resized !== 'boolean'
897
903
  || typeof candidate !== 'string' || candidate.length === 0
@@ -905,6 +911,7 @@ export class UpstreamAdapter {
905
911
  height,
906
912
  format: format as CompressedImageInfo['format'],
907
913
  mode,
914
+ hasAlpha: has_alpha,
908
915
  lossy,
909
916
  resized,
910
917
  candidate,
package/src/web.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  resolveConfig,
25
25
  isBuiltInFreeVisionProvider,
26
26
  VISION_TOOLKIT_SETTINGS_NAMESPACE,
27
+ type ResolvedProvider,
27
28
  type ResolvedVisionToolkitConfig,
28
29
  type VisionToolkitConfig,
29
30
  } from './config.ts'
@@ -66,6 +67,13 @@ export interface VisionToolkitSettingsSnapshot {
66
67
  source?: string
67
68
  writable: boolean
68
69
  }
70
+ /** Per-provider credential states, aligned with `settings.value.providers` order. */
71
+ credentials: Array<{
72
+ ref: string
73
+ configured: boolean
74
+ source?: string
75
+ writable: boolean
76
+ }>
69
77
  runtime: RuntimeManagerStatus
70
78
  release: {
71
79
  pluginVersion: string
@@ -87,6 +95,8 @@ interface HealthRequest {
87
95
  action: 'health'
88
96
  testConnection: boolean
89
97
  testModel: boolean
98
+ /** 0-based provider index; when absent, the primary provider is tested. */
99
+ providerIndex?: number
90
100
  }
91
101
 
92
102
  interface CredentialRequest {
@@ -96,6 +106,11 @@ interface CredentialRequest {
96
106
  value: string
97
107
  }
98
108
 
109
+ interface DeleteCredentialRequest {
110
+ action: 'delete-credential'
111
+ ref: CredentialRef
112
+ }
113
+
99
114
  interface CheckUpdateRequest {
100
115
  action: 'check-update'
101
116
  }
@@ -105,7 +120,7 @@ interface ApplyUpdateRequest {
105
120
  expectedVersion: string
106
121
  }
107
122
 
108
- type SettingsRequest = SaveRequest | HealthRequest | CredentialRequest | CheckUpdateRequest | ApplyUpdateRequest
123
+ type SettingsRequest = SaveRequest | HealthRequest | CredentialRequest | DeleteCredentialRequest | CheckUpdateRequest | ApplyUpdateRequest
109
124
 
110
125
  interface JsonError {
111
126
  ok: false
@@ -189,7 +204,16 @@ function parseRequest(value: unknown): SettingsRequest {
189
204
  const testModel = value.testModel === undefined ? false : value.testModel
190
205
  if (typeof testModel !== 'boolean') throw new TypeError('health.testModel must be boolean')
191
206
  if (testModel && !value.testConnection) throw new TypeError('health.testModel requires health.testConnection')
192
- return { action: 'health', testConnection: value.testConnection, testModel }
207
+ const providerIndex = value.providerIndex
208
+ if (providerIndex !== undefined && (!Number.isSafeInteger(providerIndex) || (providerIndex as number) < 0)) {
209
+ throw new TypeError('health.providerIndex must be a non-negative integer')
210
+ }
211
+ return {
212
+ action: 'health',
213
+ testConnection: value.testConnection,
214
+ testModel,
215
+ ...(providerIndex === undefined ? {} : { providerIndex: providerIndex as number }),
216
+ }
193
217
  }
194
218
  if (value.action === 'save') {
195
219
  if (!Number.isSafeInteger(value.expectedRevision) || (value.expectedRevision as number) < 0) {
@@ -223,6 +247,12 @@ function parseRequest(value: unknown): SettingsRequest {
223
247
  value: secret,
224
248
  }
225
249
  }
250
+ if (value.action === 'delete-credential') {
251
+ if (typeof value.ref !== 'string' || value.ref.trim().length === 0) {
252
+ throw new TypeError('delete-credential.ref must be a non-empty string')
253
+ }
254
+ return { action: 'delete-credential', ref: credentialRef(value.ref.trim()) }
255
+ }
226
256
  if (value.action === 'check-update') return { action: 'check-update' }
227
257
  if (value.action === 'apply-update') {
228
258
  if (typeof value.expectedVersion !== 'string' || value.expectedVersion.trim().length === 0) {
@@ -272,6 +302,18 @@ export class VisionToolkitWebBackend {
272
302
  const value = descriptor.value as VisionToolkitConfig
273
303
  const resolved = resolveConfig(value)
274
304
  const credential = await this.credential(resolved)
305
+ const credentials = await Promise.all(resolved.providers.map(async provider => {
306
+ if (isBuiltInFreeVisionProvider(provider)) {
307
+ return { ref: String(provider.credential), configured: true, source: 'built-in-free', writable: false }
308
+ }
309
+ const info = await this.ctx.credentials.describe(credentialRef(String(provider.credential)))
310
+ return {
311
+ ref: String(provider.credential),
312
+ configured: info.configured,
313
+ ...(info.source === undefined ? {} : { source: info.source }),
314
+ writable: info.writable,
315
+ }
316
+ }))
275
317
  const update = await this.updater.capability()
276
318
  return {
277
319
  schemaVersion: 1,
@@ -289,6 +331,7 @@ export class VisionToolkitWebBackend {
289
331
  ...(credential.source === undefined ? {} : { source: credential.source }),
290
332
  writable: credential.writable,
291
333
  },
334
+ credentials,
292
335
  runtime: this.manager.status(),
293
336
  release: {
294
337
  pluginVersion: PLUGIN_VERSION,
@@ -330,16 +373,21 @@ export class VisionToolkitWebBackend {
330
373
  )
331
374
  }
332
375
  const resolved = resolveConfig(descriptor.value as VisionToolkitConfig)
333
- const currentRef = credentialRef(String(resolved.provider.credential))
334
- if (currentRef !== request.ref) {
376
+ const provider = resolved.providers.find(entry => String(entry.credential) === String(request.ref))
377
+ if (provider === undefined) {
335
378
  throw new CredentialReferenceConflictError(
336
- `credential reference changed from "${request.ref}" to "${currentRef}"; reload Settings and try again`,
379
+ `credential reference "${String(request.ref)}" does not match any configured vision provider; reload Settings and try again`,
337
380
  )
338
381
  }
339
- if (isBuiltInFreeVisionProvider(resolved.provider)) {
382
+ if (isBuiltInFreeVisionProvider(provider)) {
340
383
  throw new Error('The built-in free vision provider does not accept a user API key')
341
384
  }
342
- await this.ctx.credentials.set(currentRef, request.value)
385
+ await this.ctx.credentials.set(request.ref, request.value)
386
+ return this.snapshot()
387
+ }
388
+
389
+ private async deleteCredential(request: DeleteCredentialRequest): Promise<VisionToolkitSettingsSnapshot> {
390
+ await this.ctx.credentials.unset(request.ref)
343
391
  return this.snapshot()
344
392
  }
345
393
 
@@ -351,12 +399,20 @@ export class VisionToolkitWebBackend {
351
399
  req.socket.once('close', abort)
352
400
  try {
353
401
  const runtime = this.manager.current()
402
+ let provider: ResolvedProvider | undefined
403
+ if (request.providerIndex !== undefined) {
404
+ const resolved = resolveConfig(descriptorOf(this.ctx).value as VisionToolkitConfig)
405
+ provider = resolved.providers[request.providerIndex]
406
+ if (provider === undefined) {
407
+ throw new Error(`provider index ${request.providerIndex} is out of range`)
408
+ }
409
+ }
354
410
  // Use the prepared runtime home instead of the host process cwd.
355
411
  return await runtime.health(request.testConnection, {
356
412
  signal: controller.signal,
357
413
  workspace: runtime.upstreamVersion.runtimeHome,
358
414
  sessionId: 'vision-toolkit-settings',
359
- }, request.testModel)
415
+ }, request.testModel, provider)
360
416
  } finally {
361
417
  req.off('aborted', abort)
362
418
  req.socket.off('close', abort)
@@ -401,6 +457,9 @@ export class VisionToolkitWebBackend {
401
457
  case 'credential':
402
458
  responseJson(res, 200, { ok: true, value: await this.saveCredential(parsed) })
403
459
  break
460
+ case 'delete-credential':
461
+ responseJson(res, 200, { ok: true, value: await this.deleteCredential(parsed) })
462
+ break
404
463
  case 'check-update':
405
464
  responseJson(res, 200, { ok: true, value: await this.updater.check() })
406
465
  break