@dsh-plus/image-studio 0.1.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 +21 -0
- package/lib/client.js +129 -0
- package/lib/index.d.ts +321 -0
- package/lib/index.js +1304 -0
- package/package.json +60 -0
- package/src/api.ts +213 -0
- package/src/client/api.ts +89 -0
- package/src/client/card.tsx +14 -0
- package/src/client/client.ts +75 -0
- package/src/client/i18n.ts +20 -0
- package/src/client/section.tsx +14 -0
- package/src/client/styles.ts +33 -0
- package/src/config.ts +90 -0
- package/src/credentials.ts +22 -0
- package/src/dto.ts +87 -0
- package/src/errors.ts +47 -0
- package/src/gallery/store.ts +172 -0
- package/src/index.ts +44 -0
- package/src/ns.ts +7 -0
- package/src/params/catalog.ts +184 -0
- package/src/params/spec.ts +109 -0
- package/src/provider/openai-images.ts +266 -0
- package/src/provider/registry.ts +54 -0
- package/src/provider/types.ts +86 -0
- package/src/service.ts +458 -0
- package/src/task/runner.ts +200 -0
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dsh-plus/image-studio",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dsh-plus service+ui plugin: 图像工作室——OpenAI Images 协议文生图/图生图,全参数开关化,双预设体系,并发生图任务与可二次编辑画廊",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./src/*": "./src/*",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"https-proxy-agent": "^9.1.0",
|
|
23
|
+
"@dsh-plus/shared": "0.1.11"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
27
|
+
"@deepseek-ai/dsh-credentials": "*",
|
|
28
|
+
"@deepseek-ai/dsh-host-webserver": "*",
|
|
29
|
+
"@deepseek-ai/dsh-settings": "*",
|
|
30
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
34
|
+
"@deepseek-ai/dsh-credentials": "0.1.5-alpha.1",
|
|
35
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.5-alpha.1",
|
|
36
|
+
"@deepseek-ai/dsh-session": "0.1.5-alpha.1",
|
|
37
|
+
"@deepseek-ai/dsh-settings": "0.1.5-alpha.1",
|
|
38
|
+
"@deepseek-ai/schemastery": "3.18.2",
|
|
39
|
+
"@types/node": "26.4.0",
|
|
40
|
+
"@types/react": "~18.3.31",
|
|
41
|
+
"react": "^18.2.0"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"client": {
|
|
48
|
+
"inject": [
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
50
|
+
"@deepseek-ai/dsh-client-locale",
|
|
51
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
52
|
+
],
|
|
53
|
+
"platform": "web"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsdown",
|
|
58
|
+
"watch": "tsdown --watch"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* image-studio HTTP 端点(同源 webServer,与 usage-panel 同一暴露面约定):
|
|
3
|
+
* - POST generate / GET tasks|tasks/<id> / POST tasks/<id>/cancel
|
|
4
|
+
* - GET gallery|gallery/<id> / DELETE gallery/<id> / GET images/<id>
|
|
5
|
+
* - GET|POST|DELETE credentials(describe/set/unset,值永不回传)
|
|
6
|
+
* - GET providers(协议 registry + 参数目录,前端表单依据)
|
|
7
|
+
* @module image-studio/api
|
|
8
|
+
*/
|
|
9
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
10
|
+
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
13
|
+
import type { GenerateRequest } from './dto.ts'
|
|
14
|
+
import { ImageStudioError, statusOfCode } from './errors.ts'
|
|
15
|
+
import { PARAM_CATALOG } from './params/catalog.ts'
|
|
16
|
+
import { ParamValidationError } from './params/spec.ts'
|
|
17
|
+
import { listProtocols } from './provider/registry.ts'
|
|
18
|
+
import type { ImageStudioService } from './service.ts'
|
|
19
|
+
|
|
20
|
+
const ROUTE_PREFIX = '/dsh-plus/image-studio'
|
|
21
|
+
|
|
22
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
23
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
24
|
+
res.end(JSON.stringify(body))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const chunks: Buffer[] = []
|
|
30
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
31
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
32
|
+
req.on('error', reject)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 统一错误结算:ImageStudioError/ParamValidationError 按码映射,其余 500。 */
|
|
37
|
+
function sendError(res: ServerResponse, error: unknown): void {
|
|
38
|
+
if (error instanceof ImageStudioError) {
|
|
39
|
+
sendJson(res, statusOfCode(error.code), { error: error.code, message: error.message })
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof ParamValidationError) {
|
|
43
|
+
sendJson(res, 400, { error: 'invalid-request', message: error.message })
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
sendJson(res, 500, {
|
|
47
|
+
error: 'internal',
|
|
48
|
+
message: error instanceof Error ? error.message : String(error),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 端点处理函数表(path 段 → handler)。 */
|
|
53
|
+
type Handler = (req: IncomingMessage, res: ServerResponse, rest: string) => Promise<void>
|
|
54
|
+
|
|
55
|
+
function buildHandlers(service: ImageStudioService): Record<string, Handler> {
|
|
56
|
+
return {
|
|
57
|
+
async generate(_req, res, rest) {
|
|
58
|
+
if (rest !== '') throw new ImageStudioError('invalid-request', '未知路径')
|
|
59
|
+
if (method(_req) !== 'POST') {
|
|
60
|
+
sendJson(res, 405, { error: 'POST only' })
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const body = JSON.parse(await readBody(_req)) as GenerateRequest
|
|
64
|
+
const taskId = await service.submitGenerate(body)
|
|
65
|
+
sendJson(res, 202, { taskId })
|
|
66
|
+
},
|
|
67
|
+
async tasks(_req, res, rest) {
|
|
68
|
+
// /tasks 与 /tasks/<id> 与 /tasks/<id>/cancel
|
|
69
|
+
if (rest === '') {
|
|
70
|
+
sendJson(res, 200, { tasks: service.taskWires() })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const [taskId, action] = rest.split('/')
|
|
74
|
+
if (taskId === undefined || taskId.length === 0) {
|
|
75
|
+
throw new ImageStudioError('invalid-request', '缺少任务 id')
|
|
76
|
+
}
|
|
77
|
+
if (action === 'cancel') {
|
|
78
|
+
if (method(_req) !== 'POST') {
|
|
79
|
+
sendJson(res, 405, { error: 'POST only' })
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const ok = service.cancelTask(taskId)
|
|
83
|
+
sendJson(res, ok ? 200 : 409, ok ? { ok: true } : { error: 'task-not-runnable' })
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (action !== undefined) throw new ImageStudioError('invalid-request', `未知操作:${action}`)
|
|
87
|
+
const wire = service.taskWire(taskId)
|
|
88
|
+
if (wire === null) throw new ImageStudioError('unknown-task', `任务不存在:${taskId}`)
|
|
89
|
+
sendJson(res, 200, wire)
|
|
90
|
+
},
|
|
91
|
+
async gallery(_req, res, rest) {
|
|
92
|
+
const [itemId] = rest.split('/')
|
|
93
|
+
if (itemId === undefined || itemId === '') {
|
|
94
|
+
if (method(_req) !== 'GET') {
|
|
95
|
+
sendJson(res, 405, { error: 'GET only' })
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
const items = await service.gallery()
|
|
99
|
+
sendJson(res, 200, { items, total: items.length })
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
if (itemId === 'credentials' || itemId === 'providers') return
|
|
103
|
+
if (method(_req) === 'DELETE') {
|
|
104
|
+
const removed = await service.deleteGalleryItem(itemId)
|
|
105
|
+
sendJson(res, removed ? 200 : 404, removed ? { ok: true } : { error: 'not-found' })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (method(_req) !== 'GET') {
|
|
109
|
+
sendJson(res, 405, { error: 'GET or DELETE only' })
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
const items = await service.gallery()
|
|
113
|
+
const item = items.find((entry) => entry.id === itemId)
|
|
114
|
+
if (item === undefined) {
|
|
115
|
+
sendJson(res, 404, { error: 'not-found' })
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
sendJson(res, 200, item)
|
|
119
|
+
},
|
|
120
|
+
async images(_req, res, rest) {
|
|
121
|
+
const imageId = rest.replace(/\/$/, '')
|
|
122
|
+
if (imageId.length === 0 || imageId.includes('/')) {
|
|
123
|
+
sendJson(res, 404, { error: 'not-found' })
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
const bytes = await service.imageBytes(imageId)
|
|
127
|
+
if (bytes === null) {
|
|
128
|
+
sendJson(res, 404, { error: 'not-found' })
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const mime = bytes.ext === 'jpg' ? 'image/jpeg' : `image/${bytes.ext}`
|
|
132
|
+
res.writeHead(200, { 'content-type': mime, 'cache-control': 'private, max-age=3600' })
|
|
133
|
+
res.end(Buffer.from(bytes.data))
|
|
134
|
+
},
|
|
135
|
+
async credentials(_req, res, rest) {
|
|
136
|
+
const refName = rest.replace(/\/$/, '')
|
|
137
|
+
if (refName.length === 0) {
|
|
138
|
+
sendJson(res, 400, { error: 'invalid-request', message: '缺少凭据引用名' })
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (method(_req) === 'GET') {
|
|
142
|
+
sendJson(res, 200, await service.credentialStatus(refName))
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (method(_req) === 'POST') {
|
|
146
|
+
const body = JSON.parse(await readBody(_req)) as { value?: unknown }
|
|
147
|
+
if (typeof body.value !== 'string' || body.value.length === 0) {
|
|
148
|
+
sendJson(res, 400, { error: 'invalid-request', message: 'value 必须是非空字符串' })
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
await service.setCredential(refName, body.value)
|
|
152
|
+
sendJson(res, 200, { ok: true })
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
if (method(_req) === 'DELETE') {
|
|
156
|
+
await service.unsetCredential(refName)
|
|
157
|
+
sendJson(res, 200, { ok: true })
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
sendJson(res, 405, { error: 'GET/POST/DELETE only' })
|
|
161
|
+
},
|
|
162
|
+
async providers(_req, res, rest) {
|
|
163
|
+
if (rest !== '' || method(_req) !== 'GET') {
|
|
164
|
+
sendJson(res, rest !== '' ? 404 : 405, { error: 'GET /providers only' })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
sendJson(res, 200, {
|
|
168
|
+
protocols: listProtocols(),
|
|
169
|
+
params: PARAM_CATALOG,
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
async presets(_req, res, rest) {
|
|
173
|
+
// 预设读写走官方 settings RPC;本端点仅提供只读快照(懒人通道)。
|
|
174
|
+
if (rest !== '' || method(_req) !== 'GET') {
|
|
175
|
+
sendJson(res, 405, { error: 'GET /presets only' })
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
sendJson(res, 200, service.presets())
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 请求方法小写化。 */
|
|
184
|
+
function method(req: IncomingMessage): string {
|
|
185
|
+
return (req.method ?? 'GET').toUpperCase()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 路由分发(prefix 单注册 + 内部分段)。 */
|
|
189
|
+
export function registerImageStudioApi(ctx: Context, service: ImageStudioService): void {
|
|
190
|
+
const handlers = buildHandlers(service)
|
|
191
|
+
ctx.webServer.register({
|
|
192
|
+
kind: 'prefix',
|
|
193
|
+
path: ROUTE_PREFIX,
|
|
194
|
+
handler: (req, res) => {
|
|
195
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
196
|
+
const segments = url.pathname.slice(ROUTE_PREFIX.length).replace(/^\//, '')
|
|
197
|
+
const [head, ...tail] = segments.split('/')
|
|
198
|
+
const rest = tail.join('/')
|
|
199
|
+
void (async () => {
|
|
200
|
+
if (head === undefined || head === '') {
|
|
201
|
+
sendJson(res, 404, { error: 'unknown endpoint' })
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
const handler = handlers[head]
|
|
205
|
+
if (handler === undefined) {
|
|
206
|
+
sendJson(res, 404, { error: `unknown endpoint: ${head}` })
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
await handler(req, res, rest)
|
|
210
|
+
})().catch((error: unknown) => sendError(res, error))
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据端点通道(前端预留):同源 fetch 封装,类型与 dto.ts 对齐。
|
|
3
|
+
* 本次仅交付通道层,不交付调用它的界面。
|
|
4
|
+
* @module image-studio/client/api
|
|
5
|
+
*/
|
|
6
|
+
import { getJson, postJson } from '@dsh-plus/shared/client'
|
|
7
|
+
import type {
|
|
8
|
+
CredentialStatusWire,
|
|
9
|
+
GalleryItem,
|
|
10
|
+
GalleryListWire,
|
|
11
|
+
GenerateAccepted,
|
|
12
|
+
GenerateRequest,
|
|
13
|
+
PresetsWire,
|
|
14
|
+
ProvidersWire,
|
|
15
|
+
TaskWire,
|
|
16
|
+
} from '../dto.ts'
|
|
17
|
+
|
|
18
|
+
const BASE = '/dsh-plus/image-studio'
|
|
19
|
+
|
|
20
|
+
/** 提交生图任务。 */
|
|
21
|
+
export function generate(request: GenerateRequest): Promise<GenerateAccepted> {
|
|
22
|
+
return postJson<GenerateAccepted>(`${BASE}/generate`, request)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 批量任务轮询。 */
|
|
26
|
+
export function fetchTasks(): Promise<{ tasks: TaskWire[] }> {
|
|
27
|
+
return getJson(`${BASE}/tasks`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 单任务查询。 */
|
|
31
|
+
export function fetchTask(taskId: string): Promise<TaskWire> {
|
|
32
|
+
return getJson(`${BASE}/tasks/${taskId}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 取消任务。 */
|
|
36
|
+
export function cancelTask(taskId: string): Promise<{ ok: boolean }> {
|
|
37
|
+
return postJson(`${BASE}/tasks/${taskId}/cancel`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 画廊列表。 */
|
|
41
|
+
export function fetchGallery(): Promise<GalleryListWire> {
|
|
42
|
+
return getJson(`${BASE}/gallery`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 画廊条目详情(含参数回放)。 */
|
|
46
|
+
export function fetchGalleryItem(itemId: string): Promise<GalleryItem> {
|
|
47
|
+
return getJson(`${BASE}/gallery/${itemId}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 删除画廊条目。 */
|
|
51
|
+
export function deleteGalleryItem(itemId: string): Promise<{ ok: boolean }> {
|
|
52
|
+
return fetch(`${BASE}/gallery/${itemId}`, {
|
|
53
|
+
method: 'DELETE',
|
|
54
|
+
credentials: 'same-origin',
|
|
55
|
+
}).then((res) => res.json() as Promise<{ ok: boolean }>)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 图片 URL(<img src> 直连,非 fetch)。 */
|
|
59
|
+
export function imageUrl(imageId: string): string {
|
|
60
|
+
return `${BASE}/images/${imageId}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 凭据状态(describe,永不回传值)。 */
|
|
64
|
+
export function fetchCredentialStatus(refName: string): Promise<CredentialStatusWire> {
|
|
65
|
+
return getJson(`${BASE}/credentials/${refName}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 设置凭据值。 */
|
|
69
|
+
export function setCredential(refName: string, value: string): Promise<{ ok: boolean }> {
|
|
70
|
+
return postJson(`${BASE}/credentials/${refName}`, { value })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 删除凭据。 */
|
|
74
|
+
export function unsetCredential(refName: string): Promise<{ ok: boolean }> {
|
|
75
|
+
return fetch(`${BASE}/credentials/${refName}`, {
|
|
76
|
+
method: 'DELETE',
|
|
77
|
+
credentials: 'same-origin',
|
|
78
|
+
}).then((res) => res.json() as Promise<{ ok: boolean }>)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 协议 registry + 参数目录。 */
|
|
82
|
+
export function fetchProviders(): Promise<ProvidersWire> {
|
|
83
|
+
return getJson(`${BASE}/providers`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 预设只读快照(读写走官方 settings RPC)。 */
|
|
87
|
+
export function fetchPresets(): Promise<PresetsWire> {
|
|
88
|
+
return getJson(`${BASE}/presets`)
|
|
89
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片占位组件:本次交付为空壳(不做前端设计与编码)。
|
|
3
|
+
* 后续版本在此渲染三组预设编辑与凭据管理;keyed 槽位契约已声明。
|
|
4
|
+
* @module image-studio/client/card
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 占位组件。 */
|
|
8
|
+
export function StudioConfigCard(props: { t?: (key: string) => string }): unknown {
|
|
9
|
+
const t = props.t ?? ((key: string) => key)
|
|
10
|
+
const el = document.createElement('div')
|
|
11
|
+
el.className = 'dsh-plus-image-studio-card-placeholder'
|
|
12
|
+
el.textContent = t('cardPlaceholder')
|
|
13
|
+
return { type: el, props: {} }
|
|
14
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器半入口(本次预留空壳,不做界面设计与编码):
|
|
3
|
+
* - 注册独立设置页占位(settings.section),声明画廊/生图页后续挂载点;
|
|
4
|
+
* - 注册 settings.plugin.item 配置卡片占位(key = settings 命名空间)。
|
|
5
|
+
* 数据通道与插槽契约见 docs/README.md「前端接入点」。
|
|
6
|
+
* @module @dsh-plus/image-studio/client
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
9
|
+
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'
|
|
13
|
+
|
|
14
|
+
export const name = 'dsh-plus-image-studio'
|
|
15
|
+
|
|
16
|
+
/** 浏览器半需要的 cordis 服务 key(loader 据此注入)。 */
|
|
17
|
+
export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
|
|
18
|
+
|
|
19
|
+
interface SlotsLike {
|
|
20
|
+
inject(key: string, callback: () => unknown): unknown
|
|
21
|
+
register(options: Record<string, unknown>, component: unknown): () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface LocaleLike {
|
|
25
|
+
register(ns: string, dict: { zh: Record<string, string>; en: Record<string, string> }): () => void
|
|
26
|
+
bind(ns: string): (key: string) => string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ClientContext {
|
|
30
|
+
slots: SlotsLike
|
|
31
|
+
locale: LocaleLike
|
|
32
|
+
effect(execute: () => () => void, label?: string): unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function apply(ctx: Context): void {
|
|
36
|
+
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
|
+
)
|
|
46
|
+
c.effect(() => c.locale.register(NS, { zh, en }), 'image-studio: locale')
|
|
47
|
+
const t = c.locale.bind(NS)
|
|
48
|
+
|
|
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
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// 配置卡片占位(settings.plugin.item:keyed 槽位,key = settings 命名空间)。
|
|
64
|
+
c.slots.inject('settings.plugin.item', () =>
|
|
65
|
+
c.slots.register(
|
|
66
|
+
{
|
|
67
|
+
name: 'settings.plugin.item',
|
|
68
|
+
key: 'dsh-plus-image-studio',
|
|
69
|
+
locale: NS,
|
|
70
|
+
inject: () => ({ t }),
|
|
71
|
+
},
|
|
72
|
+
StudioConfigCard,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n 字典(zh 为主,en 兜底);NS 与 settings 命名空间分离,
|
|
3
|
+
* 仅承载前端文案 key。本次只含占位文案。
|
|
4
|
+
* @module image-studio/client/i18n
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** locale 命名空间。 */
|
|
8
|
+
export const NS = 'dsh-plus-image-studio'
|
|
9
|
+
|
|
10
|
+
export const zh = {
|
|
11
|
+
nav: '图像工作室',
|
|
12
|
+
placeholder: '生图工作台与画廊将在后续版本提供',
|
|
13
|
+
cardPlaceholder: '图像工作室配置(预设/凭据管理将在后续版本提供)',
|
|
14
|
+
}
|
|
15
|
+
|
|
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)',
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 独立设置页占位组件:本次交付为空壳(不做前端设计与编码)。
|
|
3
|
+
* 后续版本在此渲染生图工作台与画廊;插槽契约已按官方 settings.section 形态声明。
|
|
4
|
+
* @module image-studio/client/section
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 占位组件(props 形态对齐 settings.section 四 share 注入)。 */
|
|
8
|
+
export function StudioSection(props: { t?: (key: string) => string }): unknown {
|
|
9
|
+
const t = props.t ?? ((key: string) => key)
|
|
10
|
+
const el = document.createElement('div')
|
|
11
|
+
el.className = 'dsh-plus-image-studio-section-placeholder'
|
|
12
|
+
el.textContent = t('placeholder')
|
|
13
|
+
return { type: el, props: {} }
|
|
14
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 画廊/生图占位样式:本次交付仅注入极简占位类名(无视觉设计)。
|
|
3
|
+
* 沿用官方约定 <style data-plugin data-plugin-css>,HMR 按此卸载。
|
|
4
|
+
* @module image-studio/client/styles
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const PKG = '@dsh-plus/image-studio'
|
|
8
|
+
|
|
9
|
+
/** 注入设置页占位样式。 */
|
|
10
|
+
export function injectSectionStyle(): HTMLStyleElement | null {
|
|
11
|
+
return inject(
|
|
12
|
+
'dsh-plus-image-studio-section',
|
|
13
|
+
'.dsh-plus-image-studio-section-placeholder{padding:16px;opacity:.6}',
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 注入配置卡片占位样式。 */
|
|
18
|
+
export function injectCardStyles(): HTMLStyleElement | null {
|
|
19
|
+
return inject(
|
|
20
|
+
'dsh-plus-image-studio-card',
|
|
21
|
+
'.dsh-plus-image-studio-card-placeholder{padding:8px;opacity:.6}',
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function inject(id: string, css: string): HTMLStyleElement | null {
|
|
26
|
+
if (typeof document === 'undefined') return null
|
|
27
|
+
const tag = document.createElement('style')
|
|
28
|
+
tag.dataset.plugin = PKG
|
|
29
|
+
tag.dataset.pluginCss = `${PKG}/${id}`
|
|
30
|
+
tag.textContent = css
|
|
31
|
+
document.head.appendChild(tag)
|
|
32
|
+
return tag
|
|
33
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置单一事实源:cordis 行级 Config(组合默认)与 settings namespace
|
|
3
|
+
* (用户层,$DSH_HOME/settings.yaml 热生效)共用同一 schemastery schema。
|
|
4
|
+
* 三组纯用户配置:提示词预设 / 参数预设 / 提供商预设(API key 不在此,
|
|
5
|
+
* 走 credentials,引用名由 preset id 派生)。
|
|
6
|
+
* @module image-studio/config
|
|
7
|
+
*/
|
|
8
|
+
import z from '@deepseek-ai/schemastery'
|
|
9
|
+
|
|
10
|
+
import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
|
|
11
|
+
|
|
12
|
+
/** settings 命名空间(字面量即合法命名空间,编译期校验)。 */
|
|
13
|
+
export const SETTINGS_NS = NS_LITERAL
|
|
14
|
+
|
|
15
|
+
// dts 可移植性:嵌套 schema 精确类型经 TypeT 消费,显式断掉推断链
|
|
16
|
+
// (同 usage-panel/config 的既有约定)。
|
|
17
|
+
/* biome-ignore lint/suspicious/noExplicitAny: 嵌套 schema 的 dts 可移植性 */
|
|
18
|
+
const PromptPreset: any = z.object({
|
|
19
|
+
id: z.string().required().description('预设 id(稳定标识,删除级联引用)'),
|
|
20
|
+
name: z.string().required().description('预设名称'),
|
|
21
|
+
text: z.string().required().description('提示词模板'),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/* biome-ignore lint/suspicious/noExplicitAny: 嵌套 schema 的 dts 可移植性 */
|
|
25
|
+
const ParamPreset: any = z.object({
|
|
26
|
+
id: z.string().required().description('预设 id'),
|
|
27
|
+
name: z.string().required().description('预设名称'),
|
|
28
|
+
endpoint: z.string().required().description('适用端点:generation | edit'),
|
|
29
|
+
/** 参数表结构由 params/spec 校验,settings 层存原始 JSON。 */
|
|
30
|
+
paramSpecs: z.any().required().description('参数开关表(ParamSpecMap JSON)'),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/* biome-ignore lint/suspicious/noExplicitAny: 嵌套 schema 的 dts 可移植性 */
|
|
34
|
+
const ProviderPreset: any = z.object({
|
|
35
|
+
id: z.string().required().description('预设 id'),
|
|
36
|
+
name: z.string().required().description('预设名称'),
|
|
37
|
+
protocol: z.string().required().description('协议 id(如 openai-images)'),
|
|
38
|
+
baseUrl: z.string().required().description('API 基础地址(含 /v1)'),
|
|
39
|
+
model: z.string().required().description('模型 id'),
|
|
40
|
+
credentialRef: z.string().required().description('凭据引用名(IMAGE_STUDIO_PRESET_*)'),
|
|
41
|
+
extraHeaders: z.dict(z.string()).description('附加请求头(部分中转需要)').default({}),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
export const Config = z.object({
|
|
45
|
+
promptPresets: z.array(PromptPreset).description('提示词预设').default([]),
|
|
46
|
+
paramPresets: z.array(ParamPreset).description('参数预设').default([]),
|
|
47
|
+
providerPresets: z.array(ProviderPreset).description('提供商预设').default([]),
|
|
48
|
+
maxConcurrent: z.number().min(0).max(16).description('生图任务并发上限(0 = 不限)').default(3),
|
|
49
|
+
requestTimeoutMs: z
|
|
50
|
+
.number()
|
|
51
|
+
.min(10_000)
|
|
52
|
+
.max(600_000)
|
|
53
|
+
.description('单请求超时(毫秒;生图长耗时,默认 5 分钟)')
|
|
54
|
+
.default(300_000),
|
|
55
|
+
proxy: z.string().description('上游请求代理(空 = 直连)').default(''),
|
|
56
|
+
galleryMaxItems: z
|
|
57
|
+
.number()
|
|
58
|
+
.min(0)
|
|
59
|
+
.max(10_000)
|
|
60
|
+
.description('画廊保留上限(条,0 = 不清理;超限按最旧删除)')
|
|
61
|
+
.default(500),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export type ImageStudioConfig = Schemastery.TypeT<typeof Config>
|
|
65
|
+
|
|
66
|
+
/** 提示词预设(解析后视图)。 */
|
|
67
|
+
export interface PromptPresetEntry {
|
|
68
|
+
id: string
|
|
69
|
+
name: string
|
|
70
|
+
text: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 参数预设(解析后视图)。 */
|
|
74
|
+
export interface ParamPresetEntry {
|
|
75
|
+
id: string
|
|
76
|
+
name: string
|
|
77
|
+
endpoint: 'generation' | 'edit'
|
|
78
|
+
paramSpecs: unknown
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 提供商预设(解析后视图)。 */
|
|
82
|
+
export interface ProviderPresetEntry {
|
|
83
|
+
id: string
|
|
84
|
+
name: string
|
|
85
|
+
protocol: string
|
|
86
|
+
baseUrl: string
|
|
87
|
+
model: string
|
|
88
|
+
credentialRef: string
|
|
89
|
+
extraHeaders: Record<string, string>
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 凭据引用名派生与校验:提供商预设 id → IMAGE_STUDIO_PRESET_<ID>。
|
|
3
|
+
* 规则对齐《插件存储规范》:大写下划线、全大写、前缀属主。
|
|
4
|
+
* 纯函数,零依赖。
|
|
5
|
+
* @module image-studio/credentials
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 引用名前缀(属主段)。 */
|
|
9
|
+
const REF_PREFIX = 'IMAGE_STUDIO_PRESET_'
|
|
10
|
+
|
|
11
|
+
/** 预设 id 是否合法(kebab-case,用于引用名派生)。 */
|
|
12
|
+
export function isValidPresetId(id: string): boolean {
|
|
13
|
+
return /^[a-z][a-z0-9-]*$/.test(id) && id.length <= 64
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 预设 id → 凭据引用名(kebab → SNAKE 大写)。 */
|
|
17
|
+
export function credentialRefNameOf(presetId: string): string {
|
|
18
|
+
if (!isValidPresetId(presetId)) {
|
|
19
|
+
throw new Error(`非法提供商预设 id:${JSON.stringify(presetId)}`)
|
|
20
|
+
}
|
|
21
|
+
return `${REF_PREFIX}${presetId.replaceAll('-', '_').toUpperCase()}`
|
|
22
|
+
}
|