@dsh-plus/image-studio 0.1.7 → 0.1.9

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/lib/client.js CHANGED
@@ -338,28 +338,24 @@ window.__ModuleLoader__.load({
338
338
  }
339
339
  //#endregion
340
340
  //#region ../shared/src/client/fetch.ts
341
- /**
342
- * 同源自定义端点的最小 fetch 封装(各插件 client/api.ts 的公共收编版)。
343
- * 仅同源(credentials: same-origin),错误统一抛 Error(body.error)。
344
- * @module @dsh-plus/shared/client/fetch
345
- */
346
- async function parse(res) {
341
+ async function parse(res, guard) {
347
342
  const body = await res.json();
348
343
  if (!res.ok) throw new Error(body.error ?? `HTTP ${res.status}`);
344
+ if (guard !== void 0 && !guard(body)) throw new Error(`响应结构不符合预期(${res.url})`);
349
345
  return body;
350
346
  }
351
347
  /** GET 同源 JSON 端点。 */
352
- async function getJson(route) {
353
- return parse(await fetch(route, { credentials: "same-origin" }));
348
+ async function getJson(route, guard) {
349
+ return parse(await fetch(route, { credentials: "same-origin" }), guard);
354
350
  }
355
351
  /** POST 同源 JSON 端点(body 可省略)。 */
356
- async function postJson(route, body) {
352
+ async function postJson(route, body, guard) {
357
353
  return parse(await fetch(route, {
358
354
  method: "POST",
359
355
  credentials: "same-origin",
360
356
  headers: { "content-type": "application/json" },
361
- body: body === void 0 ? void 0 : JSON.stringify(body)
362
- }));
357
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
358
+ }), guard);
363
359
  }
364
360
  //#endregion
365
361
  //#region ../shared/src/client/fields.tsx
@@ -929,9 +925,14 @@ ${extra}
929
925
  function cancelTask(taskId) {
930
926
  return postJson(`${BASE}/tasks/${taskId}/cancel`);
931
927
  }
928
+ /** 画廊列表应答形状守卫:items 需为数组(渲染期直接 map)。 */
929
+ function isGalleryList(value) {
930
+ if (typeof value !== "object" || value === null) return false;
931
+ return Array.isArray(value.items);
932
+ }
932
933
  /** 画廊列表。 */
933
934
  function fetchGallery() {
934
- return getJson(`${BASE}/gallery`);
935
+ return getJson(`${BASE}/gallery`, isGalleryList);
935
936
  }
936
937
  /** 删除画廊条目。 */
937
938
  function deleteGalleryItem(itemId) {
@@ -2616,6 +2617,14 @@ ${extra}
2616
2617
  function presetId(prefix) {
2617
2618
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
2618
2619
  }
2620
+ /** 单选上传结果 → 遮罩引用(无成功条目时保持原值)。 */
2621
+ function uploadRef(entries) {
2622
+ const first = entries[0];
2623
+ return first === void 0 ? null : {
2624
+ kind: "upload",
2625
+ id: first.id
2626
+ };
2627
+ }
2619
2628
  function GeneratorForm(props) {
2620
2629
  const { t, endpoint, catalog, protocols, presets, gallery, scope, api } = props;
2621
2630
  const [providerId, setProviderId] = (0, react.useState)("");
@@ -2980,40 +2989,52 @@ ${extra}
2980
2989
  className: "ims-hint",
2981
2990
  children: t("sources.count").replace("{n}", String(sources.length))
2982
2991
  }),
2983
- uploads.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2984
- className: "ims-status ims-statusError",
2985
- role: "alert",
2986
- children: [t("upload.failed"), uploads.error]
2987
- }) : null,
2988
2992
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2989
2993
  className: "ims-blockTitle",
2990
2994
  children: t("sources.mask")
2991
2995
  }),
2992
2996
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2993
2997
  className: "ims-sources",
2994
- children: [mask !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2995
- className: "ims-thumb",
2996
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
2997
- src: refUrl(mask),
2998
- alt: ""
2999
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2998
+ children: [
2999
+ mask !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3000
+ className: "ims-thumb",
3001
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
3002
+ src: refUrl(mask),
3003
+ alt: ""
3004
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3005
+ type: "button",
3006
+ className: "ims-thumbRemove",
3007
+ "aria-label": t("sources.maskClear"),
3008
+ onClick: () => setMask(null),
3009
+ children: "×"
3010
+ })]
3011
+ }) : null,
3012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3000
3013
  type: "button",
3001
- className: "ims-thumbRemove",
3002
- "aria-label": t("sources.maskClear"),
3003
- onClick: () => setMask(null),
3004
- children: "×"
3005
- })]
3006
- }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3007
- type: "button",
3008
- className: "ims-thumbAdd",
3009
- onClick: () => setPicker("mask"),
3010
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.maskPick") })]
3011
- })]
3014
+ className: "ims-thumbAdd",
3015
+ onClick: () => setPicker("mask"),
3016
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconLayers, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sources.maskPick") })]
3017
+ }),
3018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(UploadButton, {
3019
+ t,
3020
+ uploads,
3021
+ multiple: false,
3022
+ accept: "image/png",
3023
+ className: "ims-thumbAdd",
3024
+ onUploaded: (entries) => setMask(uploadRef(entries)),
3025
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 18 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("upload.pick") })]
3026
+ })
3027
+ ]
3012
3028
  }),
3013
3029
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3014
3030
  className: "ims-hint",
3015
3031
  children: t("sources.maskHint")
3016
- })
3032
+ }),
3033
+ uploads.error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
3034
+ className: "ims-status ims-statusError",
3035
+ role: "alert",
3036
+ children: [t("upload.failed"), uploads.error]
3037
+ }) : null
3017
3038
  ]
3018
3039
  }) : null
3019
3040
  ]
package/lib/index.d.ts CHANGED
@@ -1,26 +1,6 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  //#region src/config.d.ts
4
- declare const Config: z<Schemastery.ObjectS<{
5
- promptPresets: z<unknown[], any[]>;
6
- paramPresets: z<unknown[], any[]>;
7
- providerPresets: z<unknown[], any[]>;
8
- maxConcurrent: z<number, number>;
9
- requestTimeoutMs: z<number, number>;
10
- proxy: z<string, string>;
11
- galleryMaxItems: z<number, number>;
12
- uploadTtlHours: z<number, number>;
13
- }>, Schemastery.ObjectT<{
14
- promptPresets: z<unknown[], any[]>;
15
- paramPresets: z<unknown[], any[]>;
16
- providerPresets: z<unknown[], any[]>;
17
- maxConcurrent: z<number, number>;
18
- requestTimeoutMs: z<number, number>;
19
- proxy: z<string, string>;
20
- galleryMaxItems: z<number, number>;
21
- uploadTtlHours: z<number, number>;
22
- }>>;
23
- type ImageStudioConfig = Schemastery.TypeT<typeof Config>;
24
4
  /** 提示词预设(解析后视图)。 */
25
5
  interface PromptPresetEntry {
26
6
  id: string;
@@ -44,6 +24,26 @@ interface ProviderPresetEntry {
44
24
  credentialRef: string;
45
25
  extraHeaders: Record<string, string>;
46
26
  }
27
+ declare const Config: z<Schemastery.ObjectS<{
28
+ promptPresets: z<PromptPresetEntry[], PromptPresetEntry[]>;
29
+ paramPresets: z<ParamPresetEntry[], ParamPresetEntry[]>;
30
+ providerPresets: z<ProviderPresetEntry[], ProviderPresetEntry[]>;
31
+ maxConcurrent: z<number, number>;
32
+ requestTimeoutMs: z<number, number>;
33
+ proxy: z<string, string>;
34
+ galleryMaxItems: z<number, number>;
35
+ uploadTtlHours: z<number, number>;
36
+ }>, Schemastery.ObjectT<{
37
+ promptPresets: z<PromptPresetEntry[], PromptPresetEntry[]>;
38
+ paramPresets: z<ParamPresetEntry[], ParamPresetEntry[]>;
39
+ providerPresets: z<ProviderPresetEntry[], ProviderPresetEntry[]>;
40
+ maxConcurrent: z<number, number>;
41
+ requestTimeoutMs: z<number, number>;
42
+ proxy: z<string, string>;
43
+ galleryMaxItems: z<number, number>;
44
+ uploadTtlHours: z<number, number>;
45
+ }>>;
46
+ type ImageStudioConfig = Schemastery.TypeT<typeof Config>;
47
47
  //#endregion
48
48
  //#region src/provider/types.d.ts
49
49
  /**
@@ -320,7 +320,7 @@ declare function credentialRefNameOf(presetId: string): string;
320
320
  * @module image-studio/errors
321
321
  */
322
322
  /** 全部错误码(封闭集合,新增协议/端点行为时按需扩充)。 */
323
- declare const ERROR_CODES: readonly ["unknown-protocol", "unknown-endpoint", "invalid-request", "unknown-source", "unknown-task", "task-not-runnable", "credential-unavailable", "settings-unavailable", "unsupported-media", "upload-too-large", "provider-error"];
323
+ declare const ERROR_CODES: readonly ['unknown-protocol', 'unknown-endpoint', 'invalid-request', 'unknown-source', 'unknown-task', 'task-not-runnable', 'credential-unavailable', 'settings-unavailable', 'unsupported-media', 'upload-too-large', 'provider-error'];
324
324
  type ErrorCode = (typeof ERROR_CODES)[number];
325
325
  declare class ImageStudioError extends Error {
326
326
  readonly code: ErrorCode;
@@ -474,7 +474,7 @@ declare function validateParamSpecs(specs: unknown): ParamSpecMap;
474
474
  //#endregion
475
475
  //#region src/index.d.ts
476
476
  declare const name = "dsh-plus-image-studio";
477
- declare const inject: readonly ["credentials"];
477
+ declare const inject: readonly ['credentials'];
478
478
  declare module '@deepseek-ai/cordis' {
479
479
  interface Context {
480
480
  imageStudio: ImageStudioService;
package/lib/index.js CHANGED
@@ -27,7 +27,7 @@ const PromptPreset = z.object({
27
27
  const ParamPreset = z.object({
28
28
  id: z.string().required().description("预设 id"),
29
29
  name: z.string().required().description("预设名称"),
30
- endpoint: z.string().required().description("适用端点:generation | edit"),
30
+ endpoint: z.union([z.const("generation"), z.const("edit")]).required().description("适用端点:generation | edit"),
31
31
  /** 参数表结构由 params/spec 校验,settings 层存原始 JSON。 */
32
32
  paramSpecs: z.any().required().description("参数开关表(ParamSpecMap JSON)")
33
33
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/image-studio",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "dsh-plus service+ui plugin: 图像工作室——OpenAI Images 协议文生图/图生图,全参数开关化,双预设体系,并发生图任务与可二次编辑画廊",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "https-proxy-agent": "^9.1.0",
23
- "@dsh-plus/shared": "0.1.15"
23
+ "@dsh-plus/shared": "0.1.16"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "@deepseek-ai/cordis": "^4.0.2",
package/src/client/api.ts CHANGED
@@ -38,9 +38,15 @@ export function cancelTask(taskId: string): Promise<{ ok: boolean }> {
38
38
  return postJson(`${BASE}/tasks/${taskId}/cancel`)
39
39
  }
40
40
 
41
+ /** 画廊列表应答形状守卫:items 需为数组(渲染期直接 map)。 */
42
+ function isGalleryList(value: unknown): value is GalleryListWire {
43
+ if (typeof value !== 'object' || value === null) return false
44
+ return Array.isArray((value as { items?: unknown }).items)
45
+ }
46
+
41
47
  /** 画廊列表。 */
42
48
  export function fetchGallery(): Promise<GalleryListWire> {
43
- return getJson(`${BASE}/gallery`)
49
+ return getJson(`${BASE}/gallery`, isGalleryList)
44
50
  }
45
51
 
46
52
  /** 画廊条目详情(含参数回放)。 */
@@ -15,7 +15,13 @@ import {
15
15
  type Scope,
16
16
  } from '@dsh-plus/shared/client'
17
17
  import { type ReactElement, useEffect, useMemo, useRef, useState } from 'react'
18
- import type { GenerateRequest, PresetsWire, ProvidersWire, SourceRef } from '../../dto.ts'
18
+ import type {
19
+ GenerateRequest,
20
+ PresetsWire,
21
+ ProvidersWire,
22
+ SourceRef,
23
+ UploadEntry,
24
+ } from '../../dto.ts'
19
25
  import type { GalleryItem } from '../../gallery/store.ts'
20
26
  import { MAX_SOURCE_IMAGES } from '../../limits.ts'
21
27
  import type { ParamEntry } from '../../params/catalog.ts'
@@ -66,6 +72,12 @@ function presetId(prefix: string): string {
66
72
  return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
67
73
  }
68
74
 
75
+ /** 单选上传结果 → 遮罩引用(无成功条目时保持原值)。 */
76
+ function uploadRef(entries: UploadEntry[]): SourceRef | null {
77
+ const first = entries[0]
78
+ return first === undefined ? null : { kind: 'upload', id: first.id }
79
+ }
80
+
69
81
  export function GeneratorForm(props: GeneratorProps): ReactElement {
70
82
  const { t, endpoint, catalog, protocols, presets, gallery, scope, api } = props
71
83
  const [providerId, setProviderId] = useState('')
@@ -433,12 +445,6 @@ export function GeneratorForm(props: GeneratorProps): ReactElement {
433
445
  </UploadButton>
434
446
  </div>
435
447
  <p className="ims-hint">{t('sources.count').replace('{n}', String(sources.length))}</p>
436
- {uploads.error !== null ? (
437
- <p className="ims-status ims-statusError" role="alert">
438
- {t('upload.failed')}
439
- {uploads.error}
440
- </p>
441
- ) : null}
442
448
  <h3 className="ims-blockTitle">{t('sources.mask')}</h3>
443
449
  <div className="ims-sources">
444
450
  {mask !== null ? (
@@ -458,8 +464,26 @@ export function GeneratorForm(props: GeneratorProps): ReactElement {
458
464
  <IconLayers size={18} />
459
465
  <span>{t('sources.maskPick')}</span>
460
466
  </button>
467
+ <UploadButton
468
+ t={t}
469
+ uploads={uploads}
470
+ multiple={false}
471
+ // 遮罩必须 PNG(官方要求带透明通道),文件选择器先行收窄。
472
+ accept="image/png"
473
+ className="ims-thumbAdd"
474
+ onUploaded={(entries) => setMask(uploadRef(entries))}
475
+ >
476
+ <IconPlusOutline16 size={18} />
477
+ <span>{t('upload.pick')}</span>
478
+ </UploadButton>
461
479
  </div>
462
480
  <p className="ims-hint">{t('sources.maskHint')}</p>
481
+ {uploads.error !== null ? (
482
+ <p className="ims-status ims-statusError" role="alert">
483
+ {t('upload.failed')}
484
+ {uploads.error}
485
+ </p>
486
+ ) : null}
463
487
  </section>
464
488
  ) : null}
465
489
  </div>
@@ -77,10 +77,13 @@ interface UploadButtonProps {
77
77
  onUploaded(entries: UploadEntry[]): void
78
78
  /** 允许多选(源图)或单选(遮罩)。 */
79
79
  multiple: boolean
80
- /** 允许的文件类型(缺省四类位图;遮罩场景传 image/png)。 */
81
- accept?: string
82
- disabled?: boolean
83
- className?: string
80
+ /**
81
+ * 允许的文件类型(缺省四类位图;遮罩场景传 image/png)。
82
+ * 并上 undefined:调用方按 `multiple ? undefined : 'image/png'` 直接透传。
83
+ */
84
+ accept?: string | undefined
85
+ disabled?: boolean | undefined
86
+ className?: string | undefined
84
87
  children: ReactNode
85
88
  }
86
89
 
package/src/config.ts CHANGED
@@ -12,26 +12,56 @@ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
12
12
  /** settings 命名空间(字面量即合法命名空间,编译期校验)。 */
13
13
  export const SETTINGS_NS = NS_LITERAL
14
14
 
15
- // dts 可移植性:嵌套 schema 精确类型经 TypeT 消费,显式断掉推断链
16
- // (同 usage-panel/config 的既有约定)。
17
- /* biome-ignore lint/suspicious/noExplicitAny: 嵌套 schema 的 dts 可移植性 */
18
- const PromptPreset: any = z.object({
15
+ /** 提示词预设(解析后视图)。 */
16
+ export interface PromptPresetEntry {
17
+ id: string
18
+ name: string
19
+ text: string
20
+ }
21
+
22
+ /** 参数预设(解析后视图)。 */
23
+ export interface ParamPresetEntry {
24
+ id: string
25
+ name: string
26
+ endpoint: 'generation' | 'edit'
27
+ paramSpecs: unknown
28
+ }
29
+
30
+ /** 提供商预设(解析后视图)。 */
31
+ export interface ProviderPresetEntry {
32
+ id: string
33
+ name: string
34
+ protocol: string
35
+ baseUrl: string
36
+ model: string
37
+ credentialRef: string
38
+ extraHeaders: Record<string, string>
39
+ }
40
+
41
+ // 显式标注而非 `any`:z.object() 推断类型含 cosmokit 的 `& Dict` 索引签名,
42
+ // 直接导出会触发 TS2883(inferred type cannot be named / not portable)并使
43
+ // tsdown 的 dts 生成失败。标注成具名契约后既保住类型、又让产物可移植——
44
+ // 退回 `any` 会让三个预设数组元素全部退化为 any。
45
+ const PromptPreset: z<PromptPresetEntry, PromptPresetEntry> = z.object({
19
46
  id: z.string().required().description('预设 id(稳定标识,删除级联引用)'),
20
47
  name: z.string().required().description('预设名称'),
21
48
  text: z.string().required().description('提示词模板'),
22
49
  })
23
50
 
24
- /* biome-ignore lint/suspicious/noExplicitAny: 嵌套 schema 的 dts 可移植性 */
25
- const ParamPreset: any = z.object({
51
+ const ParamPreset: z<ParamPresetEntry, ParamPresetEntry> = z.object({
26
52
  id: z.string().required().description('预设 id'),
27
53
  name: z.string().required().description('预设名称'),
28
- endpoint: z.string().required().description('适用端点:generation | edit'),
54
+ // 用字面量联合而非 z.string():endpoint 只有两个合法取值,收窄后与
55
+ // ParamPresetEntry['endpoint'] 对齐,拼错端点会在编译期暴露。
56
+ endpoint: z
57
+ .union([z.const('generation'), z.const('edit')])
58
+ .required()
59
+ .description('适用端点:generation | edit'),
29
60
  /** 参数表结构由 params/spec 校验,settings 层存原始 JSON。 */
30
61
  paramSpecs: z.any().required().description('参数开关表(ParamSpecMap JSON)'),
31
62
  })
32
63
 
33
- /* biome-ignore lint/suspicious/noExplicitAny: 嵌套 schema 的 dts 可移植性 */
34
- const ProviderPreset: any = z.object({
64
+ const ProviderPreset: z<ProviderPresetEntry, ProviderPresetEntry> = z.object({
35
65
  id: z.string().required().description('预设 id'),
36
66
  name: z.string().required().description('预设名称'),
37
67
  protocol: z.string().required().description('协议 id(如 openai-images)'),
@@ -68,29 +98,3 @@ export const Config = z.object({
68
98
  })
69
99
 
70
100
  export type ImageStudioConfig = Schemastery.TypeT<typeof Config>
71
-
72
- /** 提示词预设(解析后视图)。 */
73
- export interface PromptPresetEntry {
74
- id: string
75
- name: string
76
- text: string
77
- }
78
-
79
- /** 参数预设(解析后视图)。 */
80
- export interface ParamPresetEntry {
81
- id: string
82
- name: string
83
- endpoint: 'generation' | 'edit'
84
- paramSpecs: unknown
85
- }
86
-
87
- /** 提供商预设(解析后视图)。 */
88
- export interface ProviderPresetEntry {
89
- id: string
90
- name: string
91
- protocol: string
92
- baseUrl: string
93
- model: string
94
- credentialRef: string
95
- extraHeaders: Record<string, string>
96
- }
package/src/service.ts CHANGED
@@ -126,11 +126,11 @@ export class ImageStudioService extends Service {
126
126
  }
127
127
 
128
128
  private paramPresets(): ParamPresetEntry[] {
129
- return this.current().paramPresets as ParamPresetEntry[]
129
+ return this.current().paramPresets
130
130
  }
131
131
 
132
132
  private providerPresets(): ProviderPresetEntry[] {
133
- return this.current().providerPresets as ProviderPresetEntry[]
133
+ return this.current().providerPresets
134
134
  }
135
135
 
136
136
  /** 预设快照(端点投影)。 */