@dickpy/dsh-imagegen 1.2.3 → 1.4.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.
Files changed (45) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +203 -181
  3. package/cordis.patch.yml +8 -8
  4. package/docs/images/multi-model-comparison.png +0 -0
  5. package/lib/client.js +2711 -1318
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +830 -155
  8. package/package.json +70 -68
  9. package/src/agent-image-tools.ts +418 -316
  10. package/src/client/ImageGenPanel.tsx +1703 -1476
  11. package/src/client/SettingsCard.tsx +936 -648
  12. package/src/client/TemplateLibrary.tsx +336 -336
  13. package/src/client/api.ts +193 -193
  14. package/src/client/channels-form.ts +263 -0
  15. package/src/client/controller.ts +46 -46
  16. package/src/client/conversation-sync.ts +14 -0
  17. package/src/client/css-modules.d.ts +5 -5
  18. package/src/client/helpers.ts +33 -33
  19. package/src/client/image-toolview.module.css +73 -73
  20. package/src/client/image-toolview.tsx +170 -152
  21. package/src/client/index.ts +32 -22
  22. package/src/client/locales.ts +610 -484
  23. package/src/client/mount.tsx +185 -96
  24. package/src/client/panel.module.css +1713 -1445
  25. package/src/client/settings-card.module.css +1023 -536
  26. package/src/client/settings-form.ts +336 -336
  27. package/src/client/settings-scope.ts +298 -250
  28. package/src/client/sidebar-entry.ts +148 -102
  29. package/src/client/templates.module.css +453 -453
  30. package/src/engine.ts +520 -464
  31. package/src/gallery-store.ts +286 -280
  32. package/src/generation-runtime.ts +79 -48
  33. package/src/history-store.ts +250 -238
  34. package/src/image-format.ts +11 -0
  35. package/src/image-models.ts +19 -19
  36. package/src/index.ts +318 -212
  37. package/src/model-catalog.ts +115 -0
  38. package/src/presets.ts +71 -0
  39. package/src/prompt-enhancer.ts +137 -79
  40. package/src/protocol.ts +338 -253
  41. package/src/routes.ts +916 -738
  42. package/src/task-queue.ts +113 -103
  43. package/src/templates/cases.json +10196 -10196
  44. package/src/templates-store.ts +278 -278
  45. package/src/updater.ts +117 -117
@@ -1,250 +1,298 @@
1
- /**
2
- * Browser-side settings scope for the dsh-imagegen namespace, served by the
3
- * plugin's own loopback bridge routes (/api/dsh-imagegen/settings). The
4
- * official rc.6 settings scope answers "unavailable" for every third-party
5
- * namespace (the host-apiproxy allowlist is hard-coded), so this package
6
- * re-serves its namespace through the host settings seam over a same-origin,
7
- * loopback-only HTTP pair — the same pattern the dsh-web-ui family bridge
8
- * uses, self-contained per plugin.
9
- */
10
-
11
- import {
12
- createSnapshotStore,
13
- type SettingsScope,
14
- type SettingsScopeSnapshot,
15
- type SnapshotStore,
16
- } from '@deepseek-ai/dsh-client-runtime/client'
17
- import { SETTINGS_API } from '../protocol.ts'
18
-
19
- /** The fields this plugin's settings card edits. */
20
- export interface ImageGenConfig {
21
- enabled?: boolean
22
- announceToAgent?: boolean
23
- allowAgentImageGeneration?: boolean
24
- apiUrl?: string
25
- apiKey?: string
26
- imageModels?: string[]
27
- promptApiUrl?: string
28
- promptApiKey?: string
29
- promptModel?: string
30
- }
31
-
32
- /** Wire shape of one namespace view from the bridge. */
33
- interface BridgeView {
34
- ns: string
35
- value: unknown
36
- base?: unknown
37
- user?: unknown
38
- revision: number
39
- secrets?: Array<{ path: string[]; set: boolean }>
40
- }
41
-
42
- /** The bridge response envelope ({ ok: true, value } | { ok: false, code, message }). */
43
- type BridgeEnvelope =
44
- | { ok: true; value: { namespaces?: BridgeView[]; writable?: boolean } | BridgeView }
45
- | { ok: false; code: string; message: string }
46
-
47
- /** Settings wire face over the bridge routes (fetch-backed). */
48
- function createBridgeApi(fetchFn: typeof fetch): {
49
- settings: {
50
- describe(payload: Record<string, never>): Promise<{ result: BridgeEnvelope }>
51
- mutate(payload: { ns: string; ops: unknown[]; expectedRevision?: number }): Promise<{ result: BridgeEnvelope }>
52
- }
53
- } {
54
- const post = async (path: string, body: unknown): Promise<{ result: BridgeEnvelope }> => {
55
- try {
56
- const response = await fetchFn(path, {
57
- method: 'POST',
58
- headers: { 'content-type': 'application/json' },
59
- body: JSON.stringify(body),
60
- })
61
- if (!response.ok) {
62
- return { result: { ok: false, code: 'internal', message: `bridge HTTP ${response.status}` } }
63
- }
64
- return { result: await response.json() as BridgeEnvelope }
65
- } catch {
66
- return { result: { ok: false, code: 'internal', message: 'settings bridge unreachable' } }
67
- }
68
- }
69
- return {
70
- settings: {
71
- describe: async payload => post(SETTINGS_API.describe, payload),
72
- mutate: async payload => post(SETTINGS_API.mutate, payload),
73
- },
74
- }
75
- }
76
-
77
- /**
78
- * A SettingsScope over the bridge face: serialized queue, revision-fenced
79
- * writes, recovery read after a refusal. Mirrors the official controller's
80
- * ordering but trusts the Host-seam value without re-running the wire-schema
81
- * validation — the seam already validated it.
82
- */
83
- class BridgeScopeController<T> implements SettingsScope<T> {
84
- private readonly store: SnapshotStore<SettingsScopeSnapshot<T>>
85
- /** Whether the namespace currently holds a stored secret (e.g. apiKey). */
86
- private readonly keySet: SnapshotStore<boolean>
87
- /** Individual secret presence bits, keyed by the settings field name. */
88
- private readonly secretSets: SnapshotStore<Record<string, boolean>>
89
- private tail: Promise<void> = Promise.resolve()
90
- private disposed = false
91
-
92
- constructor(
93
- private readonly api: ReturnType<typeof createBridgeApi>['settings'],
94
- private readonly spec: { namespace: string },
95
- ) {
96
- this.store = createSnapshotStore<SettingsScopeSnapshot<T>>({
97
- status: 'loading',
98
- value: undefined,
99
- base: undefined,
100
- user: undefined,
101
- revision: undefined,
102
- writable: false,
103
- mode: 'host',
104
- })
105
- this.keySet = createSnapshotStore(false)
106
- this.secretSets = createSnapshotStore({})
107
- }
108
-
109
- getSnapshot(): SettingsScopeSnapshot<T> {
110
- return this.store.getSnapshot()
111
- }
112
-
113
- /** Whether a stored secret exists (from the redacted view's secrets list). */
114
- getKeySetSnapshot(): boolean {
115
- return this.keySet.getSnapshot()
116
- }
117
-
118
- /** Observe the secret-set flag. */
119
- subscribeKeySet(listener: () => void): () => void {
120
- return this.keySet.subscribe(listener)
121
- }
122
-
123
- /** Whether a specific secret field currently has a stored value. */
124
- getSecretSetSnapshot(field: string): boolean {
125
- return this.secretSets.getSnapshot()[field] === true
126
- }
127
-
128
- /** Observe changes to individual secret-field presence bits. */
129
- subscribeSecretSets(listener: () => void): () => void {
130
- return this.secretSets.subscribe(listener)
131
- }
132
-
133
- subscribe(listener: () => void): () => void {
134
- return this.store.subscribe(listener)
135
- }
136
-
137
- /** Queue a bridge refresh. */
138
- load(): Promise<void> {
139
- return this.enqueue(() => this.read())
140
- }
141
-
142
- set(field: string, value: unknown): Promise<void> {
143
- return this.enqueue(() => this.write({ op: 'set', path: [field], value }))
144
- }
145
-
146
- unset(field: string): Promise<void> {
147
- return this.enqueue(() => this.write({ op: 'unset', path: [field] }))
148
- }
149
-
150
- async dispose(): Promise<void> {
151
- this.disposed = true
152
- await this.tail
153
- }
154
-
155
- private enqueue(operation: () => Promise<void>): Promise<void> {
156
- if (this.disposed) return Promise.resolve()
157
- const task = this.tail.then(async () => {
158
- if (this.disposed) return
159
- await operation()
160
- })
161
- this.tail = task.catch(() => {})
162
- return task
163
- }
164
-
165
- private async read(): Promise<void> {
166
- let response
167
- try {
168
- response = await this.api.describe({})
169
- } catch {
170
- if (!this.disposed) this.store.update(draft => { draft.status = 'unavailable' })
171
- return
172
- }
173
- if (!response.result.ok || this.disposed) {
174
- if (!this.disposed) this.store.update(draft => { draft.status = 'unavailable' })
175
- return
176
- }
177
- const { namespaces, writable } = response.result.value as { namespaces?: BridgeView[]; writable?: boolean }
178
- const view = namespaces?.find(candidate => candidate.ns === this.spec.namespace)
179
- if (view === undefined) {
180
- this.store.update(draft => {
181
- draft.status = 'unavailable'
182
- draft.writable = writable === true
183
- })
184
- this.keySet.set(false)
185
- this.secretSets.set({})
186
- return
187
- }
188
- this.accept(view, writable)
189
- }
190
-
191
- private async write(op: { op: 'set' | 'unset'; path: string[]; value?: unknown }): Promise<void> {
192
- const revision = this.getSnapshot().revision
193
- let response
194
- try {
195
- response = await this.api.mutate({
196
- ns: this.spec.namespace,
197
- ops: [op],
198
- ...revision === undefined ? {} : { expectedRevision: revision },
199
- })
200
- } catch {
201
- await this.read()
202
- return
203
- }
204
- if (!response.result.ok || this.disposed) {
205
- await this.read()
206
- return
207
- }
208
- this.accept(response.result.value as BridgeView, undefined)
209
- }
210
-
211
- private accept(view: BridgeView, writable: boolean | undefined): void {
212
- this.store.update(draft => {
213
- draft.revision = view.revision
214
- draft.base = view.base
215
- draft.user = view.user
216
- if (writable !== undefined) draft.writable = writable
217
- draft.status = 'ready'
218
- // Trust the Host-seam value: the seam already validated it, and the
219
- // card binds without a narrowing decoder.
220
- draft.value = view.value as T
221
- })
222
- const secretSets = Object.fromEntries((view.secrets ?? []).map(secret => [secret.path.join('.'), secret.set]))
223
- this.keySet.set(Object.values(secretSets).some(Boolean))
224
- this.secretSets.set(secretSets)
225
- }
226
- }
227
-
228
- /** The bound scope plus the secret-set flag, as the card and panel consume it. */
229
- export interface ImageGenScope extends SettingsScope<ImageGenConfig> {
230
- /** Queue a bridge refresh (the invalidation path re-reads the namespace). */
231
- load(): Promise<void>
232
- getKeySetSnapshot(): boolean
233
- subscribeKeySet(listener: () => void): () => void
234
- getSecretSetSnapshot(field: string): boolean
235
- subscribeSecretSets(listener: () => void): () => void
236
- }
237
-
238
- /**
239
- * Bind the dsh-imagegen settings scope over the bridge routes and start its
240
- * initial read (the caller mounts nothing until the scope settles).
241
- * @param fetchFn - the fetch implementation (the global fetch on loopback).
242
- * @returns the scope; unavailable when the bridge is unreachable.
243
- */
244
- export function bindImageGenScope(fetchFn: typeof fetch = fetch): ImageGenScope {
245
- const controller = new BridgeScopeController<ImageGenConfig>(createBridgeApi(fetchFn).settings, {
246
- namespace: 'dsh-imagegen',
247
- })
248
- void controller.load()
249
- return controller
250
- }
1
+ /**
2
+ * Browser-side settings scope for the dsh-imagegen namespace, served by the
3
+ * plugin's own loopback bridge routes (/api/dsh-imagegen/settings). The
4
+ * official rc.6 settings scope answers "unavailable" for every third-party
5
+ * namespace (the host-apiproxy allowlist is hard-coded), so this package
6
+ * re-serves its namespace through the host settings seam over a same-origin,
7
+ * loopback-only HTTP pair — the same pattern the dsh-web-ui family bridge
8
+ * uses, self-contained per plugin.
9
+ */
10
+
11
+ import {
12
+ createSnapshotStore,
13
+ type SettingsScope,
14
+ type SettingsScopeSnapshot,
15
+ type SnapshotStore,
16
+ } from '@deepseek-ai/dsh-client-runtime/client'
17
+ import { SETTINGS_API, type ChannelConfig } from '../protocol.ts'
18
+
19
+ /** The fields this plugin's settings card edits. */
20
+ export interface ImageGenConfig {
21
+ enabled?: boolean
22
+ announceToAgent?: boolean
23
+ allowAgentImageGeneration?: boolean
24
+ /** Configured channels (each: name, endpoint, model catalog). */
25
+ channels?: ChannelConfig[]
26
+ /** Per-channel API keys, keyed by channel id. The redacted wire view returns
27
+ * this as an empty object; key presence comes from the secrets sidecar. */
28
+ channelSecrets?: Record<string, string>
29
+ /** Channel used when a request does not name one. */
30
+ defaultChannelId?: string
31
+ promptApiUrl?: string
32
+ promptApiKey?: string
33
+ promptModel?: string
34
+ /* ----- deprecated legacy single-endpoint fields (migrated to channels) ----- */
35
+ apiUrl?: string
36
+ apiKey?: string
37
+ imageModels?: string[]
38
+ }
39
+
40
+ /** One settings path-op as the bridge consumes it. */
41
+ export type SettingsOp = { op: 'set'; path: string[]; value: unknown } | { op: 'unset'; path: string[] }
42
+
43
+ /** Wire shape of one namespace view from the bridge. */
44
+ interface BridgeView {
45
+ ns: string
46
+ value: unknown
47
+ base?: unknown
48
+ user?: unknown
49
+ revision: number
50
+ secrets?: Array<{ path: string[]; set: boolean }>
51
+ }
52
+
53
+ /** The bridge response envelope ({ ok: true, value } | { ok: false, code, message }). */
54
+ type BridgeEnvelope =
55
+ | { ok: true; value: { namespaces?: BridgeView[]; writable?: boolean } | BridgeView }
56
+ | { ok: false; code: string; message: string }
57
+
58
+ /** Settings wire face over the bridge routes (fetch-backed). */
59
+ function createBridgeApi(fetchFn: typeof fetch): {
60
+ settings: {
61
+ describe(payload: Record<string, never>): Promise<{ result: BridgeEnvelope }>
62
+ mutate(payload: { ns: string; ops: unknown[]; expectedRevision?: number }): Promise<{ result: BridgeEnvelope }>
63
+ }
64
+ } {
65
+ const post = async (path: string, body: unknown): Promise<{ result: BridgeEnvelope }> => {
66
+ try {
67
+ const response = await fetchFn(path, {
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/json' },
70
+ body: JSON.stringify(body),
71
+ })
72
+ if (!response.ok) {
73
+ return { result: { ok: false, code: 'internal', message: `bridge HTTP ${response.status}` } }
74
+ }
75
+ return { result: await response.json() as BridgeEnvelope }
76
+ } catch {
77
+ return { result: { ok: false, code: 'internal', message: 'settings bridge unreachable' } }
78
+ }
79
+ }
80
+ return {
81
+ settings: {
82
+ describe: async payload => post(SETTINGS_API.describe, payload),
83
+ mutate: async payload => post(SETTINGS_API.mutate, payload),
84
+ },
85
+ }
86
+ }
87
+
88
+ /**
89
+ * A SettingsScope over the bridge face: serialized queue, revision-fenced
90
+ * writes, recovery read after a refusal. Mirrors the official controller's
91
+ * ordering but trusts the Host-seam value without re-running the wire-schema
92
+ * validation — the seam already validated it.
93
+ */
94
+ class BridgeScopeController<T> implements SettingsScope<T> {
95
+ private readonly store: SnapshotStore<SettingsScopeSnapshot<T>>
96
+ /** Whether the namespace currently holds a stored secret (e.g. apiKey). */
97
+ private readonly keySet: SnapshotStore<boolean>
98
+ /** Individual secret presence bits, keyed by the settings field name. */
99
+ private readonly secretSets: SnapshotStore<Record<string, boolean>>
100
+ private tail: Promise<void> = Promise.resolve()
101
+ private disposed = false
102
+
103
+ constructor(
104
+ private readonly api: ReturnType<typeof createBridgeApi>['settings'],
105
+ private readonly spec: { namespace: string },
106
+ ) {
107
+ this.store = createSnapshotStore<SettingsScopeSnapshot<T>>({
108
+ status: 'loading',
109
+ value: undefined,
110
+ base: undefined,
111
+ user: undefined,
112
+ revision: undefined,
113
+ writable: false,
114
+ mode: 'host',
115
+ })
116
+ this.keySet = createSnapshotStore(false)
117
+ this.secretSets = createSnapshotStore({})
118
+ }
119
+
120
+ getSnapshot(): SettingsScopeSnapshot<T> {
121
+ return this.store.getSnapshot()
122
+ }
123
+
124
+ /** Whether a stored secret exists (from the redacted view's secrets list). */
125
+ getKeySetSnapshot(): boolean {
126
+ return this.keySet.getSnapshot()
127
+ }
128
+
129
+ /** Observe the secret-set flag. */
130
+ subscribeKeySet(listener: () => void): () => void {
131
+ return this.keySet.subscribe(listener)
132
+ }
133
+
134
+ /** Whether a specific secret field currently has a stored value. */
135
+ getSecretSetSnapshot(field: string): boolean {
136
+ return this.secretSets.getSnapshot()[field] === true
137
+ }
138
+
139
+ /** Observe changes to individual secret-field presence bits. */
140
+ subscribeSecretSets(listener: () => void): () => void {
141
+ return this.secretSets.subscribe(listener)
142
+ }
143
+
144
+ subscribe(listener: () => void): () => void {
145
+ return this.store.subscribe(listener)
146
+ }
147
+
148
+ /** Queue a bridge refresh. */
149
+ load(): Promise<void> {
150
+ return this.enqueue(() => this.read())
151
+ }
152
+
153
+ set(field: string, value: unknown): Promise<void> {
154
+ return this.enqueue(() => this.writeOps([{ op: 'set', path: [field], value }]))
155
+ }
156
+
157
+ unset(field: string): Promise<void> {
158
+ return this.enqueue(() => this.writeOps([{ op: 'unset', path: [field] }]))
159
+ }
160
+
161
+ /** Apply several path ops in one revision-fenced mutate call (atomic save).
162
+ * Path ops may address plain-object fields (e.g. `channelSecrets.<id>`),
163
+ * but never navigate *inside* arrays — write array fields wholesale. */
164
+ mutateOps(ops: SettingsOp[]): Promise<void> {
165
+ return this.enqueue(() => this.writeOps(ops))
166
+ }
167
+
168
+ async dispose(): Promise<void> {
169
+ this.disposed = true
170
+ await this.tail
171
+ }
172
+
173
+ private enqueue(operation: () => Promise<void>): Promise<void> {
174
+ if (this.disposed) return Promise.resolve()
175
+ const task = this.tail.then(async () => {
176
+ if (this.disposed) return
177
+ await operation()
178
+ })
179
+ this.tail = task.catch(() => {})
180
+ return task
181
+ }
182
+
183
+ private async read(): Promise<void> {
184
+ let response
185
+ try {
186
+ response = await this.api.describe({})
187
+ } catch {
188
+ if (!this.disposed) this.store.update(draft => { draft.status = 'unavailable' })
189
+ return
190
+ }
191
+ if (!response.result.ok || this.disposed) {
192
+ if (!this.disposed) this.store.update(draft => { draft.status = 'unavailable' })
193
+ return
194
+ }
195
+ const { namespaces, writable } = response.result.value as { namespaces?: BridgeView[]; writable?: boolean }
196
+ const view = namespaces?.find(candidate => candidate.ns === this.spec.namespace)
197
+ if (view === undefined) {
198
+ this.store.update(draft => {
199
+ draft.status = 'unavailable'
200
+ draft.writable = writable === true
201
+ })
202
+ this.keySet.set(false)
203
+ this.secretSets.set({})
204
+ return
205
+ }
206
+ this.accept(view, writable)
207
+ }
208
+
209
+ private async writeOps(ops: SettingsOp[]): Promise<void> {
210
+ const revision = this.getSnapshot().revision
211
+ let response
212
+ try {
213
+ response = await this.api.mutate({
214
+ ns: this.spec.namespace,
215
+ ops,
216
+ ...revision === undefined ? {} : { expectedRevision: revision },
217
+ })
218
+ } catch {
219
+ await this.read()
220
+ return
221
+ }
222
+ if (!response.result.ok || this.disposed) {
223
+ await this.read()
224
+ return
225
+ }
226
+ this.accept(response.result.value as BridgeView, undefined)
227
+ }
228
+
229
+ private accept(view: BridgeView, writable: boolean | undefined): void {
230
+ this.store.update(draft => {
231
+ draft.revision = view.revision
232
+ draft.base = view.base
233
+ draft.user = view.user
234
+ if (writable !== undefined) draft.writable = writable
235
+ draft.status = 'ready'
236
+ // Trust the Host-seam value: the seam already validated it, and the
237
+ // card binds without a narrowing decoder.
238
+ draft.value = view.value as T
239
+ })
240
+ const secretSets = Object.fromEntries((view.secrets ?? []).map(secret => [secret.path.join('.'), secret.set]))
241
+ this.keySet.set(Object.values(secretSets).some(Boolean))
242
+ this.secretSets.set(secretSets)
243
+ }
244
+ }
245
+
246
+ /** The bound scope plus the secret-set flag, as the card and panel consume it. */
247
+ export interface ImageGenScope extends SettingsScope<ImageGenConfig> {
248
+ /** Queue a bridge refresh (the invalidation path re-reads the namespace). */
249
+ load(): Promise<void>
250
+ /** Apply several path ops in one revision-fenced mutate call. */
251
+ mutateOps(ops: SettingsOp[]): Promise<void>
252
+ getKeySetSnapshot(): boolean
253
+ subscribeKeySet(listener: () => void): () => void
254
+ getSecretSetSnapshot(field: string): boolean
255
+ subscribeSecretSets(listener: () => void): () => void
256
+ }
257
+
258
+ /**
259
+ * Bind the dsh-imagegen settings scope over the bridge routes and start its
260
+ * initial read (the caller mounts nothing until the scope settles).
261
+ * @param fetchFn - the fetch implementation (the global fetch on loopback).
262
+ * @returns the scope; unavailable when the bridge is unreachable.
263
+ */
264
+ export function bindImageGenScope(fetchFn: typeof fetch = fetch): ImageGenScope {
265
+ const controller = new BridgeScopeController<ImageGenConfig>(createBridgeApi(fetchFn).settings, {
266
+ namespace: 'dsh-imagegen',
267
+ })
268
+ void controller.load()
269
+ return controller
270
+ }
271
+
272
+ /**
273
+ * Flatten the configured channels into the model options the panel lists
274
+ * (aliases; the default channel's models first) plus the default channel id.
275
+ * Falls back to the legacy flat allow-list while no channels exist (upgrade
276
+ * path). Pure projection — no host calls.
277
+ */
278
+ export function imageModelOptions(config: ImageGenConfig | undefined): { models: string[]; defaultChannelId?: string } {
279
+ const channels = config?.channels ?? []
280
+ if (channels.length === 0) {
281
+ const legacy = Array.isArray(config?.imageModels)
282
+ ? config.imageModels.filter((model): model is string => typeof model === 'string' && model.trim() !== '')
283
+ : []
284
+ return { models: legacy }
285
+ }
286
+ const defaultId = config?.defaultChannelId !== undefined && channels.some(channel => channel.id === config.defaultChannelId)
287
+ ? config.defaultChannelId
288
+ : channels[0]!.id
289
+ const ordered = [defaultId, ...channels.filter(channel => channel.id !== defaultId).map(channel => channel.id)]
290
+ const models: string[] = []
291
+ for (const id of ordered) {
292
+ const channel = channels.find(candidate => candidate.id === id)!
293
+ for (const model of channel.models) {
294
+ if (model.alias !== '' && !models.includes(model.alias)) models.push(model.alias)
295
+ }
296
+ }
297
+ return models.length > 0 ? { models, defaultChannelId: defaultId } : { models: [], defaultChannelId: defaultId }
298
+ }