@dsh-plus/image-studio 0.1.34 → 0.1.36
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 +1395 -394
- package/lib/index.d.ts +2 -2
- package/lib/index.js +13 -8
- package/package.json +15 -15
- package/src/client/card.tsx +21 -25
- package/src/client/i18n.ts +27 -4
- package/src/client/preset-sections.tsx +140 -174
- package/src/client/styles.ts +0 -24
- package/src/credentials.ts +9 -5
- package/src/uploads/store.ts +6 -3
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 配置卡片的三组预设编辑区:提供商(含逐条 API Key 管理)、提示词、参数。
|
|
3
3
|
* 从 card.tsx 拆出,避免单组件过长;草稿类型与纯转换在 ./draft.ts。
|
|
4
|
+
*
|
|
5
|
+
* 排版与控件一律走 @dsh-plus/shared/client 套件(Section / ListEditor / 字段控件),
|
|
6
|
+
* 与其它插件卡片保持一致:条目头提供折叠/上移/下移/删除,新增固定在列表底部。
|
|
4
7
|
* @module image-studio/client/preset-sections
|
|
5
8
|
*/
|
|
6
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
CodeField,
|
|
12
|
+
ListEditor,
|
|
13
|
+
type ListLabels,
|
|
14
|
+
Section,
|
|
15
|
+
SelectField,
|
|
16
|
+
TextField,
|
|
17
|
+
} from '@dsh-plus/shared/client'
|
|
7
18
|
import { type ReactElement, useEffect, useState } from 'react'
|
|
8
19
|
|
|
9
20
|
import { isValidPresetId } from '../credentials.ts'
|
|
@@ -68,22 +79,18 @@ function CredentialRow(props: { t: Translate; presetId: string; disabled: boolea
|
|
|
68
79
|
{t(configured ? 'card.credential.configured' : 'card.credential.unconfigured')}
|
|
69
80
|
</span>
|
|
70
81
|
) : null}
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
<Button
|
|
83
|
+
prefix="imsc"
|
|
84
|
+
label={t('card.credential.set')}
|
|
74
85
|
disabled={disabled || busy || value === '' || !isValidPresetId(presetId)}
|
|
75
86
|
onClick={() => act(() => setCredential(presetId, value))}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
type="button"
|
|
81
|
-
className="imsc-btn imsc-btnGhost"
|
|
87
|
+
/>
|
|
88
|
+
<Button
|
|
89
|
+
prefix="imsc"
|
|
90
|
+
label={t('card.credential.unset')}
|
|
82
91
|
disabled={disabled || busy || configured !== true || !isValidPresetId(presetId)}
|
|
83
92
|
onClick={() => act(() => unsetCredential(presetId))}
|
|
84
|
-
|
|
85
|
-
{t('card.credential.unset')}
|
|
86
|
-
</button>
|
|
93
|
+
/>
|
|
87
94
|
</div>
|
|
88
95
|
)
|
|
89
96
|
}
|
|
@@ -100,26 +107,34 @@ export interface PresetSectionsProps {
|
|
|
100
107
|
onPatchPrompt(index: number, patch: Partial<PromptDraft>): void
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
/** 列表编辑器文案(公共字典键,各插件一致)。 */
|
|
111
|
+
function listLabels(t: Translate): ListLabels {
|
|
112
|
+
return {
|
|
113
|
+
add: t('listAdd'),
|
|
114
|
+
remove: t('listRemove'),
|
|
115
|
+
up: t('listMoveUp'),
|
|
116
|
+
down: t('listMoveDown'),
|
|
117
|
+
duplicate: t('listDuplicate'),
|
|
118
|
+
collapse: t('collapse'),
|
|
119
|
+
expand: t('expand'),
|
|
120
|
+
empty: t('listEmpty'),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
export function ProviderPresetSection(props: PresetSectionsProps): ReactElement {
|
|
104
125
|
const { t, draft, disabled, protocols, onPatch, onPatchProvider } = props
|
|
105
126
|
return (
|
|
106
|
-
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
onClick={() => onPatch({ providers: draft.providers.filter((_, i) => i !== index) })}
|
|
118
|
-
>
|
|
119
|
-
{t('common.delete')}
|
|
120
|
-
</button>
|
|
121
|
-
</div>
|
|
122
|
-
<div className="imsc-grid2">
|
|
127
|
+
<Section prefix="imsc" title={t('card.providers')} description={t('card.providersHint')}>
|
|
128
|
+
<ListEditor
|
|
129
|
+
prefix="imsc"
|
|
130
|
+
items={draft.providers}
|
|
131
|
+
labels={listLabels(t)}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
titleOf={(provider) => provider.name || provider.id || '—'}
|
|
134
|
+
create={() => emptyProviderDraft(protocols[0]?.id ?? 'openai-images')}
|
|
135
|
+
onChange={(providers) => onPatch({ providers })}
|
|
136
|
+
renderItem={(provider, index) => (
|
|
137
|
+
<>
|
|
123
138
|
<TextField
|
|
124
139
|
prefix="imsc"
|
|
125
140
|
id={`imsc-pid-${index}`}
|
|
@@ -139,31 +154,19 @@ export function ProviderPresetSection(props: PresetSectionsProps): ReactElement
|
|
|
139
154
|
disabled={disabled}
|
|
140
155
|
onEdit={(v) => onPatchProvider(index, { name: v })}
|
|
141
156
|
/>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
onChange={(event) => onPatchProvider(index, { protocol: event.target.value })}
|
|
156
|
-
>
|
|
157
|
-
{(protocols.length > 0
|
|
158
|
-
? protocols
|
|
159
|
-
: [{ id: provider.protocol, label: provider.protocol }]
|
|
160
|
-
).map((protocol) => (
|
|
161
|
-
<option key={protocol.id} value={protocol.id}>
|
|
162
|
-
{protocol.label}
|
|
163
|
-
</option>
|
|
164
|
-
))}
|
|
165
|
-
</select>
|
|
166
|
-
</div>
|
|
157
|
+
<SelectField
|
|
158
|
+
prefix="imsc"
|
|
159
|
+
id={`imsc-pproto-${index}`}
|
|
160
|
+
label={t('card.field.protocol')}
|
|
161
|
+
value={provider.protocol}
|
|
162
|
+
options={
|
|
163
|
+
protocols.length > 0
|
|
164
|
+
? protocols.map((protocol) => ({ value: protocol.id, label: protocol.label }))
|
|
165
|
+
: [{ value: provider.protocol, label: provider.protocol }]
|
|
166
|
+
}
|
|
167
|
+
disabled={disabled}
|
|
168
|
+
onEdit={(v) => onPatchProvider(index, { protocol: v })}
|
|
169
|
+
/>
|
|
167
170
|
<TextField
|
|
168
171
|
prefix="imsc"
|
|
169
172
|
id={`imsc-pmodel-${index}`}
|
|
@@ -172,150 +175,113 @@ export function ProviderPresetSection(props: PresetSectionsProps): ReactElement
|
|
|
172
175
|
disabled={disabled}
|
|
173
176
|
onEdit={(v) => onPatchProvider(index, { model: v })}
|
|
174
177
|
/>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<div className="imsc-head">
|
|
186
|
-
<label className="imsc-label" htmlFor={`imsc-pheaders-${index}`}>
|
|
187
|
-
{t('card.field.extraHeaders')}
|
|
188
|
-
</label>
|
|
189
|
-
</div>
|
|
190
|
-
<textarea
|
|
178
|
+
<TextField
|
|
179
|
+
prefix="imsc"
|
|
180
|
+
id={`imsc-purl-${index}`}
|
|
181
|
+
label={t('card.field.baseUrl')}
|
|
182
|
+
value={provider.baseUrl}
|
|
183
|
+
disabled={disabled}
|
|
184
|
+
onEdit={(v) => onPatchProvider(index, { baseUrl: v })}
|
|
185
|
+
/>
|
|
186
|
+
<CodeField
|
|
187
|
+
prefix="imsc"
|
|
191
188
|
id={`imsc-pheaders-${index}`}
|
|
192
|
-
|
|
189
|
+
label={t('card.field.extraHeaders')}
|
|
193
190
|
value={provider.extraHeadersText}
|
|
194
191
|
disabled={disabled}
|
|
195
|
-
|
|
192
|
+
onEdit={(v) => onPatchProvider(index, { extraHeadersText: v })}
|
|
196
193
|
/>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
type="button"
|
|
203
|
-
className="imsc-btn imsc-btnGhost"
|
|
204
|
-
disabled={disabled}
|
|
205
|
-
onClick={() =>
|
|
206
|
-
onPatch({
|
|
207
|
-
providers: [
|
|
208
|
-
...draft.providers,
|
|
209
|
-
emptyProviderDraft(protocols[0]?.id ?? 'openai-images'),
|
|
210
|
-
],
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
>
|
|
214
|
-
{t('card.add')} · {t('card.providers')}
|
|
215
|
-
</button>
|
|
216
|
-
</>
|
|
194
|
+
<CredentialRow t={t} presetId={provider.id} disabled={disabled} />
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
/>
|
|
198
|
+
</Section>
|
|
217
199
|
)
|
|
218
200
|
}
|
|
219
201
|
|
|
220
202
|
export function PromptPresetSection(props: PresetSectionsProps): ReactElement {
|
|
221
203
|
const { t, draft, disabled, onPatch, onPatchPrompt } = props
|
|
222
204
|
return (
|
|
223
|
-
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
205
|
+
<Section prefix="imsc" title={t('card.prompts')} description={t('card.promptsHint')}>
|
|
206
|
+
<ListEditor
|
|
207
|
+
prefix="imsc"
|
|
208
|
+
items={draft.prompts}
|
|
209
|
+
labels={listLabels(t)}
|
|
210
|
+
disabled={disabled}
|
|
211
|
+
titleOf={(prompt) => prompt.name || '—'}
|
|
212
|
+
create={() => emptyPromptDraft()}
|
|
213
|
+
onChange={(prompts) => onPatch({ prompts })}
|
|
214
|
+
renderItem={(prompt, index) => (
|
|
215
|
+
<>
|
|
216
|
+
<TextField
|
|
217
|
+
prefix="imsc"
|
|
218
|
+
id={`imsc-prname-${index}`}
|
|
219
|
+
label={t('card.field.name')}
|
|
232
220
|
value={prompt.name}
|
|
233
221
|
disabled={disabled}
|
|
234
|
-
|
|
222
|
+
onEdit={(v) => onPatchPrompt(index, { name: v })}
|
|
235
223
|
/>
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
224
|
+
<CodeField
|
|
225
|
+
prefix="imsc"
|
|
226
|
+
id={`imsc-prtext-${index}`}
|
|
227
|
+
label={t('card.field.text')}
|
|
228
|
+
value={prompt.text}
|
|
239
229
|
disabled={disabled}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
className="imsc-textarea"
|
|
247
|
-
aria-label={t('card.field.text')}
|
|
248
|
-
value={prompt.text}
|
|
249
|
-
disabled={disabled}
|
|
250
|
-
onChange={(event) => onPatchPrompt(index, { text: event.target.value })}
|
|
251
|
-
/>
|
|
252
|
-
</div>
|
|
253
|
-
))}
|
|
254
|
-
<button
|
|
255
|
-
type="button"
|
|
256
|
-
className="imsc-btn imsc-btnGhost"
|
|
257
|
-
disabled={disabled}
|
|
258
|
-
onClick={() => onPatch({ prompts: [...draft.prompts, emptyPromptDraft()] })}
|
|
259
|
-
>
|
|
260
|
-
{t('card.add')} · {t('card.prompts')}
|
|
261
|
-
</button>
|
|
262
|
-
</>
|
|
230
|
+
onEdit={(v) => onPatchPrompt(index, { text: v })}
|
|
231
|
+
/>
|
|
232
|
+
</>
|
|
233
|
+
)}
|
|
234
|
+
/>
|
|
235
|
+
</Section>
|
|
263
236
|
)
|
|
264
237
|
}
|
|
265
238
|
|
|
266
239
|
export function ParamPresetSection(props: PresetSectionsProps): ReactElement {
|
|
267
240
|
const { t, draft, disabled, onPatch, onPatchParam } = props
|
|
241
|
+
const endpoints = [
|
|
242
|
+
{ value: 'generation', label: t('tab.generation') },
|
|
243
|
+
{ value: 'edit', label: t('tab.edit') },
|
|
244
|
+
]
|
|
268
245
|
return (
|
|
269
|
-
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
246
|
+
<Section prefix="imsc" title={t('card.params')} description={t('card.paramsHint')}>
|
|
247
|
+
<ListEditor
|
|
248
|
+
prefix="imsc"
|
|
249
|
+
items={draft.params}
|
|
250
|
+
labels={listLabels(t)}
|
|
251
|
+
disabled={disabled}
|
|
252
|
+
titleOf={(param) => param.name || '—'}
|
|
253
|
+
create={() => emptyParamDraft()}
|
|
254
|
+
onChange={(params) => onPatch({ params })}
|
|
255
|
+
renderItem={(param, index) => (
|
|
256
|
+
<>
|
|
257
|
+
<TextField
|
|
258
|
+
prefix="imsc"
|
|
259
|
+
id={`imsc-paname-${index}`}
|
|
260
|
+
label={t('card.field.name')}
|
|
278
261
|
value={param.name}
|
|
279
262
|
disabled={disabled}
|
|
280
|
-
|
|
263
|
+
onEdit={(v) => onPatchParam(index, { name: v })}
|
|
281
264
|
/>
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
265
|
+
<SelectField
|
|
266
|
+
prefix="imsc"
|
|
267
|
+
id={`imsc-paendpoint-${index}`}
|
|
268
|
+
label={t('card.field.endpoint')}
|
|
285
269
|
value={param.endpoint}
|
|
270
|
+
options={endpoints}
|
|
286
271
|
disabled={disabled}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
className="imsc-btn imsc-btnGhost"
|
|
272
|
+
onEdit={(v) => onPatchParam(index, { endpoint: v })}
|
|
273
|
+
/>
|
|
274
|
+
<CodeField
|
|
275
|
+
prefix="imsc"
|
|
276
|
+
id={`imsc-paspecs-${index}`}
|
|
277
|
+
label={t('card.field.paramSpecs')}
|
|
278
|
+
value={param.specsText}
|
|
295
279
|
disabled={disabled}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
className="imsc-textarea"
|
|
303
|
-
aria-label={t('card.field.paramSpecs')}
|
|
304
|
-
placeholder='{ "size": { "enabled": true, "value": "1024x1024" } }'
|
|
305
|
-
value={param.specsText}
|
|
306
|
-
disabled={disabled}
|
|
307
|
-
onChange={(event) => onPatchParam(index, { specsText: event.target.value })}
|
|
308
|
-
/>
|
|
309
|
-
</div>
|
|
310
|
-
))}
|
|
311
|
-
<button
|
|
312
|
-
type="button"
|
|
313
|
-
className="imsc-btn imsc-btnGhost"
|
|
314
|
-
disabled={disabled}
|
|
315
|
-
onClick={() => onPatch({ params: [...draft.params, emptyParamDraft()] })}
|
|
316
|
-
>
|
|
317
|
-
{t('card.add')} · {t('card.params')}
|
|
318
|
-
</button>
|
|
319
|
-
</>
|
|
280
|
+
onEdit={(v) => onPatchParam(index, { specsText: v })}
|
|
281
|
+
/>
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
/>
|
|
285
|
+
</Section>
|
|
320
286
|
)
|
|
321
287
|
}
|
package/src/client/styles.ts
CHANGED
|
@@ -559,29 +559,6 @@ export const imageStudioCss = `
|
|
|
559
559
|
|
|
560
560
|
/* ── 配置卡片附加件(imsc- 基础类由 cardCss('imsc') 生成)── */
|
|
561
561
|
.imsc-loading{padding:14px 16px;margin:0;font-size:13px;color:var(--dsw-alias-label-tertiary)}
|
|
562
|
-
.imsc-groupTitle { margin: 14px 0 6px; font-size: 13px; font-weight: 600; }
|
|
563
|
-
.imsc-presetBox {
|
|
564
|
-
border-top: 1px solid var(--dsw-alias-border-l3);
|
|
565
|
-
padding: 10px 0 2px;
|
|
566
|
-
display: grid;
|
|
567
|
-
gap: 8px;
|
|
568
|
-
}
|
|
569
|
-
.imsc-presetHead { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
570
|
-
.imsc-presetName { font-size: 13px; font-weight: 600; }
|
|
571
|
-
.imsc-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
572
|
-
.imsc-textarea {
|
|
573
|
-
box-sizing: border-box;
|
|
574
|
-
width: 100%;
|
|
575
|
-
min-height: 64px;
|
|
576
|
-
padding: 6px 10px;
|
|
577
|
-
border: 1px solid var(--dsw-alias-border-l2);
|
|
578
|
-
border-radius: 8px;
|
|
579
|
-
background: var(--dsw-alias-bg-layer-3);
|
|
580
|
-
color: var(--dsw-alias-label-primary);
|
|
581
|
-
font-family: var(--ds-font-family-code);
|
|
582
|
-
font-size: 12px;
|
|
583
|
-
resize: vertical;
|
|
584
|
-
}
|
|
585
562
|
.imsc-credRow { display: flex; align-items: center; gap: 8px; }
|
|
586
563
|
.imsc-credRow .imsc-input { flex: 1; min-width: 0; }
|
|
587
564
|
|
|
@@ -617,7 +594,6 @@ export const imageStudioCss = `
|
|
|
617
594
|
.ims-paramControl { width: 108px; }
|
|
618
595
|
.ims-btn { min-height: 40px; }
|
|
619
596
|
.ims-btnSmall { min-height: 32px; }
|
|
620
|
-
.imsc-grid2 { grid-template-columns: 1fr; }
|
|
621
597
|
}
|
|
622
598
|
`
|
|
623
599
|
|
package/src/credentials.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 凭据引用名派生与校验:提供商预设 id → IMAGE_STUDIO_PRESET_<ID>。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 命名与归一走 `@dsh-plus/shared` 的公共助手,本模块只补预设 id 的形态校验。
|
|
4
|
+
* 纯函数,零依赖(浏览器半亦可安全 import)。
|
|
5
5
|
* @module image-studio/credentials
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { credentialRefName } from '@dsh-plus/shared'
|
|
9
|
+
|
|
10
|
+
/** 属主段(插件 id)。 */
|
|
11
|
+
const REF_OWNER = 'image-studio'
|
|
12
|
+
/** 业务段前缀(与属主一起构成 IMAGE_STUDIO_PRESET_<ID>)。 */
|
|
13
|
+
const REF_KIND = 'preset'
|
|
10
14
|
|
|
11
15
|
/** 预设 id 是否合法(kebab-case,用于引用名派生)。 */
|
|
12
16
|
export function isValidPresetId(id: string): boolean {
|
|
@@ -18,5 +22,5 @@ export function credentialRefNameOf(presetId: string): string {
|
|
|
18
22
|
if (!isValidPresetId(presetId)) {
|
|
19
23
|
throw new Error(`非法提供商预设 id:${JSON.stringify(presetId)}`)
|
|
20
24
|
}
|
|
21
|
-
return
|
|
25
|
+
return credentialRefName(REF_OWNER, REF_KIND, presetId)
|
|
22
26
|
}
|
package/src/uploads/store.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
11
11
|
import { appendFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
12
12
|
import { join } from 'node:path'
|
|
13
13
|
|
|
14
|
-
import { atomicWriteFile, commitTmpFile,
|
|
14
|
+
import { atomicWriteFile, commitTmpFile, pluginCachePath } from '@dsh-plus/shared'
|
|
15
15
|
import { isImageId } from '../images/id.ts'
|
|
16
16
|
import { extOfMime } from '../images/mime.ts'
|
|
17
17
|
|
|
@@ -27,9 +27,12 @@ export interface UploadEntry {
|
|
|
27
27
|
size: number
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* 存储层根路径(惰性解析:DSH_HOME 可能在模块加载后才被测试环境设置)。
|
|
32
|
+
* 上传是带 TTL 的暂存派生物(`uploadTtlHours` 回收),故落缓存目录。
|
|
33
|
+
*/
|
|
31
34
|
function uploadsRoot(): string {
|
|
32
|
-
return
|
|
35
|
+
return pluginCachePath('image-studio', 'uploads')
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
function indexFile(): string {
|