@anionex/dsh-vision-toolkit 0.1.7 → 0.1.8
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.
- package/README.i18n.yaml +2 -2
- package/README.md +17 -1
- package/README.zh.md +17 -1
- package/lib/client.js +306 -16
- package/lib/client.js.map +1 -1
- package/lib/config.js +14 -0
- package/lib/config.js.map +1 -1
- package/lib/image-input-variants.js +615 -0
- package/lib/image-input-variants.js.map +1 -0
- package/lib/index.js +8 -1
- package/lib/index.js.map +1 -1
- package/lib/paste-images.js +6 -0
- package/lib/paste-images.js.map +1 -1
- package/lib/types/client/paste-images.d.ts +69 -0
- package/lib/types/client/paste-images.d.ts.map +1 -1
- package/lib/types/config.d.ts +26 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/image-input-variants.d.ts +153 -0
- package/lib/types/image-input-variants.d.ts.map +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/paste-images.d.ts +35 -0
- package/lib/types/paste-images.d.ts.map +1 -1
- package/lib/types/web-request.d.ts +7 -0
- package/lib/types/web-request.d.ts.map +1 -1
- package/lib/types/web.d.ts +17 -2
- package/lib/types/web.d.ts.map +1 -1
- package/lib/web-request.js +11 -2
- package/lib/web-request.js.map +1 -1
- package/lib/web.js +81 -3
- package/lib/web.js.map +1 -1
- package/package.json +2 -1
- package/src/client/paste-images.tsx +331 -15
- package/src/config.ts +40 -0
- package/src/image-input-variants.ts +688 -0
- package/src/index.ts +18 -1
- package/src/paste-images.ts +39 -0
- package/src/web-request.ts +12 -2
- package/src/web.ts +84 -2
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type VisionToolkitConfig,
|
|
22
22
|
} from './config.ts'
|
|
23
23
|
import { VisionToolExposure } from './exposure.ts'
|
|
24
|
+
import { createPasteTakeoverResolver, installImageInputVariants } from './image-input-variants.ts'
|
|
24
25
|
import { VisionToolkitRuntimeManager } from './runtime-manager.ts'
|
|
25
26
|
import { VISION_TOOLS_SKILL } from './skill.ts'
|
|
26
27
|
import { createVisionTools } from './tools.ts'
|
|
@@ -98,11 +99,27 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
|
|
|
98
99
|
const pastedImages = new PastedImageBackend(ctx, {
|
|
99
100
|
maxImageBytes: () => manager.status().activeConfig?.maxImageBytes ?? resolveConfig(settings.get()).maxImageBytes,
|
|
100
101
|
})
|
|
101
|
-
|
|
102
|
+
// Image-input variants register asynchronously once eligible routes exist;
|
|
103
|
+
// the runtime getter stays lazy so variants appear even when the runtime
|
|
104
|
+
// becomes ready after the first sweep.
|
|
105
|
+
const variants = installImageInputVariants(
|
|
106
|
+
ctx,
|
|
107
|
+
() => resolveConfig(settings.get()),
|
|
108
|
+
() => manager.ready ? manager.current() : undefined,
|
|
109
|
+
)
|
|
110
|
+
installVisionToolkitWeb(
|
|
111
|
+
ctx,
|
|
112
|
+
backend,
|
|
113
|
+
artifacts,
|
|
114
|
+
pastedImages,
|
|
115
|
+
createPasteTakeoverResolver(ctx, () => resolveConfig(settings.get())),
|
|
116
|
+
)
|
|
117
|
+
disposers.push(variants.dispose)
|
|
102
118
|
disposers.push(settings.watch(async (next) => {
|
|
103
119
|
try {
|
|
104
120
|
await manager.reconfigure(next)
|
|
105
121
|
ensureOperational()
|
|
122
|
+
variants.reconcile()
|
|
106
123
|
} catch (error) {
|
|
107
124
|
const message = error instanceof Error ? error.message : String(error)
|
|
108
125
|
ctx.logger.error('dsh-vision-toolkit: keeping the previous runtime after a refused Settings generation. %s', message)
|
package/src/paste-images.ts
CHANGED
|
@@ -11,6 +11,45 @@ import { sameOriginPost } from './web-request.ts'
|
|
|
11
11
|
/** Exact route used by the browser paste integration. */
|
|
12
12
|
export const PASTE_IMAGES_ROUTE = '/_dsh/vision-toolkit/paste-images'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Exact route the browser paste integration asks before taking a paste over:
|
|
16
|
+
* `GET ?sessionId=&model=&provider=&modelId=` answers the verdict from the
|
|
17
|
+
* live Session's model route.
|
|
18
|
+
*/
|
|
19
|
+
export const PASTE_POLICY_ROUTE = '/_dsh/vision-toolkit/paste-policy'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One exact model route the browser should switch the Session to before the
|
|
23
|
+
* native attachment flow: the image-input variant of the current text-only
|
|
24
|
+
* model. The variant declares image input, so the paste keeps the composer
|
|
25
|
+
* thumbnail and the durable session image.
|
|
26
|
+
*/
|
|
27
|
+
export interface PasteSwitchRoute {
|
|
28
|
+
/** The variant provider route (`vision-toolkit-` + upstream provider id). */
|
|
29
|
+
provider: string
|
|
30
|
+
/** The model id, identical to the upstream text-only model's id. */
|
|
31
|
+
model: string
|
|
32
|
+
/** The variant's selector display name (upstream name + variant suffix). */
|
|
33
|
+
label: string
|
|
34
|
+
/** The upstream reasoning effort, when the selection carries one. */
|
|
35
|
+
reasoningEffort?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The exact model route the browser read from the live model catalog. */
|
|
39
|
+
export interface PasteSelectionQuery {
|
|
40
|
+
provider: string
|
|
41
|
+
model: string
|
|
42
|
+
reasoningEffort?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The paste-policy answer for one Session and model route. */
|
|
46
|
+
export interface PasteVerdict {
|
|
47
|
+
/** Whether the browser should turn the paste into workspace paths instead of attachments. */
|
|
48
|
+
takeOver: boolean
|
|
49
|
+
/** When present, the browser switches to this route first, then lets the paste flow natively. */
|
|
50
|
+
autoSwitch?: PasteSwitchRoute
|
|
51
|
+
}
|
|
52
|
+
|
|
14
53
|
const MAX_NAME_BYTES = 180
|
|
15
54
|
|
|
16
55
|
interface PasteImageResponse {
|
package/src/web-request.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Accept a request only from the DSH Web application's origin. Method-agnostic:
|
|
5
|
+
* the same fence guards state-changing POSTs and policy GETs.
|
|
6
|
+
* @param req - the incoming request whose headers carry the origin evidence.
|
|
7
|
+
* @returns whether the request may be answered.
|
|
8
|
+
*/
|
|
9
|
+
export function sameOriginRequest(req: IncomingMessage): boolean {
|
|
5
10
|
const fetchSite = req.headers['sec-fetch-site']
|
|
6
11
|
if (fetchSite === 'cross-site') return false
|
|
7
12
|
const origin = req.headers.origin
|
|
@@ -15,3 +20,8 @@ export function sameOriginPost(req: IncomingMessage): boolean {
|
|
|
15
20
|
return false
|
|
16
21
|
}
|
|
17
22
|
}
|
|
23
|
+
|
|
24
|
+
/** Accept state-changing requests only from the DSH Web application's origin. */
|
|
25
|
+
export function sameOriginPost(req: IncomingMessage): boolean {
|
|
26
|
+
return sameOriginRequest(req)
|
|
27
|
+
}
|
package/src/web.ts
CHANGED
|
@@ -13,7 +13,13 @@ import { SettingsConflictError, type SettingsDescriptor } from '@deepseek-ai/dsh
|
|
|
13
13
|
// Type-only import activates the optional webServer Context declaration.
|
|
14
14
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
15
15
|
import { ArtifactAccessController, ARTIFACT_ROUTE_PREFIX } from './artifact-access.ts'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
PastedImageBackend,
|
|
18
|
+
PASTE_IMAGES_ROUTE,
|
|
19
|
+
PASTE_POLICY_ROUTE,
|
|
20
|
+
type PasteSelectionQuery,
|
|
21
|
+
type PasteVerdict,
|
|
22
|
+
} from './paste-images.ts'
|
|
17
23
|
import {
|
|
18
24
|
resolveConfig,
|
|
19
25
|
VISION_TOOLKIT_SETTINGS_NAMESPACE,
|
|
@@ -27,7 +33,7 @@ import {
|
|
|
27
33
|
type RuntimeManagerStatus,
|
|
28
34
|
} from './runtime-manager.ts'
|
|
29
35
|
import { PLUGIN_VERSION, UPSTREAM_COMMIT, UPSTREAM_REPOSITORY, UPSTREAM_VERSION } from './version.ts'
|
|
30
|
-
import { sameOriginPost } from './web-request.ts'
|
|
36
|
+
import { sameOriginPost, sameOriginRequest } from './web-request.ts'
|
|
31
37
|
|
|
32
38
|
/** Exact route used by the browser Settings page. */
|
|
33
39
|
export const SETTINGS_ROUTE = '/_dsh/vision-toolkit/settings'
|
|
@@ -353,17 +359,87 @@ export class VisionToolkitWebBackend {
|
|
|
353
359
|
}
|
|
354
360
|
}
|
|
355
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Same-origin policy handler for the paste route: whether the browser should
|
|
364
|
+
* take a paste over into workspace paths, or let it flow natively after an
|
|
365
|
+
* optional automatic switch to the image-input variant. The optional `model`
|
|
366
|
+
* query carries the model-selector label the client currently shows; the
|
|
367
|
+
* optional `provider`/`modelId`/`reasoningEffort` queries carry the exact
|
|
368
|
+
* route the client read from the live model catalog, which the resolver
|
|
369
|
+
* prefers (a label alone cannot pick a provider). Unresolvable routes answer
|
|
370
|
+
* native — the safe default.
|
|
371
|
+
* @param resolve - resolves one live Session's paste verdict.
|
|
372
|
+
* @returns the HTTP handler.
|
|
373
|
+
*/
|
|
374
|
+
export function createPastePolicyHandler(
|
|
375
|
+
resolve: (sessionId: string, selection?: PasteSelectionQuery, modelLabel?: string) => Promise<PasteVerdict>,
|
|
376
|
+
): (req: IncomingMessage, res: ServerResponse) => void {
|
|
377
|
+
return (req, res) => {
|
|
378
|
+
void (async () => {
|
|
379
|
+
try {
|
|
380
|
+
if (req.method !== 'GET') {
|
|
381
|
+
requestError(res, 405, 'method-not-allowed', 'Use GET')
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
if (!sameOriginRequest(req)) {
|
|
385
|
+
requestError(res, 403, 'origin-rejected', 'The request must originate from this DSH Web application')
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
let sessionId: string
|
|
389
|
+
let modelLabel: string | undefined
|
|
390
|
+
let selection: PasteSelectionQuery | undefined
|
|
391
|
+
try {
|
|
392
|
+
const url = new URL(req.url ?? PASTE_POLICY_ROUTE, 'http://dsh.internal')
|
|
393
|
+
const sessions = url.searchParams.getAll('sessionId')
|
|
394
|
+
if (sessions.length !== 1 || sessions[0] === undefined || sessions[0] === '') {
|
|
395
|
+
throw new TypeError('sessionId is required exactly once')
|
|
396
|
+
}
|
|
397
|
+
sessionId = sessions[0]!
|
|
398
|
+
const models = url.searchParams.getAll('model')
|
|
399
|
+
if (models.length > 1) throw new TypeError('model may be given at most once')
|
|
400
|
+
modelLabel = models[0]
|
|
401
|
+
const providers = url.searchParams.getAll('provider')
|
|
402
|
+
if (providers.length > 1) throw new TypeError('provider may be given at most once')
|
|
403
|
+
const modelIds = url.searchParams.getAll('modelId')
|
|
404
|
+
if (modelIds.length > 1) throw new TypeError('modelId may be given at most once')
|
|
405
|
+
const efforts = url.searchParams.getAll('reasoningEffort')
|
|
406
|
+
if (efforts.length > 1) throw new TypeError('reasoningEffort may be given at most once')
|
|
407
|
+
const provider = providers[0]
|
|
408
|
+
const modelId = modelIds[0]
|
|
409
|
+
if (provider !== undefined && modelId !== undefined && provider !== '' && modelId !== '') {
|
|
410
|
+
selection = {
|
|
411
|
+
provider,
|
|
412
|
+
model: modelId,
|
|
413
|
+
...(efforts[0] === undefined || efforts[0] === '' ? {} : { reasoningEffort: efforts[0] }),
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch (error) {
|
|
417
|
+
requestError(res, 400, 'invalid-request', publicMessage(error))
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
const verdict = await resolve(sessionId, selection, modelLabel)
|
|
421
|
+
responseJson(res, 200, { ok: true, value: verdict })
|
|
422
|
+
} catch (error) {
|
|
423
|
+
requestError(res, 500, 'policy-failed', publicMessage(error))
|
|
424
|
+
}
|
|
425
|
+
})()
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
356
429
|
/**
|
|
357
430
|
* Attach optional Web routes whenever a webServer service is present.
|
|
358
431
|
* @param ctx - plugin context owning route effects.
|
|
359
432
|
* @param backend - Settings handler.
|
|
360
433
|
* @param artifacts - signed Artifact handler.
|
|
434
|
+
* @param pastedImages - pasted-image workspace handler.
|
|
435
|
+
* @param pastePolicy - paste-policy verdict resolver (sessionId, selection, modelLabel).
|
|
361
436
|
*/
|
|
362
437
|
export function installVisionToolkitWeb(
|
|
363
438
|
ctx: Context,
|
|
364
439
|
backend: VisionToolkitWebBackend,
|
|
365
440
|
artifacts: ArtifactAccessController,
|
|
366
441
|
pastedImages: PastedImageBackend,
|
|
442
|
+
pastePolicy: (sessionId: string, selection?: PasteSelectionQuery, modelLabel?: string) => Promise<PasteVerdict>,
|
|
367
443
|
): void {
|
|
368
444
|
ctx.inject(['webServer'], (webCtx) => {
|
|
369
445
|
webCtx.effect(() => {
|
|
@@ -383,7 +459,13 @@ export function installVisionToolkitWeb(
|
|
|
383
459
|
path: PASTE_IMAGES_ROUTE,
|
|
384
460
|
handler: (req, res) => pastedImages.handle(req, res),
|
|
385
461
|
})
|
|
462
|
+
const disposePastePolicy = webCtx.webServer.register({
|
|
463
|
+
kind: 'exact',
|
|
464
|
+
path: PASTE_POLICY_ROUTE,
|
|
465
|
+
handler: createPastePolicyHandler(pastePolicy),
|
|
466
|
+
})
|
|
386
467
|
return () => {
|
|
468
|
+
disposePastePolicy()
|
|
387
469
|
disposePasteImages()
|
|
388
470
|
disposeSettings()
|
|
389
471
|
disposeArtifact()
|