@dickpy/dsh-imagegen 1.2.2 → 1.3.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/README.md +24 -11
- package/docs/images/community-qq.png +0 -0
- package/lib/client.js +2040 -906
- package/lib/client.js.map +1 -1
- package/lib/index.js +710 -109
- package/package.json +2 -2
- package/src/agent-image-tools.ts +132 -30
- package/src/client/ImageGenPanel.tsx +56 -20
- package/src/client/SettingsCard.tsx +505 -196
- package/src/client/channels-form.ts +263 -0
- package/src/client/image-toolview.tsx +19 -12
- package/src/client/locales.ts +112 -2
- package/src/client/settings-card.module.css +487 -0
- package/src/client/settings-scope.ts +56 -8
- package/src/engine.ts +107 -8
- package/src/gallery-store.ts +6 -0
- package/src/generation-runtime.ts +32 -5
- package/src/history-store.ts +6 -0
- package/src/image-format.ts +11 -0
- package/src/image-models.ts +1 -1
- package/src/index.ts +153 -47
- package/src/model-catalog.ts +98 -0
- package/src/presets.ts +63 -0
- package/src/protocol.ts +81 -6
- package/src/routes.ts +192 -24
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staged form model for the channel list of the settings card. Mirrors the
|
|
3
|
+
* CardForm staging pattern (dirty → one save) but for a structured value, so
|
|
4
|
+
* the card can edit N channels, per-channel keys, and the default-channel
|
|
5
|
+
* flag, then persist everything in one revision-fenced mutate call.
|
|
6
|
+
*
|
|
7
|
+
* Storage rules (dictated by dsh-settings semantics):
|
|
8
|
+
* - the whole `channels` array is written wholesale via `path: ['channels']`;
|
|
9
|
+
* - every channel's API key lives at `channelSecrets.<channelId>` (a secret
|
|
10
|
+
* dict), written per-key so untouched keys are never clobbered by a save
|
|
11
|
+
* the reader could not see (keys are redacted out of the wire view);
|
|
12
|
+
* - path ops never navigate *inside* the channels array.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
16
|
+
import type { ChannelConfig, ModelMapping } from '../protocol.ts'
|
|
17
|
+
import type { ImageGenScope, SettingsOp } from './settings-scope.ts'
|
|
18
|
+
|
|
19
|
+
/** One channel as the editor stages it (secrets never travel here). */
|
|
20
|
+
export interface ChannelDraft {
|
|
21
|
+
id: string
|
|
22
|
+
preset: string
|
|
23
|
+
name: string
|
|
24
|
+
apiUrl: string
|
|
25
|
+
models: ModelMapping[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One staged key edit for a channel. */
|
|
29
|
+
export type KeyEdit = { kind: 'set'; value: string } | { kind: 'clear' }
|
|
30
|
+
|
|
31
|
+
/** The state the card renders. */
|
|
32
|
+
export interface ChannelsFormState {
|
|
33
|
+
/** Staged channel list (the scope value when nothing is staged). */
|
|
34
|
+
channels: ChannelDraft[]
|
|
35
|
+
/** Which channels currently hold a stored secret (staged edits included). */
|
|
36
|
+
keySet: Record<string, boolean>
|
|
37
|
+
/** The effective default channel id. */
|
|
38
|
+
defaultChannelId: string
|
|
39
|
+
/** Whether a save would write anything. */
|
|
40
|
+
dirty: boolean
|
|
41
|
+
/** Whether the document accepts writes. */
|
|
42
|
+
writable: boolean
|
|
43
|
+
/** Whether a save is crossing the wire. */
|
|
44
|
+
saving: boolean
|
|
45
|
+
/** Whether the last save failed (cleared by the next edit or save). */
|
|
46
|
+
failed: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The actions the card's slot entry injects. */
|
|
50
|
+
export interface ChannelsFormActions {
|
|
51
|
+
/** Replace the whole channel list (add/edit/remove go through here). */
|
|
52
|
+
setChannels: (channels: ChannelDraft[]) => void
|
|
53
|
+
/** Stage a key for one channel ('' clears; undefined = no staged change). */
|
|
54
|
+
setChannelKey: (id: string, value: string | undefined) => void
|
|
55
|
+
/** Stage the default-channel flag. */
|
|
56
|
+
setDefaultChannel: (id: string) => void
|
|
57
|
+
/** Write every staged edit, then re-seed from what the Host accepted. */
|
|
58
|
+
commit: () => Promise<void>
|
|
59
|
+
/** Drop every staged edit. */
|
|
60
|
+
discard: () => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Deep equality over JSON-compatible data (the change predicate). */
|
|
64
|
+
function deepEqualJson(a: unknown, b: unknown): boolean {
|
|
65
|
+
if (a === b) return true
|
|
66
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false
|
|
67
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
68
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false
|
|
69
|
+
return a.every((entry, index) => deepEqualJson(entry, b[index]))
|
|
70
|
+
}
|
|
71
|
+
const left = a as Record<string, unknown>
|
|
72
|
+
const right = b as Record<string, unknown>
|
|
73
|
+
const keys = Object.keys(left)
|
|
74
|
+
if (keys.length !== Object.keys(right).length) return false
|
|
75
|
+
return keys.every(key => key in right && deepEqualJson(left[key], right[key]))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Trim and normalize a draft channel (models never carry empty aliases). */
|
|
79
|
+
function stripChannel(channel: ChannelDraft): ChannelDraft {
|
|
80
|
+
const models = channel.models
|
|
81
|
+
.map(model => ({ alias: model.alias.trim(), id: model.id.trim() === '' ? model.alias.trim() : model.id.trim() }))
|
|
82
|
+
.filter(model => model.alias !== '')
|
|
83
|
+
return {
|
|
84
|
+
id: channel.id,
|
|
85
|
+
preset: channel.preset,
|
|
86
|
+
name: channel.name.trim(),
|
|
87
|
+
apiUrl: channel.apiUrl.trim(),
|
|
88
|
+
models: [...new Map(models.map(model => [model.alias, model])).values()],
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class ChannelsForm {
|
|
93
|
+
private stagedChannels: ChannelDraft[] | null = null
|
|
94
|
+
private readonly stagedKeys = new Map<string, KeyEdit>()
|
|
95
|
+
private stagedDefault: string | null = null
|
|
96
|
+
private readonly listeners = new Set<() => void>()
|
|
97
|
+
private saving = false
|
|
98
|
+
private failed = false
|
|
99
|
+
|
|
100
|
+
constructor(private readonly scope: ImageGenScope) {
|
|
101
|
+
scope.subscribe(() => { this.publish() })
|
|
102
|
+
scope.subscribeSecretSets(() => { this.publish() })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Publish a projection of this form, rebuilt on every scope or draft change. */
|
|
106
|
+
bind<S>(project: () => S): SnapshotStore<S> {
|
|
107
|
+
const store = createSnapshotStore(project())
|
|
108
|
+
this.listeners.add(() => { store.set(project()) })
|
|
109
|
+
return store
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Subscribe to staged and persisted channel changes. */
|
|
113
|
+
subscribe(listener: () => void): () => void {
|
|
114
|
+
this.listeners.add(listener)
|
|
115
|
+
return () => { this.listeners.delete(listener) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The staged channel list, or the scope value when nothing is staged. */
|
|
119
|
+
private channelsValue(): ChannelDraft[] {
|
|
120
|
+
const view = this.scope.getSnapshot().value as { channels?: ChannelConfig[] } | undefined
|
|
121
|
+
return this.stagedChannels ?? (Array.isArray(view?.channels) ? view.channels.map(toDraft) : [])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Whether a channel currently holds a stored or staged secret. */
|
|
125
|
+
private keyHeld(id: string): boolean {
|
|
126
|
+
const edit = this.stagedKeys.get(id)
|
|
127
|
+
if (edit !== undefined) return edit.kind === 'set' && edit.value !== ''
|
|
128
|
+
return this.scope.getSecretSetSnapshot(`channelSecrets.${id}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private defaultValue(): string {
|
|
132
|
+
if (this.stagedDefault !== null) return this.stagedDefault
|
|
133
|
+
const view = this.scope.getSnapshot().value as { defaultChannelId?: string } | undefined
|
|
134
|
+
const channels = this.channelsValue()
|
|
135
|
+
if (view?.defaultChannelId !== undefined && channels.some(channel => channel.id === view.defaultChannelId)) return view.defaultChannelId
|
|
136
|
+
return channels[0]?.id ?? ''
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private dirtyValue(): boolean {
|
|
140
|
+
const channels = this.channelsValue()
|
|
141
|
+
const stagedChanged = this.stagedChannels !== null && !deepEqualJson(this.stagedChannels, scopeChannelsOf(this.scope))
|
|
142
|
+
const scopeView = this.scope.getSnapshot().value as { defaultChannelId?: string } | undefined
|
|
143
|
+
const scopeDefault = scopeView?.defaultChannelId ?? channels[0]?.id ?? ''
|
|
144
|
+
const defaultChanged = this.stagedDefault !== null && this.stagedDefault !== scopeDefault
|
|
145
|
+
return stagedChanged || defaultChanged || this.stagedKeys.size > 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The card-facing snapshot. */
|
|
149
|
+
snapshot(): ChannelsFormState {
|
|
150
|
+
const channels = this.channelsValue()
|
|
151
|
+
const keySet: Record<string, boolean> = {}
|
|
152
|
+
for (const channel of channels) keySet[channel.id] = this.keyHeld(channel.id)
|
|
153
|
+
return {
|
|
154
|
+
channels,
|
|
155
|
+
keySet,
|
|
156
|
+
defaultChannelId: this.defaultValue(),
|
|
157
|
+
dirty: this.dirtyValue(),
|
|
158
|
+
writable: this.scope.getSnapshot().writable !== false,
|
|
159
|
+
saving: this.saving,
|
|
160
|
+
failed: this.failed,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The actions the card's slot registration injects. */
|
|
165
|
+
actions(): ChannelsFormActions {
|
|
166
|
+
return {
|
|
167
|
+
setChannels: (channels) => { this.stageChannels(channels) },
|
|
168
|
+
setChannelKey: (id, value) => { this.stageKey(id, value) },
|
|
169
|
+
setDefaultChannel: (id) => { this.stagedDefault = id; this.failed = false; this.publish() },
|
|
170
|
+
commit: () => this.commit(),
|
|
171
|
+
discard: () => {
|
|
172
|
+
if (this.stagedChannels === null && this.stagedKeys.size === 0 && this.stagedDefault === null && !this.failed) return
|
|
173
|
+
this.stagedChannels = null
|
|
174
|
+
this.stagedKeys.clear()
|
|
175
|
+
this.stagedDefault = null
|
|
176
|
+
this.failed = false
|
|
177
|
+
this.publish()
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ------------------------------------------------------------------ staging
|
|
183
|
+
|
|
184
|
+
private stageChannels(channels: ChannelDraft[]): void {
|
|
185
|
+
const cleaned = channels.map(stripChannel)
|
|
186
|
+
this.stagedChannels = cleaned
|
|
187
|
+
this.failed = false
|
|
188
|
+
this.publish()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private stageKey(id: string, value: string | undefined): void {
|
|
192
|
+
if (value === undefined || value.trim() === '') {
|
|
193
|
+
if (this.keyHeld(id)) this.stagedKeys.set(id, { kind: 'clear' })
|
|
194
|
+
// An empty key for a key-less channel stages nothing.
|
|
195
|
+
} else {
|
|
196
|
+
this.stagedKeys.set(id, { kind: 'set', value: value.trim() })
|
|
197
|
+
}
|
|
198
|
+
this.failed = false
|
|
199
|
+
this.publish()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ----------------------------------------------------------------- save
|
|
203
|
+
|
|
204
|
+
/** Build the single batch of path ops a save performs. */
|
|
205
|
+
private planOps(): SettingsOp[] {
|
|
206
|
+
const ops: SettingsOp[] = []
|
|
207
|
+
if (this.stagedChannels !== null) {
|
|
208
|
+
ops.push({ op: 'set', path: ['channels'], value: this.stagedChannels })
|
|
209
|
+
// Once channels exist, the legacy flat fields are obsolete (idempotent).
|
|
210
|
+
ops.push({ op: 'unset', path: ['apiUrl'] })
|
|
211
|
+
ops.push({ op: 'unset', path: ['apiKey'] })
|
|
212
|
+
ops.push({ op: 'unset', path: ['imageModels'] })
|
|
213
|
+
}
|
|
214
|
+
for (const [id, edit] of this.stagedKeys) {
|
|
215
|
+
if (edit.kind === 'set') ops.push({ op: 'set', path: ['channelSecrets', id], value: edit.value })
|
|
216
|
+
else ops.push({ op: 'unset', path: ['channelSecrets', id] })
|
|
217
|
+
}
|
|
218
|
+
if (this.stagedDefault !== null) {
|
|
219
|
+
ops.push({ op: 'set', path: ['defaultChannelId'], value: this.stagedDefault })
|
|
220
|
+
}
|
|
221
|
+
return ops
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Write every staged edit, then re-seed from what the Host accepted.
|
|
226
|
+
* @returns settlement after the write settles.
|
|
227
|
+
*/
|
|
228
|
+
async commit(): Promise<void> {
|
|
229
|
+
if (this.saving) return
|
|
230
|
+
const ops = this.planOps()
|
|
231
|
+
if (ops.length === 0) return
|
|
232
|
+
this.saving = true
|
|
233
|
+
this.failed = false
|
|
234
|
+
this.publish()
|
|
235
|
+
try {
|
|
236
|
+
await this.scope.mutateOps(ops)
|
|
237
|
+
this.stagedChannels = null
|
|
238
|
+
this.stagedKeys.clear()
|
|
239
|
+
this.stagedDefault = null
|
|
240
|
+
this.failed = false
|
|
241
|
+
} catch {
|
|
242
|
+
this.failed = true
|
|
243
|
+
} finally {
|
|
244
|
+
this.saving = false
|
|
245
|
+
this.publish()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private publish(): void {
|
|
250
|
+
for (const listener of [...this.listeners]) listener()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Project a stored channel into a draft (secrets never travel in channels). */
|
|
255
|
+
function toDraft(channel: ChannelConfig): ChannelDraft {
|
|
256
|
+
return { id: channel.id, preset: channel.preset, name: channel.name, apiUrl: channel.apiUrl, models: channel.models.map(model => ({ ...model })) }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The scope's current channels value (a plain array), for change detection. */
|
|
260
|
+
function scopeChannelsOf(scope: ImageGenScope): unknown {
|
|
261
|
+
const view = scope.getSnapshot().value as { channels?: unknown } | undefined
|
|
262
|
+
return Array.isArray(view?.channels) ? view.channels : []
|
|
263
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Inline renderer for image-generation tool-result attachments. */
|
|
2
2
|
|
|
3
3
|
import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
|
|
4
|
-
import type { ClientContext,
|
|
4
|
+
import type { ClientContext, SessionId, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|
5
5
|
import { useEffect, useMemo, useState } from 'react'
|
|
6
|
+
import { AGENT_IMAGE_API } from '../protocol.ts'
|
|
6
7
|
import css from './image-toolview.module.css'
|
|
7
8
|
|
|
8
9
|
/** Owner props supplied by the host's keyed tool-call slot. */
|
|
@@ -31,7 +32,9 @@ function isSettled(block: ToolCallBlock): block is Extract<ToolCallBlock, { kind
|
|
|
31
32
|
|
|
32
33
|
function imageRefsOf(block: ToolCallBlock): ImageAttachmentRef[] {
|
|
33
34
|
if (!isSettled(block)) return []
|
|
34
|
-
|
|
35
|
+
const resultContent = block.resultView?.card === 'generic' ? block.resultView.content ?? [] : []
|
|
36
|
+
return [...block.content, ...resultContent]
|
|
37
|
+
.flatMap(content => content.type === 'image' ? [content.attachment] : [])
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
function textOf(block: ToolCallBlock): string {
|
|
@@ -108,16 +111,20 @@ function useAttachmentImages(
|
|
|
108
111
|
|
|
109
112
|
/** Register the inline image result view for all image-generation result tools. */
|
|
110
113
|
export function registerImageToolviews(ctx: ClientContext): void {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
const load = async (_sessionId: SessionId, ref: ImageAttachmentRef): Promise<string> => {
|
|
115
|
+
// Tool-result images intentionally do not occur in model-visible session
|
|
116
|
+
// content, so session.readAttachment() rejects them. The plugin route
|
|
117
|
+
// reads the same durable attachment after validating the complete ref.
|
|
118
|
+
const query = new URLSearchParams({
|
|
119
|
+
attachment_id: String(ref.attachmentId),
|
|
120
|
+
media_type: ref.mediaType,
|
|
121
|
+
bytes: String(ref.bytes),
|
|
122
|
+
width: String(ref.width),
|
|
123
|
+
height: String(ref.height),
|
|
124
|
+
})
|
|
125
|
+
const response = await fetch(`${AGENT_IMAGE_API}?${query.toString()}`)
|
|
126
|
+
if (!response.ok) throw new Error(`无法读取图片附件(HTTP ${response.status})。`)
|
|
127
|
+
const blob = await response.blob()
|
|
121
128
|
return URL.createObjectURL(blob)
|
|
122
129
|
}
|
|
123
130
|
|
package/src/client/locales.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export const zh = {
|
|
6
6
|
'entry.label': 'AI 生图',
|
|
7
|
-
'entry.tooltip': 'AI 生图面板(gpt-image-2 / grok-imagine-image
|
|
7
|
+
'entry.tooltip': 'AI 生图面板(gpt-image-2 / grok-imagine-image / nanobanana / seedream 系列)',
|
|
8
8
|
'panel.title': 'AI 生图',
|
|
9
9
|
'panel.githubTip': '觉得好用或有建议?欢迎来 GitHub 提 issues、点个 star 支持一下!',
|
|
10
10
|
// mode
|
|
@@ -72,6 +72,9 @@ export const zh = {
|
|
|
72
72
|
'canvas.emptyTitle': '开始你的创作',
|
|
73
73
|
'canvas.emptyHint': '在左侧输入提示词并点击「开始生成」,结果将显示在这里',
|
|
74
74
|
'canvas.error': '生成失败:{error}',
|
|
75
|
+
'canvas.submitting': '正在加入生成队列…',
|
|
76
|
+
'canvas.queued': '已加入生成队列',
|
|
77
|
+
'canvas.queueHint': '当前有 {count} 个任务等待处理',
|
|
75
78
|
'canvas.generating': '正在生成图片…',
|
|
76
79
|
'canvas.elapsed': '已用时 {seconds}s',
|
|
77
80
|
'canvas.images': '本次生成 {count} 张',
|
|
@@ -249,11 +252,63 @@ export const zh = {
|
|
|
249
252
|
'templates.attribution': '模板与图片来自 awesome-gpt-image-2 项目,作者链接见各模板详情',
|
|
250
253
|
'templates.source': '来源:vibeui.top',
|
|
251
254
|
'templates.featured': '精选',
|
|
255
|
+
// channel management
|
|
256
|
+
'channels.title': '渠道',
|
|
257
|
+
'channels.hint': '填写各渠道的 API 地址与密钥即可使用对应模型',
|
|
258
|
+
'channels.empty': '尚无渠道,点击下方按钮添加',
|
|
259
|
+
'channels.addProvider': '添加提供方',
|
|
260
|
+
'channels.addCustom': '添加自定义渠道',
|
|
261
|
+
'channels.untitled': '未命名渠道',
|
|
262
|
+
'channels.keySet': '密钥已设置',
|
|
263
|
+
'channels.keyMissing': '未设置密钥',
|
|
264
|
+
'channels.modelCount': '{n} 个模型',
|
|
265
|
+
'channels.noModels': '未配置模型',
|
|
266
|
+
'channels.defaultLabel': '默认',
|
|
267
|
+
'channels.statusReady': '可用',
|
|
268
|
+
'channels.statusIncomplete': '配置未完成',
|
|
269
|
+
'channels.edit': '编辑',
|
|
270
|
+
'channels.delete': '删除',
|
|
271
|
+
'channels.deleteConfirmTitle': '删除渠道「{name}」?该渠道的密钥将被移除;历史与画廊中的记录仍会保留。',
|
|
272
|
+
'channels.confirm': '确认删除',
|
|
273
|
+
'channels.cancel': '取消',
|
|
274
|
+
'channels.editorTitle': '渠道',
|
|
275
|
+
'channels.editorSaveNote': '改动将随卡片底部「保存」一起生效',
|
|
276
|
+
'channels.displayName': '显示名称',
|
|
277
|
+
'channels.apiUrl': 'API 地址',
|
|
278
|
+
'channels.apiKey': 'API 密钥',
|
|
279
|
+
'channels.keyReplaceHint': '已配置 — 输入新值可替换',
|
|
280
|
+
'channels.keyMissingHint': '填写密钥',
|
|
281
|
+
'channels.keyClear': '清除密钥',
|
|
282
|
+
'channels.modelCatalogTitle': '模型目录',
|
|
283
|
+
'channels.noModelsHint': '暂无模型:点击「检测」拉取候选,或手动添加;别名(显示名)默认等于上游模型 id,可自行修改。',
|
|
284
|
+
'channels.detect': '重新检测',
|
|
285
|
+
'channels.detecting': '检测中…',
|
|
286
|
+
'channels.detectSuccess': '连通 ✓ · {n} 个候选',
|
|
287
|
+
'channels.detectFailed': '检测失败:{error}',
|
|
288
|
+
'channels.detectOk': '已连接该 API',
|
|
289
|
+
'channels.modelAliasLabel': '显示名(别名)',
|
|
290
|
+
'channels.modelIdLabel': '上游模型 id',
|
|
291
|
+
'channels.generated': '已生成 {n} 次',
|
|
292
|
+
'channels.unknownProtocol': '未知协议,按通用 OpenAI 协议尝试',
|
|
293
|
+
'channels.removeModel': '移除模型',
|
|
294
|
+
'channels.manualAddPlaceholder': '输入模型 id,例如 qwen-image',
|
|
295
|
+
'channels.addModelConfirm': '添加',
|
|
296
|
+
'channels.copyFrom': '从其他渠道复制…',
|
|
297
|
+
'channels.copyApply': '复制',
|
|
298
|
+
'channels.candidatesTitle': '候选模型(点击加入)',
|
|
299
|
+
'channels.candidatesQuick': '只勾已收录协议',
|
|
300
|
+
'channels.deleteThisChannel': '删除此渠道',
|
|
301
|
+
'channels.setDefault': '设为默认渠道',
|
|
302
|
+
'channels.presetPickerTitle': '添加提供方',
|
|
303
|
+
'channels.presetPickerHint': '选择一个内置提供方,仅需填写 API 密钥',
|
|
304
|
+
'channels.presetModelsHint': '{n} 个预置模型',
|
|
305
|
+
'channels.presetCustomHint': '自行填写 API 地址、密钥与模型目录',
|
|
306
|
+
'channels.presetLoadFailed': '加载提供方失败:{error}',
|
|
252
307
|
} as const
|
|
253
308
|
|
|
254
309
|
export const en: Record<keyof typeof zh, string> = {
|
|
255
310
|
'entry.label': 'AI Image',
|
|
256
|
-
'entry.tooltip': 'AI image generation studio (gpt-image-2 / grok-imagine-image)',
|
|
311
|
+
'entry.tooltip': 'AI image generation studio (gpt-image-2 / grok-imagine-image / nanobanana / seedream family)',
|
|
257
312
|
'panel.title': 'AI Image',
|
|
258
313
|
'panel.githubTip': 'Like it or have suggestions? Head to GitHub to open issues and star us!',
|
|
259
314
|
'mode.text': 'Text to Image',
|
|
@@ -315,6 +370,9 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
315
370
|
'canvas.emptyTitle': 'Start creating',
|
|
316
371
|
'canvas.emptyHint': 'Enter a prompt on the left and click "Generate"; results appear here',
|
|
317
372
|
'canvas.error': 'Generation failed: {error}',
|
|
373
|
+
'canvas.submitting': 'Adding to the generation queue…',
|
|
374
|
+
'canvas.queued': 'Added to the generation queue',
|
|
375
|
+
'canvas.queueHint': '{count} task(s) waiting or generating',
|
|
318
376
|
'canvas.generating': 'Generating images…',
|
|
319
377
|
'canvas.elapsed': 'Elapsed {seconds}s',
|
|
320
378
|
'canvas.images': '{count} image(s) generated',
|
|
@@ -486,6 +544,58 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
486
544
|
'templates.attribution': 'Templates and images come from the awesome-gpt-image-2 project; author links are on each template',
|
|
487
545
|
'templates.source': 'Source: vibeui.top',
|
|
488
546
|
'templates.featured': 'Featured',
|
|
547
|
+
// channel management
|
|
548
|
+
'channels.title': 'Channels',
|
|
549
|
+
'channels.hint': 'Fill in each channel\'s API URL and key to use its models',
|
|
550
|
+
'channels.empty': 'No channels yet — add one below',
|
|
551
|
+
'channels.addProvider': 'Add provider',
|
|
552
|
+
'channels.addCustom': 'Add custom channel',
|
|
553
|
+
'channels.untitled': 'Untitled channel',
|
|
554
|
+
'channels.keySet': 'Key set',
|
|
555
|
+
'channels.keyMissing': 'Key missing',
|
|
556
|
+
'channels.modelCount': '{n} model(s)',
|
|
557
|
+
'channels.noModels': 'No models',
|
|
558
|
+
'channels.defaultLabel': 'Default',
|
|
559
|
+
'channels.statusReady': 'Ready',
|
|
560
|
+
'channels.statusIncomplete': 'Config incomplete',
|
|
561
|
+
'channels.edit': 'Edit',
|
|
562
|
+
'channels.delete': 'Delete',
|
|
563
|
+
'channels.deleteConfirmTitle': 'Delete channel "{name}"? Its key will be removed; history and gallery records are kept.',
|
|
564
|
+
'channels.confirm': 'Delete',
|
|
565
|
+
'channels.cancel': 'Cancel',
|
|
566
|
+
'channels.editorTitle': 'Channel',
|
|
567
|
+
'channels.editorSaveNote': 'Changes take effect with the card\'s "Save" button',
|
|
568
|
+
'channels.displayName': 'Display name',
|
|
569
|
+
'channels.apiUrl': 'API URL',
|
|
570
|
+
'channels.apiKey': 'API key',
|
|
571
|
+
'channels.keyReplaceHint': 'Configured — type a new value to replace it',
|
|
572
|
+
'channels.keyMissingHint': 'Enter a key',
|
|
573
|
+
'channels.keyClear': 'Clear key',
|
|
574
|
+
'channels.modelCatalogTitle': 'Model catalog',
|
|
575
|
+
'channels.noModelsHint': 'No models yet: run "Detect" to pull candidates, or add one manually; the alias (display name) defaults to the upstream model id and can be renamed.',
|
|
576
|
+
'channels.detect': 'Re-detect',
|
|
577
|
+
'channels.detecting': 'Detecting…',
|
|
578
|
+
'channels.detectSuccess': 'Connected ✓ · {n} candidates',
|
|
579
|
+
'channels.detectFailed': 'Detection failed: {error}',
|
|
580
|
+
'channels.detectOk': 'Connected to this API',
|
|
581
|
+
'channels.modelAliasLabel': 'Display name (alias)',
|
|
582
|
+
'channels.modelIdLabel': 'Upstream model id',
|
|
583
|
+
'channels.generated': 'Generated {n} time(s)',
|
|
584
|
+
'channels.unknownProtocol': 'Unknown protocol — best-effort OpenAI',
|
|
585
|
+
'channels.removeModel': 'Remove model',
|
|
586
|
+
'channels.manualAddPlaceholder': 'Enter a model id, e.g. qwen-image',
|
|
587
|
+
'channels.addModelConfirm': 'Add',
|
|
588
|
+
'channels.copyFrom': 'Copy from another channel…',
|
|
589
|
+
'channels.copyApply': 'Copy',
|
|
590
|
+
'channels.candidatesTitle': 'Candidates (click to add)',
|
|
591
|
+
'channels.candidatesQuick': 'Add only known protocols',
|
|
592
|
+
'channels.deleteThisChannel': 'Delete this channel',
|
|
593
|
+
'channels.setDefault': 'Set as default channel',
|
|
594
|
+
'channels.presetPickerTitle': 'Add provider',
|
|
595
|
+
'channels.presetPickerHint': 'Pick a built-in provider — only the API key is needed',
|
|
596
|
+
'channels.presetModelsHint': '{n} preset model(s)',
|
|
597
|
+
'channels.presetCustomHint': 'Configure the API URL, key, and model catalog yourself',
|
|
598
|
+
'channels.presetLoadFailed': 'Failed to load providers: {error}',
|
|
489
599
|
}
|
|
490
600
|
|
|
491
601
|
/** Locale key union. */
|