@dsh-plus/image-studio 0.1.0 → 0.1.1

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.
@@ -1,21 +1,36 @@
1
1
  /**
2
- * 浏览器半入口(本次预留空壳,不做界面设计与编码):
3
- * - 注册独立设置页占位(settings.section),声明画廊/生图页后续挂载点;
4
- * - 注册 settings.plugin.item 配置卡片占位(key = settings 命名空间)。
5
- * 数据通道与插槽契约见 docs/README.md「前端接入点」。
2
+ * 浏览器半入口:
3
+ * - sidebar.footer.action:侧栏底部入口按钮(与文件/终端入口 flex 等分行宽);
4
+ * - shell.overlay:图像工作室面板(文生图/图生图/画廊 + 任务条);
5
+ * - settings.plugin.item:配置卡片(三组预设 + 凭据 + 高级项,key = settings 命名空间)。
6
+ * 模式与 web-files/web-terminal 一致:CJS factory 产物、data-plugin-css 样式
7
+ * 约定、slot 失配静默降级;配置读写经 ctx.remote.settings 直连(远程域名可用)。
6
8
  * @module @dsh-plus/image-studio/client
7
9
  */
8
10
  import type { Context } from '@deepseek-ai/cordis'
11
+ import {
12
+ cardCss,
13
+ createNamespaceApi,
14
+ createSettingsScope,
15
+ injectCardStyle,
16
+ type ScopeHostContext,
17
+ } from '@dsh-plus/shared/client'
18
+
19
+ import { SETTINGS_NS } from '../ns.ts'
9
20
  import { StudioConfigCard } from './card.tsx'
10
- import { en, NS, zh } from './i18n.ts'
11
- import { StudioSection } from './section.tsx'
12
- import { injectCardStyles, injectSectionStyle } from './styles.ts'
21
+ import { en, NS, type Translate, zh } from './i18n.ts'
22
+ import { createPanelController } from './panel/controller.ts'
23
+ import { StudioEntryButton } from './panel/entry.tsx'
24
+ import { StudioPanel } from './panel/panel.tsx'
25
+ import { injectStudioStyles } from './styles.ts'
13
26
 
14
27
  export const name = 'dsh-plus-image-studio'
15
28
 
16
- /** 浏览器半需要的 cordis 服务 key(loader 据此注入)。 */
29
+ /** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
17
30
  export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
18
31
 
32
+ const PLUGIN_ID = '@dsh-plus/image-studio'
33
+
19
34
  interface SlotsLike {
20
35
  inject(key: string, callback: () => unknown): unknown
21
36
  register(options: Record<string, unknown>, component: unknown): () => void
@@ -26,50 +41,68 @@ interface LocaleLike {
26
41
  bind(ns: string): (key: string) => string
27
42
  }
28
43
 
29
- interface ClientContext {
44
+ /** createSettingsScope 要求的宿主窄面 + 本插件用到的 slots/locale 服务。 */
45
+ interface ClientContext extends ScopeHostContext {
30
46
  slots: SlotsLike
31
47
  locale: LocaleLike
32
- effect(execute: () => () => void, label?: string): unknown
33
48
  }
34
49
 
35
50
  export function apply(ctx: Context): void {
36
51
  const c = ctx as unknown as ClientContext
37
- const sectionTag = injectSectionStyle()
38
- const cardTag = injectCardStyles()
39
- c.effect(
40
- () => () => {
41
- sectionTag?.remove()
42
- cardTag?.remove()
43
- },
44
- 'image-studio: style',
45
- )
52
+ const panelTag = injectStudioStyles()
53
+ const cardTag = injectCardStyle(PLUGIN_ID, cardCss('imsc'))
54
+ const controller = createPanelController()
46
55
  c.effect(() => c.locale.register(NS, { zh, en }), 'image-studio: locale')
47
- const t = c.locale.bind(NS)
56
+ const t = c.locale.bind(NS) as Translate
48
57
 
49
- // 独立设置页占位(画廊 + 生图工作台后续版本替换占位组件)。
50
- c.slots.inject('settings.section', () =>
51
- c.slots.register(
52
- {
53
- name: 'settings.section',
54
- id: 'dsh-plus-image-studio',
55
- order: 16,
56
- label: () => t('nav'),
57
- inject: () => ({ t }),
58
- },
59
- StudioSection,
60
- ),
58
+ c.effect(
59
+ () =>
60
+ c.slots.inject('sidebar.footer.action', () =>
61
+ c.slots.register(
62
+ {
63
+ name: 'sidebar.footer.action',
64
+ id: 'image-studio-entry',
65
+ locale: NS,
66
+ inject: () => ({ studio: controller, t }),
67
+ },
68
+ StudioEntryButton,
69
+ ),
70
+ ),
71
+ 'image-studio: entry slot',
72
+ )
73
+ const scope = createSettingsScope(c, SETTINGS_NS, 'image-studio: settings scope')
74
+ const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
75
+ c.effect(
76
+ () =>
77
+ c.slots.inject('shell.overlay', () =>
78
+ c.slots.register(
79
+ {
80
+ name: 'shell.overlay',
81
+ id: 'image-studio-panel',
82
+ locale: NS,
83
+ inject: () => ({ studio: controller, t, scope, api }),
84
+ },
85
+ StudioPanel,
86
+ ),
87
+ ),
88
+ 'image-studio: overlay slot',
61
89
  )
62
-
63
- // 配置卡片占位(settings.plugin.item:keyed 槽位,key = settings 命名空间)。
64
90
  c.slots.inject('settings.plugin.item', () =>
65
91
  c.slots.register(
66
92
  {
67
93
  name: 'settings.plugin.item',
68
- key: 'dsh-plus-image-studio',
94
+ key: SETTINGS_NS,
69
95
  locale: NS,
70
- inject: () => ({ t }),
96
+ inject: () => ({ t, scope, api }),
71
97
  },
72
98
  StudioConfigCard,
73
99
  ),
74
100
  )
101
+ c.effect(
102
+ () => () => {
103
+ panelTag?.remove()
104
+ cardTag?.remove()
105
+ },
106
+ 'image-studio: cleanup',
107
+ )
75
108
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * i18n 字典(zh 为主,en 兜底);NS 与 settings 命名空间分离,
3
- * 仅承载前端文案 key。本次只含占位文案。
2
+ * i18n 字典(zh 为键域事实源,en 全量对译);NS 与 settings 命名空间分离,
3
+ * 仅承载前端文案 key。面板 / 入口 / 配置卡片共用同一命名空间。
4
4
  * @module image-studio/client/i18n
5
5
  */
6
6
 
@@ -8,13 +8,234 @@
8
8
  export const NS = 'dsh-plus-image-studio'
9
9
 
10
10
  export const zh = {
11
- nav: '图像工作室',
12
- placeholder: '生图工作台与画廊将在后续版本提供',
13
- cardPlaceholder: '图像工作室配置(预设/凭据管理将在后续版本提供)',
11
+ // 入口与面板骨架
12
+ 'entry.label': '图像',
13
+ 'panel.title': '图像工作室',
14
+ 'tab.generation': '文生图',
15
+ 'tab.edit': '图生图',
16
+ 'tab.gallery': '画廊',
17
+ 'common.close': '关闭',
18
+ 'common.cancel': '取消',
19
+ 'common.save': '保存',
20
+ 'common.saving': '保存中…',
21
+ 'common.retry': '重试',
22
+ 'common.loading': '加载中…',
23
+ 'common.loadFailed': '加载失败',
24
+ 'common.delete': '删除',
25
+ 'common.confirmDelete': '确认删除',
26
+ 'common.refresh': '刷新',
27
+ 'common.download': '下载',
28
+ 'common.unsaved': '未保存',
29
+ // 提供商
30
+ 'provider.label': '提供商',
31
+ 'provider.placeholder': '选择提供商预设…',
32
+ 'provider.empty': '暂无提供商预设:请到 设置 → 插件 → 图像工作室 添加',
33
+ 'provider.credential.configured': 'Key 已配置',
34
+ 'provider.credential.missing': 'Key 未配置',
35
+ 'provider.inline.toggle': '临时覆盖协议/地址/模型',
36
+ 'provider.inline.protocol': '协议',
37
+ 'provider.inline.baseUrl': 'API 地址',
38
+ 'provider.inline.model': '模型',
39
+ 'provider.inline.hint': '覆盖时凭据沿用所选预设的 API Key',
40
+ // 提示词
41
+ 'prompt.label': '提示词',
42
+ 'prompt.placeholder': '描述想要生成的画面…',
43
+ 'prompt.preset.insert': '插入提示词预设…',
44
+ 'prompt.preset.save': '存为预设',
45
+ 'prompt.preset.name': '预设名称',
46
+ 'prompt.preset.saved': '已保存提示词预设',
47
+ // 参数
48
+ 'params.basic': '常用参数',
49
+ 'params.advanced': '高级参数',
50
+ 'params.preset.placeholder': '应用参数预设…',
51
+ 'params.preset.save': '存为预设',
52
+ 'params.preset.saved': '已保存参数预设',
53
+ 'params.enabled': '启用',
54
+ // 源图与遮罩
55
+ 'sources.label': '源图',
56
+ 'sources.pick': '选择源图',
57
+ 'sources.count': '已选 {n}/16 张',
58
+ 'sources.mask': '遮罩(可选)',
59
+ 'sources.maskPick': '选择遮罩',
60
+ 'sources.maskClear': '清除',
61
+ 'picker.empty': '画廊暂无图片',
62
+ 'picker.confirm': '使用所选({n})',
63
+ // 提交
64
+ 'generate.submit': '生成',
65
+ 'generate.submitted': '已提交任务 {id},进度见下方任务条',
66
+ 'generate.failed': '提交失败:',
67
+ 'generate.needProvider': '请先选择提供商预设',
68
+ 'generate.needPrompt': '请填写提示词',
69
+ 'generate.needSources': '请先选择至少一张源图',
70
+ // 任务条
71
+ 'tasks.title': '任务',
72
+ 'tasks.state.queued': '排队中',
73
+ 'tasks.state.running': '生成中',
74
+ 'tasks.state.succeeded': '已完成',
75
+ 'tasks.state.failed': '失败',
76
+ 'tasks.state.cancelled': '已取消',
77
+ 'tasks.view': '查看',
78
+ // 画廊
79
+ 'gallery.empty': '画廊为空,去文生图生成第一张吧',
80
+ 'gallery.total': '共 {n} 条',
81
+ 'gallery.detail.prompt': '提示词',
82
+ 'gallery.detail.revised': '改写提示词',
83
+ 'gallery.detail.params': '请求参数',
84
+ 'gallery.detail.model': '模型',
85
+ 'gallery.detail.endpoint': '端点',
86
+ 'gallery.detail.time': '时间',
87
+ 'gallery.detail.sources': '源图',
88
+ 'gallery.edit': '二次编辑',
89
+ // 配置卡片
90
+ 'card.title': '图像工作室',
91
+ 'card.description': '文生图/图生图:提供商与 API Key、参数/提示词预设',
92
+ 'card.providers': '提供商预设',
93
+ 'card.prompts': '提示词预设',
94
+ 'card.params': '参数预设',
95
+ 'card.advanced': '高级设置',
96
+ 'card.add': '添加',
97
+ 'card.field.id': 'ID',
98
+ 'card.field.name': '名称',
99
+ 'card.field.protocol': '协议',
100
+ 'card.field.baseUrl': 'API 地址',
101
+ 'card.field.model': '模型',
102
+ 'card.field.extraHeaders': '附加请求头(JSON)',
103
+ 'card.field.text': '提示词模板',
104
+ 'card.field.endpoint': '适用端点',
105
+ 'card.field.paramSpecs': '参数表(JSON:{ "size": { "enabled": true, "value": "1024x1024" } })',
106
+ 'card.idHint': '小写字母开头,仅小写字母/数字/中划线(凭据引用名派生用)',
107
+ 'card.credential.label': 'API Key',
108
+ 'card.credential.placeholder': '输入 Key(保存后不回显)',
109
+ 'card.credential.set': '保存 Key',
110
+ 'card.credential.unset': '删除 Key',
111
+ 'card.credential.configured': '已配置',
112
+ 'card.credential.unconfigured': '未配置',
113
+ 'card.maxConcurrent': '生图并发上限(0 = 不限)',
114
+ 'card.requestTimeoutMs': '单请求超时(毫秒)',
115
+ 'card.proxy': '上游代理(空 = 直连)',
116
+ 'card.galleryMaxItems': '画廊保留上限(0 = 不清理)',
117
+ 'card.specsInvalid': '参数表非法:',
118
+ 'card.idInvalid': 'ID 非法:',
119
+ 'card.save': '保存',
120
+ 'card.discard': '放弃修改',
121
+ 'card.saved': '已保存',
122
+ 'card.saveFailed': '保存失败:',
123
+ 'card.readOnly': '当前部署的设置文档为只读',
124
+ 'card.badgeOn': '{n} 个提供商',
125
+ 'card.badgeOff': '未配置',
14
126
  }
15
127
 
16
- export const en = {
17
- nav: 'Image Studio',
18
- placeholder: 'Image workspace and gallery arrive in a later release',
19
- cardPlaceholder: 'Image Studio settings (preset/credential management arrives later)',
128
+ export type DictKey = keyof typeof zh
129
+
130
+ /** 面板/卡片共用的翻译函数类型(slot 注入的 bind 结果按此消费)。 */
131
+ export type Translate = (key: DictKey) => string
132
+
133
+ export const en: Record<DictKey, string> = {
134
+ 'entry.label': 'Image',
135
+ 'panel.title': 'Image Studio',
136
+ 'tab.generation': 'Text to Image',
137
+ 'tab.edit': 'Image to Image',
138
+ 'tab.gallery': 'Gallery',
139
+ 'common.close': 'Close',
140
+ 'common.cancel': 'Cancel',
141
+ 'common.save': 'Save',
142
+ 'common.saving': 'Saving…',
143
+ 'common.retry': 'Retry',
144
+ 'common.loading': 'Loading…',
145
+ 'common.loadFailed': 'Load failed',
146
+ 'common.delete': 'Delete',
147
+ 'common.confirmDelete': 'Confirm delete',
148
+ 'common.refresh': 'Refresh',
149
+ 'common.download': 'Download',
150
+ 'common.unsaved': 'Unsaved',
151
+ 'provider.label': 'Provider',
152
+ 'provider.placeholder': 'Select a provider preset…',
153
+ 'provider.empty': 'No provider presets: add one in Settings → Plugins → Image Studio',
154
+ 'provider.credential.configured': 'Key configured',
155
+ 'provider.credential.missing': 'Key missing',
156
+ 'provider.inline.toggle': 'Override protocol/URL/model',
157
+ 'provider.inline.protocol': 'Protocol',
158
+ 'provider.inline.baseUrl': 'API base URL',
159
+ 'provider.inline.model': 'Model',
160
+ 'provider.inline.hint': 'Overrides reuse the API key of the selected preset',
161
+ 'prompt.label': 'Prompt',
162
+ 'prompt.placeholder': 'Describe the image you want…',
163
+ 'prompt.preset.insert': 'Insert prompt preset…',
164
+ 'prompt.preset.save': 'Save as preset',
165
+ 'prompt.preset.name': 'Preset name',
166
+ 'prompt.preset.saved': 'Prompt preset saved',
167
+ 'params.basic': 'Common parameters',
168
+ 'params.advanced': 'Advanced parameters',
169
+ 'params.preset.placeholder': 'Apply a parameter preset…',
170
+ 'params.preset.save': 'Save as preset',
171
+ 'params.preset.saved': 'Parameter preset saved',
172
+ 'params.enabled': 'Enabled',
173
+ 'sources.label': 'Source images',
174
+ 'sources.pick': 'Pick sources',
175
+ 'sources.count': '{n}/16 selected',
176
+ 'sources.mask': 'Mask (optional)',
177
+ 'sources.maskPick': 'Pick mask',
178
+ 'sources.maskClear': 'Clear',
179
+ 'picker.empty': 'No images in the gallery yet',
180
+ 'picker.confirm': 'Use selected ({n})',
181
+ 'generate.submit': 'Generate',
182
+ 'generate.submitted': 'Task {id} submitted; progress in the task bar below',
183
+ 'generate.failed': 'Submit failed: ',
184
+ 'generate.needProvider': 'Select a provider preset first',
185
+ 'generate.needPrompt': 'Enter a prompt first',
186
+ 'generate.needSources': 'Pick at least one source image',
187
+ 'tasks.title': 'Tasks',
188
+ 'tasks.state.queued': 'Queued',
189
+ 'tasks.state.running': 'Running',
190
+ 'tasks.state.succeeded': 'Done',
191
+ 'tasks.state.failed': 'Failed',
192
+ 'tasks.state.cancelled': 'Cancelled',
193
+ 'tasks.view': 'View',
194
+ 'gallery.empty': 'Gallery is empty — generate your first image',
195
+ 'gallery.total': '{n} items',
196
+ 'gallery.detail.prompt': 'Prompt',
197
+ 'gallery.detail.revised': 'Revised prompt',
198
+ 'gallery.detail.params': 'Request parameters',
199
+ 'gallery.detail.model': 'Model',
200
+ 'gallery.detail.endpoint': 'Endpoint',
201
+ 'gallery.detail.time': 'Time',
202
+ 'gallery.detail.sources': 'Sources',
203
+ 'gallery.edit': 'Edit again',
204
+ 'card.title': 'Image Studio',
205
+ 'card.description': 'Text/Image-to-image: providers & API keys, param/prompt presets',
206
+ 'card.providers': 'Provider presets',
207
+ 'card.prompts': 'Prompt presets',
208
+ 'card.params': 'Parameter presets',
209
+ 'card.advanced': 'Advanced',
210
+ 'card.add': 'Add',
211
+ 'card.field.id': 'ID',
212
+ 'card.field.name': 'Name',
213
+ 'card.field.protocol': 'Protocol',
214
+ 'card.field.baseUrl': 'API base URL',
215
+ 'card.field.model': 'Model',
216
+ 'card.field.extraHeaders': 'Extra headers (JSON)',
217
+ 'card.field.text': 'Prompt template',
218
+ 'card.field.endpoint': 'Endpoint',
219
+ 'card.field.paramSpecs':
220
+ 'Param specs (JSON: { "size": { "enabled": true, "value": "1024x1024" } })',
221
+ 'card.idHint': 'Lowercase letter first; lowercase/digits/dashes only (derives credential ref)',
222
+ 'card.credential.label': 'API Key',
223
+ 'card.credential.placeholder': 'Enter key (never echoed back)',
224
+ 'card.credential.set': 'Save key',
225
+ 'card.credential.unset': 'Remove key',
226
+ 'card.credential.configured': 'Configured',
227
+ 'card.credential.unconfigured': 'Not configured',
228
+ 'card.maxConcurrent': 'Max concurrent tasks (0 = unlimited)',
229
+ 'card.requestTimeoutMs': 'Request timeout (ms)',
230
+ 'card.proxy': 'Upstream proxy (empty = direct)',
231
+ 'card.galleryMaxItems': 'Gallery retention (0 = keep all)',
232
+ 'card.specsInvalid': 'Invalid param specs: ',
233
+ 'card.idInvalid': 'Invalid ID: ',
234
+ 'card.save': 'Save',
235
+ 'card.discard': 'Discard',
236
+ 'card.saved': 'Saved',
237
+ 'card.saveFailed': 'Save failed: ',
238
+ 'card.readOnly': 'Settings document is read-only in this deployment',
239
+ 'card.badgeOn': '{n} providers',
240
+ 'card.badgeOff': 'Not configured',
20
241
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 面板开合与跨标签联动控制器:footer 入口 ↔ overlay 面板共享开合态;
3
+ * 画廊「二次编辑」经 editSeed 单向投递给图生图表单(nonce 保证同参可重复触发)。
4
+ * 模式照抄 web-files/web-terminal 的 PanelController。
5
+ * @module image-studio/client/panel/controller
6
+ */
7
+
8
+ /** 面板标签页。 */
9
+ export type StudioTab = 'generation' | 'edit' | 'gallery'
10
+
11
+ /** 二次编辑种子(画廊条目 → 图生图表单回填)。 */
12
+ export interface EditSeed {
13
+ /** 源图 imageId 列表(≤16)。 */
14
+ sourceIds: string[]
15
+ /** 历史提示词回填。 */
16
+ prompt: string
17
+ /** 历史请求参数回填(画廊条目 params 快照)。 */
18
+ params: Record<string, unknown>
19
+ /** 历史提供商预设(存在则预选)。 */
20
+ providerPresetId: string | null
21
+ /** 触发序号:同一参数再次「二次编辑」也需重新生效。 */
22
+ nonce: number
23
+ }
24
+
25
+ /** 面板状态(footer 按钮与 overlay 面板共享)。 */
26
+ export interface PanelState {
27
+ open: boolean
28
+ tab: StudioTab
29
+ editSeed: EditSeed | null
30
+ }
31
+
32
+ /** footer 按钮与 shell.overlay 面板之间共享的控制器。 */
33
+ export interface PanelController {
34
+ subscribe: (listener: () => void) => () => void
35
+ getSnapshot: () => PanelState
36
+ setOpen: (open: boolean) => void
37
+ toggle: () => void
38
+ setTab: (tab: StudioTab) => void
39
+ /** 投递二次编辑种子并切到图生图标签。 */
40
+ seedEdit: (seed: Omit<EditSeed, 'nonce'>) => void
41
+ }
42
+
43
+ /** 创建一个模块级控制器(apply 内调用一次,inject 给两个 slot 入口)。 */
44
+ export function createPanelController(): PanelController {
45
+ let state: PanelState = { open: false, tab: 'generation', editSeed: null }
46
+ let nonce = 0
47
+ const listeners = new Set<() => void>()
48
+ const publish = () => {
49
+ for (const listener of [...listeners]) listener()
50
+ }
51
+ const set = (next: PanelState) => {
52
+ state = next
53
+ publish()
54
+ }
55
+ return {
56
+ subscribe: (listener) => {
57
+ listeners.add(listener)
58
+ return () => listeners.delete(listener)
59
+ },
60
+ getSnapshot: () => state,
61
+ setOpen: (open) => {
62
+ if (state.open === open) return
63
+ set({ ...state, open })
64
+ },
65
+ toggle: () => set({ ...state, open: !state.open }),
66
+ setTab: (tab) => {
67
+ if (state.tab === tab) return
68
+ set({ ...state, tab })
69
+ },
70
+ seedEdit: (seed) => {
71
+ nonce += 1
72
+ set({ ...state, tab: 'edit', editSeed: { ...seed, nonce } })
73
+ },
74
+ }
75
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 侧边栏 footer 入口按钮:与文件/终端入口同款(整行图标 + 文案;
3
+ * rail 折叠态退化为居中圆形图标)。flex:1 由样式层与邻居等分行宽。
4
+ * @module image-studio/client/panel/entry
5
+ */
6
+ import { IconImage } from '@dsh-plus/shared/client'
7
+ import type { ReactElement } from 'react'
8
+ import { useSyncExternalStore } from 'react'
9
+ import type { Translate } from '../i18n.ts'
10
+ import type { PanelController } from './controller.ts'
11
+
12
+ interface EntryProps {
13
+ studio: PanelController
14
+ t: Translate
15
+ /** 侧边栏展开态(slot owner 传入);折叠(rail)时仅渲染圆形图标。 */
16
+ wide?: boolean
17
+ }
18
+
19
+ export function StudioEntryButton({ studio, t, wide }: EntryProps): ReactElement {
20
+ const { open } = useSyncExternalStore(studio.subscribe, studio.getSnapshot)
21
+ const rail = wide === false
22
+ return (
23
+ <button
24
+ type="button"
25
+ className={rail ? 'ims-entry ims-entry-rail' : 'ims-entry'}
26
+ aria-haspopup="dialog"
27
+ aria-expanded={open}
28
+ aria-label={t('entry.label')}
29
+ onClick={() => studio.toggle()}
30
+ >
31
+ <IconImage size={rail ? 18 : 16} />
32
+ {!rail && <span className="ims-entry-label">{t('entry.label')}</span>}
33
+ </button>
34
+ )
35
+ }