@dickpy/dsh-imagegen 1.5.4 → 1.5.5
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 +2 -0
- package/lib/client.js +495 -394
- package/lib/client.js.map +1 -1
- package/lib/index.js +138 -121
- package/package.json +1 -1
- package/src/client/CanvasWorkspace.tsx +60 -1
- package/src/client/locales.ts +3 -0
- package/src/engine.ts +150 -132
- package/src/protocol.ts +1 -1
package/src/engine.ts
CHANGED
|
@@ -184,8 +184,14 @@ function requestSignal(source: AbortSignal | undefined, timeoutMs: number): { si
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
/**
|
|
188
|
-
|
|
187
|
+
/** Whether an error was produced by a requestSignal budget timeout. These can
|
|
188
|
+
* surface from the fetch call itself or from reading the response body, so the
|
|
189
|
+
* budget must stay armed until the body has been consumed. */
|
|
190
|
+
function isBudgetTimeout(error: unknown): boolean {
|
|
191
|
+
return (error instanceof DOMException || error instanceof Error) && error.name === 'TimeoutError'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Content-type extension hints for URL-fetched images. */function mimeOfExtension(path: string): string | undefined {
|
|
189
195
|
const match = /\.([a-z0-9]+)$/i.exec(path)
|
|
190
196
|
if (match === null) return undefined
|
|
191
197
|
switch (match[1]!.toLowerCase()) {
|
|
@@ -358,29 +364,32 @@ async function normalizeItem(
|
|
|
358
364
|
return { b64: parsed.base64, mime: detectImageMime(Buffer.from(parsed.base64, 'base64')) ?? parsed.mime, revisedPrompt }
|
|
359
365
|
}
|
|
360
366
|
const budget = requestSignal(signal, IMAGE_FETCH_TIMEOUT_MS)
|
|
361
|
-
let response: Response
|
|
362
367
|
try {
|
|
363
|
-
response
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
let response: Response
|
|
369
|
+
try {
|
|
370
|
+
response = await fetch(url, {
|
|
371
|
+
...isPresignedUrl(url) || upstream.apiKey === ''
|
|
372
|
+
? {}
|
|
373
|
+
: { headers: { authorization: `Bearer ${upstream.apiKey}` } },
|
|
374
|
+
signal: budget.signal,
|
|
375
|
+
})
|
|
376
|
+
} catch (error) {
|
|
377
|
+
throw new ImageGenError(`failed to fetch the generated image url: ${error instanceof Error ? error.message : String(error)}`)
|
|
378
|
+
}
|
|
379
|
+
if (!response.ok) {
|
|
380
|
+
throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`)
|
|
381
|
+
}
|
|
382
|
+
// Budget stays armed through the body read so a stalled download cannot hang the task.
|
|
383
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
384
|
+
const contentType = response.headers.get('content-type')
|
|
385
|
+
const mime = detectImageMime(buffer)
|
|
386
|
+
?? (contentType !== null && contentType !== ''
|
|
387
|
+
? contentType.split(';')[0]!.trim()
|
|
388
|
+
: mimeOfExtension(url) ?? 'image/png')
|
|
389
|
+
return { b64: buffer.toString('base64'), mime, revisedPrompt }
|
|
371
390
|
} finally {
|
|
372
391
|
budget.dispose()
|
|
373
392
|
}
|
|
374
|
-
if (!response.ok) {
|
|
375
|
-
throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`)
|
|
376
|
-
}
|
|
377
|
-
const buffer = Buffer.from(await response.arrayBuffer())
|
|
378
|
-
const contentType = response.headers.get('content-type')
|
|
379
|
-
const mime = detectImageMime(buffer)
|
|
380
|
-
?? (contentType !== null && contentType !== ''
|
|
381
|
-
? contentType.split(';')[0]!.trim()
|
|
382
|
-
: mimeOfExtension(url) ?? 'image/png')
|
|
383
|
-
return { b64: buffer.toString('base64'), mime, revisedPrompt }
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
/** Expand a provider image item whose URL may be a string or an array. */
|
|
@@ -477,52 +486,54 @@ async function pollAsyncTask(
|
|
|
477
486
|
while (Date.now() < deadline) {
|
|
478
487
|
const remaining = deadline - Date.now()
|
|
479
488
|
const budget = requestSignal(signal, Math.min(ASYNC_POLL_REQUEST_TIMEOUT_MS, remaining))
|
|
480
|
-
let response: Response
|
|
481
489
|
try {
|
|
482
|
-
response
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
490
|
+
let response: Response
|
|
491
|
+
try {
|
|
492
|
+
response = await fetch(`${baseUrl}/tasks/${encodeURIComponent(taskId)}`, {
|
|
493
|
+
method: 'GET',
|
|
494
|
+
headers: { authorization: `Bearer ${upstream.apiKey.trim()}` },
|
|
495
|
+
signal: budget.signal,
|
|
496
|
+
})
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (signal?.aborted === true) throw new ImageGenError('任务已取消', 'cancelled')
|
|
499
|
+
if (isBudgetTimeout(error)) throw new ImageGenError('上游异步任务轮询超时', 'upstream-timeout')
|
|
500
|
+
throw new ImageGenError(`无法轮询上游异步任务:${error instanceof Error ? error.message : String(error)}`, 'upstream-unreachable')
|
|
501
|
+
}
|
|
502
|
+
let payload: unknown
|
|
503
|
+
try {
|
|
504
|
+
payload = await response.json()
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (isBudgetTimeout(error)) throw new ImageGenError('上游异步任务轮询超时', 'upstream-timeout')
|
|
507
|
+
throw new ImageGenError(`上游任务接口返回了非 JSON 响应(HTTP ${response.status})`, 'upstream-invalid')
|
|
508
|
+
}
|
|
509
|
+
if (!response.ok || payload === null || typeof payload !== 'object') {
|
|
510
|
+
throw new ImageGenError(asyncErrorMessage(payload, `上游任务轮询失败(HTTP ${response.status})`), 'upstream-rejected')
|
|
511
|
+
}
|
|
512
|
+
const record = payload as Record<string, unknown>
|
|
513
|
+
const data = record.data
|
|
514
|
+
const statusRecord = Array.isArray(data) ? data[0] : data !== null && typeof data === 'object' ? data : record
|
|
515
|
+
const statusValue = statusRecord !== null && typeof statusRecord === 'object'
|
|
516
|
+
? (statusRecord as Record<string, unknown>).status
|
|
517
|
+
: undefined
|
|
518
|
+
const status = typeof statusValue === 'string' ? statusValue.toLowerCase() : ''
|
|
519
|
+
if (ASYNC_FAILED_STATUSES.has(status)) {
|
|
520
|
+
throw new ImageGenError(asyncErrorMessage(payload, `上游异步任务失败(${status || 'unknown'})`), 'upstream-rejected')
|
|
521
|
+
}
|
|
522
|
+
const nested = statusRecord !== null && typeof statusRecord === 'object' ? statusRecord as Record<string, unknown> : record
|
|
523
|
+
const result = nested.result ?? (nested.output !== null && typeof nested.output === 'object' ? (nested.output as Record<string, unknown>).result : undefined) ?? record.result
|
|
524
|
+
const resultRecord = result !== null && typeof result === 'object' ? result as Record<string, unknown> : undefined
|
|
525
|
+
const images = resultRecord?.images ?? (nested.images ?? record.images)
|
|
526
|
+
if (ASYNC_COMPLETED_STATUSES.has(status) || images !== undefined) {
|
|
527
|
+
const items = Array.isArray(images) ? images.flatMap(imageItemsOf) : imageItemsOf(images)
|
|
528
|
+
if (items.length > 0) return items
|
|
529
|
+
if (ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError('上游异步任务完成但没有图片结果', 'upstream-empty')
|
|
530
|
+
}
|
|
531
|
+
if (status !== '' && !ASYNC_PENDING_STATUSES.has(status) && !ASYNC_COMPLETED_STATUSES.has(status)) {
|
|
532
|
+
throw new ImageGenError(`上游返回了未知异步任务状态:${status}`, 'upstream-invalid')
|
|
533
|
+
}
|
|
492
534
|
} finally {
|
|
493
535
|
budget.dispose()
|
|
494
536
|
}
|
|
495
|
-
let payload: unknown
|
|
496
|
-
try {
|
|
497
|
-
payload = await response.json()
|
|
498
|
-
} catch {
|
|
499
|
-
throw new ImageGenError(`上游任务接口返回了非 JSON 响应(HTTP ${response.status})`, 'upstream-invalid')
|
|
500
|
-
}
|
|
501
|
-
if (!response.ok || payload === null || typeof payload !== 'object') {
|
|
502
|
-
throw new ImageGenError(asyncErrorMessage(payload, `上游任务轮询失败(HTTP ${response.status})`), 'upstream-rejected')
|
|
503
|
-
}
|
|
504
|
-
const record = payload as Record<string, unknown>
|
|
505
|
-
const data = record.data
|
|
506
|
-
const statusRecord = Array.isArray(data) ? data[0] : data !== null && typeof data === 'object' ? data : record
|
|
507
|
-
const statusValue = statusRecord !== null && typeof statusRecord === 'object'
|
|
508
|
-
? (statusRecord as Record<string, unknown>).status
|
|
509
|
-
: undefined
|
|
510
|
-
const status = typeof statusValue === 'string' ? statusValue.toLowerCase() : ''
|
|
511
|
-
if (ASYNC_FAILED_STATUSES.has(status)) {
|
|
512
|
-
throw new ImageGenError(asyncErrorMessage(payload, `上游异步任务失败(${status || 'unknown'})`), 'upstream-rejected')
|
|
513
|
-
}
|
|
514
|
-
const nested = statusRecord !== null && typeof statusRecord === 'object' ? statusRecord as Record<string, unknown> : record
|
|
515
|
-
const result = nested.result ?? (nested.output !== null && typeof nested.output === 'object' ? (nested.output as Record<string, unknown>).result : undefined) ?? record.result
|
|
516
|
-
const resultRecord = result !== null && typeof result === 'object' ? result as Record<string, unknown> : undefined
|
|
517
|
-
const images = resultRecord?.images ?? (nested.images ?? record.images)
|
|
518
|
-
if (ASYNC_COMPLETED_STATUSES.has(status) || images !== undefined) {
|
|
519
|
-
const items = Array.isArray(images) ? images.flatMap(imageItemsOf) : imageItemsOf(images)
|
|
520
|
-
if (items.length > 0) return items
|
|
521
|
-
if (ASYNC_COMPLETED_STATUSES.has(status)) throw new ImageGenError('上游异步任务完成但没有图片结果', 'upstream-empty')
|
|
522
|
-
}
|
|
523
|
-
if (status !== '' && !ASYNC_PENDING_STATUSES.has(status) && !ASYNC_COMPLETED_STATUSES.has(status)) {
|
|
524
|
-
throw new ImageGenError(`上游返回了未知异步任务状态:${status}`, 'upstream-invalid')
|
|
525
|
-
}
|
|
526
537
|
await waitForPoll(Math.min(delay, Math.max(1, deadline - Date.now())), signal)
|
|
527
538
|
delay = Math.min(5000, delay * 2)
|
|
528
539
|
}
|
|
@@ -608,53 +619,57 @@ async function requestOneImage(
|
|
|
608
619
|
}
|
|
609
620
|
|
|
610
621
|
const budget = requestSignal(signal, UPSTREAM_TIMEOUT_MS)
|
|
611
|
-
let response: Response
|
|
612
622
|
try {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
throw new ImageGenError('上游接口响应超时(240 秒)', 'upstream-timeout')
|
|
623
|
+
let response: Response
|
|
624
|
+
try {
|
|
625
|
+
// Seedream has no /images/edits endpoint: both modes hit generations.
|
|
626
|
+
const endpoint = request.mode === 'edit' && !isSeedream(params.model)
|
|
627
|
+
? '/images/edits'
|
|
628
|
+
: '/images/generations'
|
|
629
|
+
response = await fetch(`${baseUrl}${endpoint}`, {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
headers,
|
|
632
|
+
body,
|
|
633
|
+
signal: budget.signal,
|
|
634
|
+
})
|
|
635
|
+
} catch (error) {
|
|
636
|
+
if (isBudgetTimeout(error)) throw new ImageGenError('上游接口响应超时(240 秒)', 'upstream-timeout')
|
|
637
|
+
if (signal?.aborted === true) throw new ImageGenError('任务已取消', 'cancelled')
|
|
638
|
+
throw new ImageGenError(`无法连接上游接口:${error instanceof Error ? error.message : String(error)}`, 'upstream-unreachable')
|
|
627
639
|
}
|
|
628
|
-
throw new ImageGenError(`无法连接上游接口:${message}`, 'upstream-unreachable')
|
|
629
|
-
} finally {
|
|
630
|
-
budget.dispose()
|
|
631
|
-
}
|
|
632
640
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
641
|
+
let payload: unknown
|
|
642
|
+
try {
|
|
643
|
+
// The budget stays armed through the body read: a gateway that returns
|
|
644
|
+
// headers but never completes the body must not hang the task forever.
|
|
645
|
+
payload = await response.json()
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (isBudgetTimeout(error)) throw new ImageGenError('上游接口响应超时(240 秒)', 'upstream-timeout')
|
|
648
|
+
if (signal?.aborted === true) throw new ImageGenError('任务已取消', 'cancelled')
|
|
649
|
+
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, 'upstream-invalid')
|
|
650
|
+
}
|
|
651
|
+
if (!response.ok || payload === null || typeof payload !== 'object') {
|
|
652
|
+
throw new ImageGenError(upstreamMessage(payload, response.status), 'upstream-rejected')
|
|
653
|
+
}
|
|
642
654
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
655
|
+
const record = payload as Record<string, unknown>
|
|
656
|
+
const data = dataRecordsOf(record)
|
|
657
|
+
if (data === undefined) {
|
|
658
|
+
throw new ImageGenError('上游响应缺少 data 数组', 'upstream-invalid')
|
|
659
|
+
}
|
|
660
|
+
if (data.length === 0) {
|
|
661
|
+
throw new ImageGenError('上游返回了 0 张图片', 'upstream-empty')
|
|
662
|
+
}
|
|
663
|
+
const asyncEntries = data.filter(entry => typeof entry.task_id === 'string' && entry.task_id.trim() !== '')
|
|
664
|
+
if (asyncEntries.length > 0) {
|
|
665
|
+
const asyncRecords = (await Promise.all(asyncEntries.map(entry => pollAsyncTask(baseUrl, upstream, entry.task_id as string, signal)))).flat()
|
|
666
|
+
if (asyncRecords.length === 0) throw new ImageGenError('上游异步任务完成但没有图片结果', 'upstream-empty')
|
|
667
|
+
return Promise.all(asyncRecords.flatMap(imageItemsOf).map(item => normalizeItem(item, upstream, signal)))
|
|
668
|
+
}
|
|
669
|
+
return Promise.all(data.flatMap(imageItemsOf).map(item => normalizeItem(item, upstream, signal)))
|
|
670
|
+
} finally {
|
|
671
|
+
budget.dispose()
|
|
656
672
|
}
|
|
657
|
-
return Promise.all(data.flatMap(imageItemsOf).map(item => normalizeItem(item, upstream, signal)))
|
|
658
673
|
}
|
|
659
674
|
|
|
660
675
|
/**
|
|
@@ -701,36 +716,36 @@ async function generateQwenImage(
|
|
|
701
716
|
}
|
|
702
717
|
|
|
703
718
|
const budget = requestSignal(options.signal, UPSTREAM_TIMEOUT_MS)
|
|
704
|
-
let response: Response
|
|
705
719
|
try {
|
|
706
|
-
response
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
throw new ImageGenError('上游接口响应超时(240 秒)', 'upstream-timeout')
|
|
720
|
+
let response: Response
|
|
721
|
+
try {
|
|
722
|
+
response = await fetch(`${baseUrl}/services/aigc/multimodal-generation/generation`, {
|
|
723
|
+
method: 'POST',
|
|
724
|
+
headers: {
|
|
725
|
+
authorization: `Bearer ${upstream.apiKey.trim()}`,
|
|
726
|
+
'content-type': 'application/json',
|
|
727
|
+
},
|
|
728
|
+
body: JSON.stringify(body),
|
|
729
|
+
signal: budget.signal,
|
|
730
|
+
})
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (isBudgetTimeout(error)) throw new ImageGenError('上游接口响应超时(240 秒)', 'upstream-timeout')
|
|
733
|
+
if (options.signal?.aborted === true) throw new ImageGenError('任务已取消', 'cancelled')
|
|
734
|
+
throw new ImageGenError(`无法连接上游接口:${error instanceof Error ? error.message : String(error)}`, 'upstream-unreachable')
|
|
719
735
|
}
|
|
720
|
-
throw new ImageGenError(`无法连接上游接口:${message}`, 'upstream-unreachable')
|
|
721
|
-
} finally {
|
|
722
|
-
budget.dispose()
|
|
723
|
-
}
|
|
724
736
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
737
|
+
let payload: unknown
|
|
738
|
+
try {
|
|
739
|
+
// Budget stays armed through the body read (same rationale as the OpenAI path).
|
|
740
|
+
payload = await response.json()
|
|
741
|
+
} catch (error) {
|
|
742
|
+
if (isBudgetTimeout(error)) throw new ImageGenError('上游接口响应超时(240 秒)', 'upstream-timeout')
|
|
743
|
+
if (options.signal?.aborted === true) throw new ImageGenError('任务已取消', 'cancelled')
|
|
744
|
+
throw new ImageGenError(`上游接口返回了非 JSON 响应(HTTP ${response.status})`, 'upstream-invalid')
|
|
745
|
+
}
|
|
746
|
+
if (!response.ok || payload === null || typeof payload !== 'object') {
|
|
747
|
+
throw new ImageGenError(upstreamMessage(payload, response.status), 'upstream-rejected')
|
|
748
|
+
}
|
|
734
749
|
|
|
735
750
|
// output.choices[].message.content[] mixes text and { image: url } items.
|
|
736
751
|
const record = payload as Record<string, unknown>
|
|
@@ -759,6 +774,9 @@ async function generateQwenImage(
|
|
|
759
774
|
return { b64: normalized.b64, mime: normalized.mime }
|
|
760
775
|
}))
|
|
761
776
|
return { images }
|
|
777
|
+
} finally {
|
|
778
|
+
budget.dispose()
|
|
779
|
+
}
|
|
762
780
|
}
|
|
763
781
|
|
|
764
782
|
/**
|
package/src/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const IMAGEGEN_SETTINGS_NAMESPACE = 'dsh-imagegen'
|
|
9
9
|
|
|
10
10
|
/** Published package version shared by the host updater and the client UI. */
|
|
11
|
-
export const PLUGIN_VERSION = '1.5.
|
|
11
|
+
export const PLUGIN_VERSION = '1.5.5'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
14
14
|
export const SETTINGS_API = {
|