@dickpy/dsh-imagegen 1.0.0
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/LICENSE +201 -0
- package/README.md +126 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +2413 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +896 -0
- package/package.json +66 -0
- package/src/client/ImageGenPanel.tsx +687 -0
- package/src/client/SettingsCard.tsx +373 -0
- package/src/client/api.ts +89 -0
- package/src/client/controller.ts +46 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/helpers.ts +33 -0
- package/src/client/index.ts +127 -0
- package/src/client/locales.ts +204 -0
- package/src/client/mount.tsx +119 -0
- package/src/client/panel.module.css +970 -0
- package/src/client/settings-card.module.css +288 -0
- package/src/client/settings-form.ts +324 -0
- package/src/client/settings-scope.ts +227 -0
- package/src/client/sidebar-entry.ts +144 -0
- package/src/engine.ts +284 -0
- package/src/history-store.ts +217 -0
- package/src/index.ts +139 -0
- package/src/protocol.ts +118 -0
- package/src/routes.ts +373 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-imagegen surface copy: zh is the key source, en mirrors every key.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const zh = {
|
|
6
|
+
'entry.label': 'AI 生图',
|
|
7
|
+
'entry.tooltip': 'AI 生图面板(gpt-image-2)',
|
|
8
|
+
'panel.title': 'AI 生图',
|
|
9
|
+
'panel.subtitle': 'gpt-image-2 图像生成',
|
|
10
|
+
// mode
|
|
11
|
+
'mode.text': '文生图',
|
|
12
|
+
'mode.edit': '图生图',
|
|
13
|
+
// prompt
|
|
14
|
+
'prompt.placeholder': '描述你想要的画面,例如:一只戴着宇航员头盔的橘猫,在月球上举起望远镜,水彩风格,柔和光线…',
|
|
15
|
+
'prompt.required': '请输入提示词',
|
|
16
|
+
'prompt.count': '{count}/2000',
|
|
17
|
+
// parameters
|
|
18
|
+
'params.size': '尺寸',
|
|
19
|
+
'params.quality': '清晰度',
|
|
20
|
+
'params.count': '生成数量',
|
|
21
|
+
'params.detail': '细节',
|
|
22
|
+
'size.auto': '自动',
|
|
23
|
+
'size.square': '1024×1024',
|
|
24
|
+
'size.landscape': '1536×1024',
|
|
25
|
+
'size.portrait': '1024×1536',
|
|
26
|
+
'size.small': '512×512',
|
|
27
|
+
'size.wide': '1792×1024',
|
|
28
|
+
'size.tall': '1024×1792',
|
|
29
|
+
'quality.auto': '自动',
|
|
30
|
+
'quality.low': '低',
|
|
31
|
+
'quality.medium': '中',
|
|
32
|
+
'quality.high': '高',
|
|
33
|
+
'count.one': '1 张',
|
|
34
|
+
'count.two': '2 张',
|
|
35
|
+
'count.three': '3 张',
|
|
36
|
+
'count.four': '4 张',
|
|
37
|
+
'detail.auto': '自动',
|
|
38
|
+
'detail.standard': '标准',
|
|
39
|
+
'detail.high': '高清',
|
|
40
|
+
'detail.hint': '透传参数,部分 gpt-image-2 网关支持;官方接口请保持「自动」',
|
|
41
|
+
// footer
|
|
42
|
+
'model.label': '模型',
|
|
43
|
+
'generate': '开始生成',
|
|
44
|
+
'generating': '生成中…',
|
|
45
|
+
// edit reference image
|
|
46
|
+
'edit.upload': '点击或拖拽上传参考图片',
|
|
47
|
+
'edit.uploadHint': 'PNG / JPG / WEBP,不超过 10MB',
|
|
48
|
+
'edit.change': '更换图片',
|
|
49
|
+
'edit.remove': '移除',
|
|
50
|
+
'edit.required': '请先上传参考图片',
|
|
51
|
+
// canvas
|
|
52
|
+
'canvas.emptyTitle': '开始你的创作',
|
|
53
|
+
'canvas.emptyHint': '在左侧输入提示词并点击「开始生成」,结果将显示在这里',
|
|
54
|
+
'canvas.error': '生成失败:{error}',
|
|
55
|
+
'canvas.generating': '正在生成图片…',
|
|
56
|
+
'canvas.elapsed': '已用时 {seconds}s',
|
|
57
|
+
'canvas.images': '本次生成 {count} 张',
|
|
58
|
+
'download': '下载',
|
|
59
|
+
'revisedPrompt': '优化提示词:{prompt}',
|
|
60
|
+
// history
|
|
61
|
+
'history.title': '历史记录',
|
|
62
|
+
'history.empty': '暂无历史记录,生成图片后会保存在这里',
|
|
63
|
+
'history.clear': '清空',
|
|
64
|
+
'history.restore': '恢复',
|
|
65
|
+
'history.delete': '删除',
|
|
66
|
+
'history.images': '张',
|
|
67
|
+
'history.viewing': '历史 · {time}',
|
|
68
|
+
// preview
|
|
69
|
+
'preview.title': '图片预览',
|
|
70
|
+
'preview.open': '点击预览',
|
|
71
|
+
'preview.close': '关闭',
|
|
72
|
+
'preview.prev': '上一张',
|
|
73
|
+
'preview.next': '下一张',
|
|
74
|
+
'preview.index': '{index} / {total}',
|
|
75
|
+
// config banner
|
|
76
|
+
'config.missing': '尚未配置 API:请前往「设置 → 插件 → 可配置」为 AI 生图填写 api_url 与 api_key。',
|
|
77
|
+
'config.configured': '已连接 {url}',
|
|
78
|
+
'config.disabled': '插件已停用,请在设置中重新启用。',
|
|
79
|
+
// settings card
|
|
80
|
+
'settings.title': 'AI 生图(dsh-imagegen)',
|
|
81
|
+
'settings.description': '配置图像生成 API 地址与密钥',
|
|
82
|
+
'settings.apiUrl': 'API 地址(api_url)',
|
|
83
|
+
'settings.apiUrlHint': 'OpenAI 兼容接口基址,如 https://api.openai.com/v1;将自动拼接 /images/generations 与 /images/edits',
|
|
84
|
+
'settings.apiKey': 'API 密钥(api_key)',
|
|
85
|
+
'settings.apiKeyHint': 'Bearer 密钥,明文存于本机设置文档;界面只显示是否已设置',
|
|
86
|
+
'settings.apiKeySet': '已保存密钥;输入新值可更换,点击「清除」可删除',
|
|
87
|
+
'settings.apiKeyClear': '清除',
|
|
88
|
+
'settings.announceToAgent': '向 Agent 播报本插件',
|
|
89
|
+
'settings.announceToAgentHint': '开启后,本插件的存在与能力会写入每个 Agent 的系统提示词',
|
|
90
|
+
'settings.enabled': '启用插件',
|
|
91
|
+
'settings.enabledHint': '关闭后生图面板不可用(设置卡片始终可用)',
|
|
92
|
+
'settings.save': '保存',
|
|
93
|
+
'settings.saving': '保存中…',
|
|
94
|
+
'settings.discard': '放弃修改',
|
|
95
|
+
'settings.unsaved': '有未保存的修改',
|
|
96
|
+
'settings.saveFailed': '保存失败,请重试',
|
|
97
|
+
'settings.readOnly': '当前设置文档为只读,无法保存。',
|
|
98
|
+
'settings.notExposed': '设置命名空间不可用:本部署未提供该插件的设置服务。',
|
|
99
|
+
'settings.expand': '展开',
|
|
100
|
+
'settings.collapse': '收起',
|
|
101
|
+
'settings.inherit': '继承',
|
|
102
|
+
'settings.on': '开',
|
|
103
|
+
'settings.off': '关',
|
|
104
|
+
'settings.overridden': '已覆盖',
|
|
105
|
+
'settings.reset': '重置',
|
|
106
|
+
'settings.invalidNumber': '请输入有效数字',
|
|
107
|
+
} as const
|
|
108
|
+
|
|
109
|
+
export const en: Record<keyof typeof zh, string> = {
|
|
110
|
+
'entry.label': 'AI Image',
|
|
111
|
+
'entry.tooltip': 'AI image generation studio (gpt-image-2)',
|
|
112
|
+
'panel.title': 'AI Image',
|
|
113
|
+
'panel.subtitle': 'gpt-image-2 image generation',
|
|
114
|
+
'mode.text': 'Text to Image',
|
|
115
|
+
'mode.edit': 'Image to Image',
|
|
116
|
+
'prompt.placeholder': 'Describe the picture you want, e.g. an orange cat in an astronaut helmet raising a telescope on the moon, watercolor style, soft light…',
|
|
117
|
+
'prompt.required': 'Enter a prompt first',
|
|
118
|
+
'prompt.count': '{count}/2000',
|
|
119
|
+
'params.size': 'Size',
|
|
120
|
+
'params.quality': 'Quality',
|
|
121
|
+
'params.count': 'Count',
|
|
122
|
+
'params.detail': 'Detail',
|
|
123
|
+
'size.auto': 'Auto',
|
|
124
|
+
'size.square': '1024×1024',
|
|
125
|
+
'size.landscape': '1536×1024',
|
|
126
|
+
'size.portrait': '1024×1536',
|
|
127
|
+
'size.small': '512×512',
|
|
128
|
+
'size.wide': '1792×1024',
|
|
129
|
+
'size.tall': '1024×1792',
|
|
130
|
+
'quality.auto': 'Auto',
|
|
131
|
+
'quality.low': 'Low',
|
|
132
|
+
'quality.medium': 'Medium',
|
|
133
|
+
'quality.high': 'High',
|
|
134
|
+
'count.one': '1',
|
|
135
|
+
'count.two': '2',
|
|
136
|
+
'count.three': '3',
|
|
137
|
+
'count.four': '4',
|
|
138
|
+
'detail.auto': 'Auto',
|
|
139
|
+
'detail.standard': 'Standard',
|
|
140
|
+
'detail.high': 'High',
|
|
141
|
+
'detail.hint': 'Passthrough parameter supported by some gpt-image-2 gateways; keep "Auto" for official endpoints',
|
|
142
|
+
'model.label': 'Model',
|
|
143
|
+
'generate': 'Generate',
|
|
144
|
+
'generating': 'Generating…',
|
|
145
|
+
'edit.upload': 'Click or drag to upload a reference image',
|
|
146
|
+
'edit.uploadHint': 'PNG / JPG / WEBP, up to 10MB',
|
|
147
|
+
'edit.change': 'Change image',
|
|
148
|
+
'edit.remove': 'Remove',
|
|
149
|
+
'edit.required': 'Upload a reference image first',
|
|
150
|
+
'canvas.emptyTitle': 'Start creating',
|
|
151
|
+
'canvas.emptyHint': 'Enter a prompt on the left and click "Generate"; results appear here',
|
|
152
|
+
'canvas.error': 'Generation failed: {error}',
|
|
153
|
+
'canvas.generating': 'Generating images…',
|
|
154
|
+
'canvas.elapsed': 'Elapsed {seconds}s',
|
|
155
|
+
'canvas.images': '{count} image(s) generated',
|
|
156
|
+
'download': 'Download',
|
|
157
|
+
'revisedPrompt': 'Refined prompt: {prompt}',
|
|
158
|
+
'history.title': 'History',
|
|
159
|
+
'history.empty': 'No history yet — generations will be saved here',
|
|
160
|
+
'history.clear': 'Clear',
|
|
161
|
+
'history.restore': 'Restore',
|
|
162
|
+
'history.delete': 'Delete',
|
|
163
|
+
'history.images': 'images',
|
|
164
|
+
'history.viewing': 'History · {time}',
|
|
165
|
+
'preview.title': 'Image preview',
|
|
166
|
+
'preview.open': 'Click to preview',
|
|
167
|
+
'preview.close': 'Close',
|
|
168
|
+
'preview.prev': 'Previous',
|
|
169
|
+
'preview.next': 'Next',
|
|
170
|
+
'preview.index': '{index} / {total}',
|
|
171
|
+
'config.missing': 'API not configured: open "Settings → Web UI Plugins" and fill in api_url and api_key for AI Image.',
|
|
172
|
+
'config.configured': 'Connected to {url}',
|
|
173
|
+
'config.disabled': 'The plugin is disabled — re-enable it in Settings.',
|
|
174
|
+
'settings.title': 'AI Image (dsh-imagegen)',
|
|
175
|
+
'settings.description': 'Configure the image generation API endpoint and key',
|
|
176
|
+
'settings.apiUrl': 'API URL (api_url)',
|
|
177
|
+
'settings.apiUrlHint': 'OpenAI-compatible base URL, e.g. https://api.openai.com/v1; /images/generations and /images/edits are appended',
|
|
178
|
+
'settings.apiKey': 'API Key (api_key)',
|
|
179
|
+
'settings.apiKeyHint': 'Bearer key, stored in plaintext in the local settings document; the UI only shows whether it is set',
|
|
180
|
+
'settings.apiKeySet': 'A key is stored; type a new value to replace it, or click "Clear" to remove it',
|
|
181
|
+
'settings.apiKeyClear': 'Clear',
|
|
182
|
+
'settings.announceToAgent': 'Announce this plugin to agents',
|
|
183
|
+
'settings.announceToAgentHint': 'When on, the plugin presence and capabilities are written into every agent system prompt',
|
|
184
|
+
'settings.enabled': 'Enable plugin',
|
|
185
|
+
'settings.enabledHint': 'When off, the generation studio is unavailable (this card stays available)',
|
|
186
|
+
'settings.save': 'Save',
|
|
187
|
+
'settings.saving': 'Saving…',
|
|
188
|
+
'settings.discard': 'Discard',
|
|
189
|
+
'settings.unsaved': 'Unsaved changes',
|
|
190
|
+
'settings.saveFailed': 'Save failed, please retry',
|
|
191
|
+
'settings.readOnly': 'The settings document is read-only; saving is disabled.',
|
|
192
|
+
'settings.notExposed': 'Settings namespace unavailable: this deployment does not serve this plugin\'s settings.',
|
|
193
|
+
'settings.expand': 'Expand',
|
|
194
|
+
'settings.collapse': 'Collapse',
|
|
195
|
+
'settings.inherit': 'Inherit',
|
|
196
|
+
'settings.on': 'On',
|
|
197
|
+
'settings.off': 'Off',
|
|
198
|
+
'settings.overridden': 'Overridden',
|
|
199
|
+
'settings.reset': 'Reset',
|
|
200
|
+
'settings.invalidNumber': 'Enter a valid number',
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Locale key union. */
|
|
204
|
+
export type ImageGenKey = keyof typeof zh
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel view mounting.
|
|
3
|
+
*
|
|
4
|
+
* The `conversation` slot is single-occupant (ui-conversation) and external
|
|
5
|
+
* plugins cannot declare slots, so the panel takes over the center column at
|
|
6
|
+
* the DOM level: a container is appended inside the `[data-pane="conversation"]`
|
|
7
|
+
* grid item (an extra trailing child React never manages), and a stylesheet
|
|
8
|
+
* rule hides the conversation content while the panel is active. Toggling is
|
|
9
|
+
* a data attribute on <html> — no React involvement, so the conversation
|
|
10
|
+
* subtree underneath stays mounted and stateful.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
14
|
+
import type { ImageGenApi } from './api.ts'
|
|
15
|
+
import type { ImageGenController } from './controller.ts'
|
|
16
|
+
import { ImageGenPanel } from './ImageGenPanel.tsx'
|
|
17
|
+
import type { ImageGenScope } from './settings-scope.ts'
|
|
18
|
+
import css from './panel.module.css'
|
|
19
|
+
|
|
20
|
+
/** The injected panel container (kept in the DOM, hidden when inactive). */
|
|
21
|
+
export const PANEL_VIEW_SELECTOR = '[data-dsh-imagegen-view]'
|
|
22
|
+
|
|
23
|
+
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
|
|
24
|
+
const ACTIVE_ATTR = 'data-dsh-imagegen-active'
|
|
25
|
+
/** Sibling panels' activation attributes, removed when this panel opens. */
|
|
26
|
+
const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
|
|
27
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
28
|
+
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
29
|
+
const PANEL_NAME = 'imagegen'
|
|
30
|
+
|
|
31
|
+
/** Find the center column, or undefined while the frame is not mounted. */
|
|
32
|
+
function conversationColumn(): HTMLElement | undefined {
|
|
33
|
+
return document.querySelector<HTMLElement>(CONVERSATION_COLUMN_SELECTOR) ?? undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Mount the panel React tree into the center column and bind its visibility
|
|
38
|
+
* to the controller's panelOpen state.
|
|
39
|
+
* @param controller - the panel controller driving the view.
|
|
40
|
+
* @param api - the image-generation API client the panel operates through.
|
|
41
|
+
* @param scope - the settings scope (config status banner).
|
|
42
|
+
* @returns disposer unmounting the tree and restoring the column.
|
|
43
|
+
*/
|
|
44
|
+
export function mountPanel(
|
|
45
|
+
controller: ImageGenController,
|
|
46
|
+
api: ImageGenApi,
|
|
47
|
+
scope: ImageGenScope,
|
|
48
|
+
): () => void {
|
|
49
|
+
let root: Root | undefined
|
|
50
|
+
let container: HTMLDivElement | undefined
|
|
51
|
+
|
|
52
|
+
const ensure = (): void => {
|
|
53
|
+
if (container !== undefined) {
|
|
54
|
+
if (container.isConnected) return
|
|
55
|
+
// The conversation pane was replaced; drop the stale tree and remount.
|
|
56
|
+
root?.unmount()
|
|
57
|
+
root = undefined
|
|
58
|
+
container.remove()
|
|
59
|
+
container = undefined
|
|
60
|
+
}
|
|
61
|
+
const column = conversationColumn()
|
|
62
|
+
if (column === undefined) return
|
|
63
|
+
container = document.createElement('div')
|
|
64
|
+
container.dataset.dshImagegenView = ''
|
|
65
|
+
container.className = css.view
|
|
66
|
+
column.appendChild(container)
|
|
67
|
+
root = createRoot(container)
|
|
68
|
+
root.render(<ImageGenPanel api={api} scope={scope} />)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The frame mounts after boot settlement; watch for the column's arrival.
|
|
72
|
+
const waitObserver = new MutationObserver(() => { ensure() })
|
|
73
|
+
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
74
|
+
|
|
75
|
+
const applyActive = (): void => {
|
|
76
|
+
if (controller.getSnapshot().panelOpen) {
|
|
77
|
+
// Single-occupant center column: opening this panel must evict sibling
|
|
78
|
+
// panels (task board / ssh), both their html attributes and their
|
|
79
|
+
// controller states, otherwise the visibility rules fight.
|
|
80
|
+
for (const attr of OTHER_ACTIVE_ATTRS) document.documentElement.removeAttribute(attr)
|
|
81
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
82
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
|
|
83
|
+
} else {
|
|
84
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const onOtherActivate = (event: Event): void => {
|
|
88
|
+
const detail = (event as CustomEvent).detail
|
|
89
|
+
if ((detail === 'ssh' || detail === 'taskboard') && controller.getSnapshot().panelOpen) {
|
|
90
|
+
controller.close()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Jump out on sidebar context clicks: clicking a session/workspace row
|
|
94
|
+
// hands the center column back to the conversation. Capture phase.
|
|
95
|
+
const SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
96
|
+
const onClickSidebarRow = (event: MouseEvent): void => {
|
|
97
|
+
if (!controller.getSnapshot().panelOpen) return
|
|
98
|
+
const target = event.target as HTMLElement | null
|
|
99
|
+
if (target === null) return
|
|
100
|
+
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.close()
|
|
101
|
+
}
|
|
102
|
+
document.addEventListener('click', onClickSidebarRow, true)
|
|
103
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
104
|
+
const unsubscribe = controller.subscribe(applyActive)
|
|
105
|
+
applyActive()
|
|
106
|
+
ensure()
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
document.removeEventListener('click', onClickSidebarRow, true)
|
|
110
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
111
|
+
waitObserver.disconnect()
|
|
112
|
+
unsubscribe()
|
|
113
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
114
|
+
root?.unmount()
|
|
115
|
+
root = undefined
|
|
116
|
+
container?.remove()
|
|
117
|
+
container = undefined
|
|
118
|
+
}
|
|
119
|
+
}
|