@anionex/dsh-vision-toolkit 0.1.27 → 0.1.29
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.md +6 -4
- package/README.zh.md +6 -4
- package/assets/python-bootstrap.json +47 -0
- package/lib/client.js +5 -0
- package/lib/client.js.map +1 -1
- package/lib/exposure.js +27 -5
- package/lib/exposure.js.map +1 -1
- package/lib/runtime-install.js +291 -5
- package/lib/runtime-install.js.map +1 -1
- package/lib/runtime.js +7 -1
- package/lib/runtime.js.map +1 -1
- package/lib/skill.js +2 -2
- package/lib/skill.js.map +1 -1
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/exposure.d.ts +4 -0
- package/lib/types/exposure.d.ts.map +1 -1
- package/lib/types/runtime-install.d.ts +30 -0
- package/lib/types/runtime-install.d.ts.map +1 -1
- package/lib/types/runtime.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/client/index.tsx +4 -0
- package/src/exposure.ts +28 -6
- package/src/runtime-install.ts +337 -7
- package/src/runtime.ts +6 -1
- package/src/skill.ts +2 -2
package/src/exposure.ts
CHANGED
|
@@ -19,7 +19,7 @@ export const VISION_TOOLKIT_ACTIVATE = 'vision_toolkit_activate'
|
|
|
19
19
|
|
|
20
20
|
interface AgentExposure {
|
|
21
21
|
active: boolean
|
|
22
|
-
|
|
22
|
+
liftRestriction?: () => void
|
|
23
23
|
toolDisposers: Array<() => void>
|
|
24
24
|
toolNames: string[]
|
|
25
25
|
}
|
|
@@ -150,6 +150,9 @@ export class VisionToolExposure {
|
|
|
150
150
|
const listeners = [
|
|
151
151
|
this.ctx.on('agent/created', ({ agent }) => { this.attach(agent) }),
|
|
152
152
|
this.ctx.on('agent/disposed', ({ agent }) => { this.detach(agent) }),
|
|
153
|
+
this.ctx.on('session/event', (session, event) => {
|
|
154
|
+
if (event.type === 'step/end') this.applyHideActivationForSession(session)
|
|
155
|
+
}),
|
|
153
156
|
this.ctx.on('tools/result', (exec, result) => {
|
|
154
157
|
if (result.isError === false
|
|
155
158
|
&& exec.name === 'skill'
|
|
@@ -192,23 +195,42 @@ export class VisionToolExposure {
|
|
|
192
195
|
|
|
193
196
|
const definitions = this.createTools()
|
|
194
197
|
const toolDisposers: Array<() => void> = []
|
|
195
|
-
let hideActivation: (() => void) | undefined
|
|
196
198
|
try {
|
|
197
199
|
for (const definition of definitions) toolDisposers.push(agent.ctx.tools.register(definition))
|
|
198
|
-
|
|
200
|
+
// A Skill call and the bootstrap can be issued in the same model step.
|
|
201
|
+
// Restricting immediately would turn the still-in-flight bootstrap call
|
|
202
|
+
// into an UNKNOWN_TOOL error, so live sessions hide at step/end.
|
|
203
|
+
if (!this.isLiveSession(agent.session)) this.applyHideActivation(agent)
|
|
199
204
|
} catch (error) {
|
|
200
|
-
hideActivation?.()
|
|
201
205
|
for (const dispose of toolDisposers.reverse()) dispose()
|
|
202
206
|
throw error
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
state.active = true
|
|
206
|
-
state.hideActivation = hideActivation
|
|
207
210
|
state.toolDisposers = toolDisposers
|
|
208
211
|
state.toolNames = definitions.map(definition => definition.name)
|
|
209
212
|
return { activated: true, tools: [...state.toolNames] }
|
|
210
213
|
}
|
|
211
214
|
|
|
215
|
+
/** Whether the session is attached to the live SessionStore (production). */
|
|
216
|
+
private isLiveSession(session: Session): boolean {
|
|
217
|
+
return this.ctx.sessions.get(session.id) === session
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private applyHideActivationForSession(session: Session): void {
|
|
221
|
+
for (const agent of this.ctx.agents.list()) {
|
|
222
|
+
if (agent.session === session && this.states.get(agent)?.active === true) {
|
|
223
|
+
this.applyHideActivation(agent)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private applyHideActivation(agent: Agent): void {
|
|
229
|
+
const state = this.states.get(agent)
|
|
230
|
+
if (state === undefined || state.liftRestriction !== undefined) return
|
|
231
|
+
state.liftRestriction = agent.ctx.tools.restrict({ deny: [VISION_TOOLKIT_ACTIVATE] })
|
|
232
|
+
}
|
|
233
|
+
|
|
212
234
|
private detach(agent: Agent): void {
|
|
213
235
|
const state = this.states.get(agent)
|
|
214
236
|
if (state === undefined) return
|
|
@@ -222,7 +244,7 @@ export class VisionToolExposure {
|
|
|
222
244
|
}
|
|
223
245
|
|
|
224
246
|
private disposeState(state: AgentExposure): void {
|
|
225
|
-
state.
|
|
247
|
+
state.liftRestriction?.()
|
|
226
248
|
for (const dispose of state.toolDisposers.reverse()) dispose()
|
|
227
249
|
}
|
|
228
250
|
}
|
package/src/runtime-install.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { createHash, randomUUID } from 'node:crypto'
|
|
10
10
|
import {
|
|
11
|
+
access,
|
|
12
|
+
chmod,
|
|
11
13
|
lstat,
|
|
12
14
|
mkdir,
|
|
13
15
|
mkdtemp,
|
|
@@ -20,10 +22,16 @@ import {
|
|
|
20
22
|
utimes,
|
|
21
23
|
writeFile,
|
|
22
24
|
} from 'node:fs/promises'
|
|
25
|
+
import { createWriteStream } from 'node:fs'
|
|
23
26
|
import { homedir } from 'node:os'
|
|
24
27
|
import { dirname, join, resolve } from 'node:path'
|
|
28
|
+
import { Transform } from 'node:stream'
|
|
29
|
+
import { pipeline } from 'node:stream/promises'
|
|
25
30
|
import { fileURLToPath } from 'node:url'
|
|
26
31
|
import type { Context } from '@deepseek-ai/cordis'
|
|
32
|
+
import { x as extractTar } from 'tar'
|
|
33
|
+
import { EnvHttpProxyAgent } from 'undici'
|
|
34
|
+
import { request as undiciRequest } from 'undici'
|
|
27
35
|
import type { ResolvedVisionToolkitConfig } from './config.ts'
|
|
28
36
|
import { VisionToolkitError } from './errors.ts'
|
|
29
37
|
import { UPSTREAM_COMMIT, UPSTREAM_REPOSITORY, UPSTREAM_VERSION } from './version.ts'
|
|
@@ -252,10 +260,317 @@ async function pythonMetadata(
|
|
|
252
260
|
}
|
|
253
261
|
}
|
|
254
262
|
|
|
255
|
-
|
|
263
|
+
interface PythonBootstrapArtifact {
|
|
264
|
+
url: string
|
|
265
|
+
sha256: string
|
|
266
|
+
size: number
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface PythonBootstrapManifest {
|
|
270
|
+
schemaVersion: 1
|
|
271
|
+
pythonVersion: string
|
|
272
|
+
buildTag: string
|
|
273
|
+
artifacts: Record<string, PythonBootstrapArtifact>
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const PYTHON_BOOTSTRAP_MANIFEST_PATH = join(PACKAGE_ROOT, 'assets', 'python-bootstrap.json')
|
|
277
|
+
const PYTHON_DOWNLOAD_TIMEOUT_MS = 10 * 60 * 1000
|
|
278
|
+
const PYTHON_DOWNLOAD_ATTEMPTS = 3
|
|
279
|
+
|
|
280
|
+
async function readPythonBootstrapManifest(): Promise<PythonBootstrapManifest> {
|
|
281
|
+
let parsed: unknown
|
|
282
|
+
try {
|
|
283
|
+
parsed = JSON.parse(await readFile(PYTHON_BOOTSTRAP_MANIFEST_PATH, 'utf8'))
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new VisionToolkitError('runtime', `python bootstrap manifest is unreadable: ${PYTHON_BOOTSTRAP_MANIFEST_PATH}`, { cause: error })
|
|
286
|
+
}
|
|
287
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
288
|
+
throw new VisionToolkitError('runtime', 'python bootstrap manifest is not an object')
|
|
289
|
+
}
|
|
290
|
+
const manifest = parsed as Partial<PythonBootstrapManifest>
|
|
291
|
+
if (
|
|
292
|
+
manifest.schemaVersion !== 1
|
|
293
|
+
|| typeof manifest.pythonVersion !== 'string'
|
|
294
|
+
|| !/^3\.\d+\.\d+$/u.test(manifest.pythonVersion)
|
|
295
|
+
|| typeof manifest.buildTag !== 'string'
|
|
296
|
+
|| !/^\d{8}$/u.test(manifest.buildTag)
|
|
297
|
+
|| typeof manifest.artifacts !== 'object'
|
|
298
|
+
|| manifest.artifacts === null
|
|
299
|
+
) {
|
|
300
|
+
throw new VisionToolkitError('runtime', 'python bootstrap manifest is invalid')
|
|
301
|
+
}
|
|
302
|
+
for (const [target, artifact] of Object.entries(manifest.artifacts)) {
|
|
303
|
+
if (
|
|
304
|
+
typeof artifact !== 'object'
|
|
305
|
+
|| artifact === null
|
|
306
|
+
|| typeof artifact.url !== 'string'
|
|
307
|
+
|| !artifact.url.startsWith('https://github.com/astral-sh/python-build-standalone/releases/download/')
|
|
308
|
+
|| typeof artifact.sha256 !== 'string'
|
|
309
|
+
|| !/^[a-f0-9]{64}$/u.test(artifact.sha256)
|
|
310
|
+
|| !Number.isInteger(artifact.size)
|
|
311
|
+
|| artifact.size <= 0
|
|
312
|
+
) {
|
|
313
|
+
throw new VisionToolkitError('runtime', `python bootstrap manifest has an invalid artifact: ${target}`)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return manifest as PythonBootstrapManifest
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Map Node platform/arch to the pinned artifact name, including musl Linux. */
|
|
320
|
+
export function pythonBootstrapTarget(platform: string, arch: string, musl: boolean): string {
|
|
321
|
+
return platform === 'linux' && musl ? `${platform}-${arch}-musl` : `${platform}-${arch}`
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function runningMusl(): Promise<boolean> {
|
|
325
|
+
if (process.platform !== 'linux') return false
|
|
326
|
+
try {
|
|
327
|
+
await access(join('/', 'etc', 'alpine-release'))
|
|
328
|
+
return true
|
|
329
|
+
} catch {
|
|
330
|
+
// Fall through to the loader-name probe.
|
|
331
|
+
}
|
|
332
|
+
const loader = process.arch === 'arm64' ? 'aarch64' : 'x86_64'
|
|
333
|
+
try {
|
|
334
|
+
await access(join('/', 'lib', `ld-musl-${loader}.so.1`))
|
|
335
|
+
return true
|
|
336
|
+
} catch {
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface DownloadResponse {
|
|
342
|
+
statusCode: number
|
|
343
|
+
headers: Record<string, string | string[] | undefined>
|
|
344
|
+
body: NodeJS.ReadableStream
|
|
345
|
+
close: () => Promise<void>
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
type DownloadRequest = (url: string, signal: AbortSignal) => Promise<DownloadResponse>
|
|
349
|
+
|
|
350
|
+
const DOWNLOAD_HOSTS = new Set([
|
|
351
|
+
'github.com',
|
|
352
|
+
'objects.githubusercontent.com',
|
|
353
|
+
'release-assets.githubusercontent.com',
|
|
354
|
+
])
|
|
355
|
+
|
|
356
|
+
async function defaultDownloadRequest(url: string, signal: AbortSignal): Promise<DownloadResponse> {
|
|
357
|
+
const dispatcher = new EnvHttpProxyAgent()
|
|
358
|
+
let current = url
|
|
359
|
+
try {
|
|
360
|
+
for (let redirects = 0; ; redirects++) {
|
|
361
|
+
const host = new URL(current).hostname
|
|
362
|
+
if (!DOWNLOAD_HOSTS.has(host)) throw new Error(`download redirected outside GitHub: ${host}`)
|
|
363
|
+
const response = await undiciRequest(current, {
|
|
364
|
+
dispatcher,
|
|
365
|
+
signal,
|
|
366
|
+
headers: { 'user-agent': 'dsh-vision-toolkit' },
|
|
367
|
+
})
|
|
368
|
+
if (response.statusCode >= 300 && response.statusCode < 400) {
|
|
369
|
+
const location = response.headers.location
|
|
370
|
+
await response.body.dump().catch(() => {})
|
|
371
|
+
if (typeof location !== 'string' || location.length === 0 || redirects >= 5) {
|
|
372
|
+
throw new Error('download redirected too many times or without a Location header')
|
|
373
|
+
}
|
|
374
|
+
current = new URL(location, current).toString()
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
statusCode: response.statusCode,
|
|
379
|
+
headers: response.headers,
|
|
380
|
+
body: response.body,
|
|
381
|
+
close: () => dispatcher.close().catch(() => {}),
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
await dispatcher.close().catch(() => {})
|
|
386
|
+
throw error
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function downloadBundledPythonOnce(
|
|
391
|
+
artifact: PythonBootstrapArtifact,
|
|
392
|
+
destination: string,
|
|
393
|
+
requestImpl: DownloadRequest = defaultDownloadRequest,
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
const signal = AbortSignal.timeout(PYTHON_DOWNLOAD_TIMEOUT_MS)
|
|
396
|
+
let response: DownloadResponse
|
|
397
|
+
try {
|
|
398
|
+
response = await requestImpl(artifact.url, signal)
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throw new Error(`download request failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
if (response.statusCode !== 200) throw new Error(`download returned HTTP ${response.statusCode}`)
|
|
404
|
+
const hash = createHash('sha256')
|
|
405
|
+
let bytes = 0
|
|
406
|
+
const hasher = new Transform({
|
|
407
|
+
transform(chunk, _encoding, callback) {
|
|
408
|
+
hash.update(chunk as Buffer)
|
|
409
|
+
bytes += (chunk as Buffer).length
|
|
410
|
+
callback(null, chunk)
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
await pipeline(response.body, hasher, createWriteStream(destination))
|
|
414
|
+
if (bytes !== artifact.size) throw new Error(`size mismatch: expected ${artifact.size}, received ${bytes}`)
|
|
415
|
+
if (hash.digest('hex') !== artifact.sha256) throw new Error('sha256 mismatch')
|
|
416
|
+
} finally {
|
|
417
|
+
await response.close()
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function downloadBundledPython(
|
|
422
|
+
artifact: PythonBootstrapArtifact,
|
|
423
|
+
destination: string,
|
|
424
|
+
requestImpl: DownloadRequest,
|
|
425
|
+
): Promise<void> {
|
|
426
|
+
let lastError: unknown
|
|
427
|
+
for (let attempt = 0; attempt < PYTHON_DOWNLOAD_ATTEMPTS; attempt++) {
|
|
428
|
+
try {
|
|
429
|
+
await downloadBundledPythonOnce(artifact, destination, requestImpl)
|
|
430
|
+
return
|
|
431
|
+
} catch (error) {
|
|
432
|
+
lastError = error
|
|
433
|
+
if (attempt + 1 < PYTHON_DOWNLOAD_ATTEMPTS) {
|
|
434
|
+
await new Promise(resolveWait => setTimeout(resolveWait, 500 * 2 ** attempt))
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
throw lastError
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Cross-process directory lock with a stale-lock timeout and heartbeat,
|
|
443
|
+
* matching the managed-runtime lock semantics.
|
|
444
|
+
*/
|
|
445
|
+
async function withDirectoryLock<T>(lockPath: string, fn: () => Promise<T>): Promise<T> {
|
|
446
|
+
const owner = randomUUID()
|
|
447
|
+
let acquired = false
|
|
448
|
+
await mkdir(dirname(lockPath), { recursive: true })
|
|
449
|
+
try {
|
|
450
|
+
await mkdir(lockPath, { recursive: false })
|
|
451
|
+
acquired = true
|
|
452
|
+
await writeFile(join(lockPath, 'owner'), `${owner}\n`, { flag: 'wx' })
|
|
453
|
+
} catch (error) {
|
|
454
|
+
if (acquired) {
|
|
455
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {})
|
|
456
|
+
throw error
|
|
457
|
+
}
|
|
458
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
|
|
459
|
+
const started = Date.now()
|
|
460
|
+
while (Date.now() - started < PREPARE_TIMEOUT_MS) {
|
|
461
|
+
try {
|
|
462
|
+
const info = await stat(lockPath)
|
|
463
|
+
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
|
|
464
|
+
await rm(lockPath, { recursive: true, force: true })
|
|
465
|
+
return withDirectoryLock(lockPath, fn)
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
return withDirectoryLock(lockPath, fn)
|
|
469
|
+
}
|
|
470
|
+
await new Promise(resolveWait => setTimeout(resolveWait, 250))
|
|
471
|
+
}
|
|
472
|
+
throw new VisionToolkitError('runtime', 'timed out waiting for another process to prepare the bundled Python')
|
|
473
|
+
}
|
|
474
|
+
const heartbeat = setInterval(() => {
|
|
475
|
+
const now = new Date()
|
|
476
|
+
void utimes(lockPath, now, now).catch(() => {})
|
|
477
|
+
}, LOCK_HEARTBEAT_MS)
|
|
478
|
+
heartbeat.unref()
|
|
479
|
+
try {
|
|
480
|
+
return await fn()
|
|
481
|
+
} finally {
|
|
482
|
+
clearInterval(heartbeat)
|
|
483
|
+
try {
|
|
484
|
+
if ((await readFile(join(lockPath, 'owner'), 'utf8')).trim() === owner) {
|
|
485
|
+
await rm(lockPath, { recursive: true, force: true })
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
// The lock was already removed or replaced.
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function acquireBundledPython(
|
|
494
|
+
ctx: Context,
|
|
495
|
+
stateRoot: string,
|
|
496
|
+
cwd: string,
|
|
497
|
+
manifestOverride?: PythonBootstrapManifest,
|
|
498
|
+
requestImpl?: DownloadRequest,
|
|
499
|
+
): Promise<{ command: RuntimeCommand; version: string }> {
|
|
500
|
+
const manifest = manifestOverride ?? await readPythonBootstrapManifest()
|
|
501
|
+
const target = pythonBootstrapTarget(process.platform, process.arch, await runningMusl())
|
|
502
|
+
const artifact = manifest.artifacts[target]
|
|
503
|
+
if (artifact === undefined) {
|
|
504
|
+
throw new VisionToolkitError(
|
|
505
|
+
'runtime',
|
|
506
|
+
`no bundled Python ${manifest.pythonVersion} artifact for ${target}; install Python 3.11+ or configure runtime.python`,
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
const root = join(stateRoot, 'python-bootstrap', `${manifest.pythonVersion}-${target}`)
|
|
510
|
+
const interpreter = process.platform === 'win32' ? join(root, 'python.exe') : join(root, 'bin', 'python3')
|
|
511
|
+
const command: RuntimeCommand = { program: interpreter, prefix: [], display: interpreter }
|
|
512
|
+
const cached = await pythonMetadata(ctx, command, cwd)
|
|
513
|
+
if (cached !== undefined) return { command, version: cached.version }
|
|
514
|
+
await withDirectoryLock(`${root}.lock`, async () => {
|
|
515
|
+
const ready = await pythonMetadata(ctx, command, cwd)
|
|
516
|
+
if (ready !== undefined) return
|
|
517
|
+
const parent = dirname(root)
|
|
518
|
+
await mkdir(parent, { recursive: true })
|
|
519
|
+
await rm(root, { recursive: true, force: true })
|
|
520
|
+
const work = await mkdtemp(join(parent, '.python-bootstrap-'))
|
|
521
|
+
try {
|
|
522
|
+
const archive = join(work, 'python.tar.gz')
|
|
523
|
+
const extractDir = join(work, 'extract')
|
|
524
|
+
await mkdir(extractDir, { recursive: true })
|
|
525
|
+
try {
|
|
526
|
+
await downloadBundledPython(artifact, archive, requestImpl ?? defaultDownloadRequest)
|
|
527
|
+
} catch (error) {
|
|
528
|
+
throw new VisionToolkitError(
|
|
529
|
+
'runtime',
|
|
530
|
+
`bundled Python ${manifest.pythonVersion} could not be downloaded for ${target} (${error instanceof Error ? error.message : String(error)}); install Python 3.11+ or configure runtime.python`,
|
|
531
|
+
{ cause: error },
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
await extractTar({ file: archive, cwd: extractDir, strip: 1 })
|
|
536
|
+
} catch (error) {
|
|
537
|
+
throw new VisionToolkitError(
|
|
538
|
+
'runtime',
|
|
539
|
+
`bundled Python ${manifest.pythonVersion} could not be extracted for ${target}`,
|
|
540
|
+
{ cause: error },
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
const extractedInterpreter = process.platform === 'win32'
|
|
544
|
+
? join(extractDir, 'python.exe')
|
|
545
|
+
: join(extractDir, 'bin', 'python3')
|
|
546
|
+
try {
|
|
547
|
+
await access(extractedInterpreter)
|
|
548
|
+
} catch (error) {
|
|
549
|
+
throw new VisionToolkitError(
|
|
550
|
+
'runtime',
|
|
551
|
+
`bundled Python ${manifest.pythonVersion} for ${target} is missing its interpreter`,
|
|
552
|
+
{ cause: error },
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
if (process.platform !== 'win32') await chmod(extractedInterpreter, 0o755)
|
|
556
|
+
await rename(extractDir, root)
|
|
557
|
+
} finally {
|
|
558
|
+
await rm(work, { recursive: true, force: true })
|
|
559
|
+
}
|
|
560
|
+
})
|
|
561
|
+
const metadata = await pythonMetadata(ctx, command, cwd)
|
|
562
|
+
if (metadata === undefined) {
|
|
563
|
+
throw new VisionToolkitError('runtime', 'bundled Python did not start after extraction')
|
|
564
|
+
}
|
|
565
|
+
return { command, version: metadata.version }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export async function resolveBootstrapPython(
|
|
256
569
|
ctx: Context,
|
|
257
570
|
configured: string | undefined,
|
|
258
571
|
cwd: string,
|
|
572
|
+
manifestOverride?: PythonBootstrapManifest,
|
|
573
|
+
requestImpl?: DownloadRequest,
|
|
259
574
|
): Promise<{ command: RuntimeCommand; version: string; major: number; minor: number }> {
|
|
260
575
|
const candidates: RuntimeCommand[] = configured === undefined
|
|
261
576
|
? process.platform === 'win32'
|
|
@@ -275,12 +590,27 @@ async function resolveBootstrapPython(
|
|
|
275
590
|
return { command, ...metadata }
|
|
276
591
|
}
|
|
277
592
|
}
|
|
278
|
-
|
|
279
|
-
'runtime',
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
593
|
+
if (configured !== undefined) {
|
|
594
|
+
throw new VisionToolkitError('runtime', `vision-toolkit requires Python 3.11 or newer: ${configured}`)
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const stateRoot = visionToolkitStateRoot()
|
|
598
|
+
await mkdir(stateRoot, { recursive: true })
|
|
599
|
+
const bundled = await acquireBundledPython(ctx, stateRoot, cwd, manifestOverride, requestImpl)
|
|
600
|
+
return {
|
|
601
|
+
command: bundled.command,
|
|
602
|
+
version: bundled.version,
|
|
603
|
+
major: Number.parseInt(bundled.version.split('.')[0] ?? '', 10),
|
|
604
|
+
minor: Number.parseInt(bundled.version.split('.')[1] ?? '', 10),
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error instanceof VisionToolkitError) throw error
|
|
608
|
+
throw new VisionToolkitError(
|
|
609
|
+
'runtime',
|
|
610
|
+
'vision-toolkit requires Python 3.11 or newer; tried python3, python, and the Windows py launcher, and automatic bundled-Python preparation failed',
|
|
611
|
+
{ cause: error },
|
|
612
|
+
)
|
|
613
|
+
}
|
|
284
614
|
}
|
|
285
615
|
|
|
286
616
|
/** Persistent per-DSH-home cache root shared by runtime and Web support files. */
|
package/src/runtime.ts
CHANGED
|
@@ -2082,8 +2082,13 @@ export class VisionToolkitRuntime {
|
|
|
2082
2082
|
await response.body?.cancel().catch(() => {})
|
|
2083
2083
|
if (response.ok) {
|
|
2084
2084
|
service = { status: 'ok', detail: `Service responded at ${endpoint} (HTTP ${response.status})` }
|
|
2085
|
-
} else if (response.status === 401
|
|
2085
|
+
} else if (response.status === 401) {
|
|
2086
2086
|
service = { status: 'error', detail: `Service rejected the configured credential (HTTP ${response.status})` }
|
|
2087
|
+
} else if (response.status === 403) {
|
|
2088
|
+
// Some providers (e.g. Groq preview/account restrictions) block GET /models
|
|
2089
|
+
// while real multimodal requests still work. Treat 403 as a warning so the
|
|
2090
|
+
// explicit vision-model test, not the model list endpoint, decides access.
|
|
2091
|
+
service = { status: 'warning', detail: `Service is reachable but restricted GET /models (HTTP 403); the credential may still be valid for real vision requests` }
|
|
2087
2092
|
} else if (response.status === 404 || response.status === 405) {
|
|
2088
2093
|
service = { status: 'warning', detail: `Service is reachable but does not expose GET /models (HTTP ${response.status})` }
|
|
2089
2094
|
} else if (response.status === 429) {
|
package/src/skill.ts
CHANGED
|
@@ -24,8 +24,8 @@ export const VISION_TOOLS_SKILL_CONTENT = readFileSync(
|
|
|
24
24
|
/** Runtime skill registration mounted only after every native tool is ready. */
|
|
25
25
|
export const VISION_TOOLS_SKILL: SkillRegistration = {
|
|
26
26
|
name: VISION_TOOLS_SKILL_NAME,
|
|
27
|
-
description: '
|
|
28
|
-
whenToUse: '
|
|
27
|
+
description: '把截图或设计图还原为 UI(网页、应用界面或组件),生成 HTML/CSS 或项目现有前端代码;也支持图片问答、长截图/聊天记录 OCR、元素定位与盘点、裁剪、前景提取、像素对比、取色、SVG 描摹和 HTML 转截图。当任务涉及图片理解、UI 还原/重建(rebuild or restore a UI from a screenshot)、视觉回归、像素坐标、基于截图的 GUI 操作、可复用图片/SVG 素材、图表还原或长截图 OCR 时使用。',
|
|
28
|
+
whenToUse: '任务依赖图片文字/内容、像素坐标、截图转 UI 重建、视觉回归、可复用图片/SVG 素材、图表还原、基于截图的 GUI 操作或长截图 OCR 时使用。',
|
|
29
29
|
source: 'runtime',
|
|
30
30
|
resourceBase: {
|
|
31
31
|
kind: 'directory',
|