@things-factory/figure-service 10.1.13 → 10.1.16

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.
@@ -93,6 +93,48 @@ async function figureInDomain(figureId: string, domainId: string): Promise<any>
93
93
  return found
94
94
  }
95
95
 
96
+ /**
97
+ * figureId 또는 hostContext로부터 대상 Figure row(또는 미저장 초안 가상 row)를 해소한다.
98
+ */
99
+ async function resolveFigureTarget(figureId: string | undefined, domainId: string, host: any): Promise<any> {
100
+ if (
101
+ host?.editorMode === 'new' &&
102
+ host?.draftSource &&
103
+ (!figureId || figureId === host.draftSource.type || figureId.startsWith('FIGURE'))
104
+ ) {
105
+ return unsavedFigure(host)
106
+ }
107
+
108
+ if (figureId) {
109
+ const { getRepository, Figure } = getDeps()
110
+ let found = await getRepository(Figure).findOne({
111
+ where: { id: figureId, domain: { id: domainId } }
112
+ })
113
+ if (!found) {
114
+ found = await getRepository(Figure).findOne({
115
+ where: { type: figureId, domain: { id: domainId } }
116
+ })
117
+ }
118
+ if (!found) {
119
+ if (
120
+ host?.editorMode === 'new' ||
121
+ (host?.draftSource &&
122
+ (host.draftSource.type === figureId ||
123
+ figureId.startsWith('FIGURE') ||
124
+ !host.figureId ||
125
+ host.figureId === figureId))
126
+ ) {
127
+ const unsaved = unsavedFigure(host)
128
+ if (unsaved) return unsaved
129
+ }
130
+ throw new Error(`figure not found in this tenant: ${figureId}`)
131
+ }
132
+ return found
133
+ }
134
+
135
+ return unsavedFigure(host)
136
+ }
137
+
96
138
  /**
97
139
  * 모델러가 아직 저장하지 않은 source를 도구가 읽는 유일한 길.
98
140
  *
@@ -101,7 +143,8 @@ async function figureInDomain(figureId: string, domainId: string): Promise<any>
101
143
  * 프롬프트로 옮기면 비용만 태우고, DB에 있던 Figure와 다른 것을 고친다고 잘못 말할 수 있다.
102
144
  */
103
145
  function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
104
- const draft = host?.figureId === row.id ? host?.draftSource : undefined
146
+ const isMatch = host?.figureId === row.id || (!row.id && host?.editorMode === 'new') || (!row.id && !host?.figureId)
147
+ const draft = isMatch ? host?.draftSource : undefined
105
148
  if (!draft) {
106
149
  return { source: row.source ? JSON.parse(row.source) : null, draft: false }
107
150
  }
@@ -120,11 +163,20 @@ function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
120
163
  if (checked.errors.length > 0) {
121
164
  throw new Error(`the unsaved draft is invalid: ${checked.errors.map((one: any) => one.message).join(' · ')}`)
122
165
  }
166
+
167
+ let normalizedDraft = draft
123
168
  if (draft.type !== row.type) {
124
- throw new Error(`the unsaved draft type '${draft.type}' does not match Figure '${row.type}'`)
169
+ // 템플릿 기본값 'FIGURE', 임시 draftType('FIGURE_...'), 또는 빈 값인 경우
170
+ // 현재 열려 있는 Figure의 실제 type으로 보정/정규화한다.
171
+ const isGenericOrTempType = !draft.type || draft.type === 'FIGURE' || draft.type.startsWith('FIGURE_')
172
+ if (isGenericOrTempType && row.type) {
173
+ normalizedDraft = { ...draft, type: row.type }
174
+ } else {
175
+ throw new Error(`the unsaved draft type '${draft.type}' does not match Figure '${row.type}'`)
176
+ }
125
177
  }
126
178
 
127
- return { source: draft, draft: true }
179
+ return { source: normalizedDraft, draft: true }
128
180
  }
129
181
 
130
182
  /**
@@ -134,8 +186,14 @@ function sourceForTool(row: any, host: any): { source: any; draft: boolean } {
134
186
  * 도메인에서 Figure를 찾은 뒤, `proposeFigure`가 다시 길이·형식을 정리할 참고 데이터만 건넨다.
135
187
  */
136
188
  function feedbackForTool(host: any, figureId?: string): unknown[] | undefined {
137
- if (!figureId || host?.figureId !== figureId || !Array.isArray(host?.proposalFeedback)) return undefined
138
- return host.proposalFeedback.slice(-5)
189
+ if (!Array.isArray(host?.proposalFeedback)) return undefined
190
+ if (figureId && host?.figureId === figureId) {
191
+ return host.proposalFeedback.slice(-5)
192
+ }
193
+ if (host?.editorMode === 'new' || !host?.figureId) {
194
+ return host.proposalFeedback.slice(-5)
195
+ }
196
+ return undefined
139
197
  }
140
198
 
141
199
  /**
@@ -163,8 +221,8 @@ function missingFigureTarget() {
163
221
  }
164
222
 
165
223
  function unsavedFigure(host: any): any | undefined {
166
- if (host?.editorMode !== 'new' || host?.figureId || !host?.draftSource) return undefined
167
- const row = { id: host.figureId, type: host.draftSource.type, name: '', state: 'draft' }
224
+ if (host?.editorMode !== 'new' || !host?.draftSource) return undefined
225
+ const row = { id: host.figureId, type: host.draftSource.type || 'FIGURE', name: '', state: 'draft' }
168
226
  // Reuse size, shape and vocabulary validation before trusting browser content.
169
227
  const current = sourceForTool(row, host)
170
228
  return { ...row, source: JSON.stringify(current.source) }
@@ -223,7 +281,7 @@ function figureToolSpecs(): ToolSpec[] {
223
281
  builder: async (args: any, ctx: any) => {
224
282
  const domainId = requireDomainId(ctx?.state)
225
283
  const figureId = figureIdFrom(args, ctx?.host)
226
- const row = figureId ? await figureInDomain(figureId, domainId) : unsavedFigure(ctx?.host)
284
+ const row = await resolveFigureTarget(figureId, domainId, ctx?.host)
227
285
  if (!row) return missingFigureTarget()
228
286
 
229
287
  const current = sourceForTool(row, ctx?.host)
@@ -258,7 +316,7 @@ function figureToolSpecs(): ToolSpec[] {
258
316
  builder: async (args: any, ctx: any) => {
259
317
  const domainId = requireDomainId(ctx?.state)
260
318
  const figureId = figureIdFrom(args, ctx?.host)
261
- const row = figureId ? await figureInDomain(figureId, domainId) : unsavedFigure(ctx?.host)
319
+ const row = await resolveFigureTarget(figureId, domainId, ctx?.host)
262
320
  if (!row) return missingFigureTarget()
263
321
 
264
322
  const current = sourceForTool(row, ctx?.host)
@@ -308,12 +366,9 @@ function figureToolSpecs(): ToolSpec[] {
308
366
  let base: unknown
309
367
  if (!args?.create && args?.figureId !== null) {
310
368
  const open = typeof args?.figureId === 'string' ? args.figureId : ctx?.host?.figureId
311
- if (typeof open === 'string' && open.trim()) {
312
- const row = await figureInDomain(open.trim(), domainId)
369
+ const row = await resolveFigureTarget(open ? open.trim() : undefined, domainId, ctx?.host)
370
+ if (row) {
313
371
  base = sourceForTool(row, ctx?.host).source ?? undefined
314
- } else {
315
- const draft = unsavedFigure(ctx?.host)
316
- if (draft) base = JSON.parse(draft.source)
317
372
  }
318
373
  }
319
374
 
@@ -328,13 +383,23 @@ function figureToolSpecs(): ToolSpec[] {
328
383
 
329
384
  const { proposeFigure } = getDeps()
330
385
  const feedback = feedbackForTool(ctx?.host, base ? figureIdFrom(args, ctx?.host) : undefined)
386
+
387
+ let hostImage: { data: Buffer; mediaType: any } | undefined
388
+ const rawImage = ctx?.host?.image || (Array.isArray(ctx?.host?.attachments) ? ctx.host.attachments.find((a: any) => a?.mediaType?.startsWith('image/')) : undefined)
389
+ if (rawImage?.data) {
390
+ const rawData = typeof rawImage.data === 'string' ? rawImage.data.replace(/^data:image\/[a-zA-Z]+;base64,/, '') : rawImage.data
391
+ const buf = typeof rawData === 'string' ? Buffer.from(rawData, 'base64') : Buffer.from(rawData)
392
+ hostImage = { data: buf, mediaType: rawImage.mediaType || 'image/png' }
393
+ }
394
+
331
395
  const result = await proposeFigure({
332
396
  prompt,
333
397
  palette,
334
398
  base,
335
399
  type: typeof args?.type === 'string' ? args.type : undefined,
336
400
  feedback,
337
- refine: !!feedback?.length
401
+ refine: !!feedback?.length,
402
+ image: hostImage
338
403
  })
339
404
 
340
405
  /*
@@ -351,6 +416,13 @@ function figureToolSpecs(): ToolSpec[] {
351
416
  reason:
352
417
  `등급 ${result.score.grade} · 삼각형 ${result.cost.triangles} · ` +
353
418
  `한 도면 ${result.cost.at.drawCalls}회 · ${result.attempts}번에 만들었다`,
419
+ choices: [
420
+ { id: 'save', action: 'save', label: '초안 저장', icon: 'save', primary: true },
421
+ { id: 'repropose', action: 'prompt', label: '다른 콘셉트 재제안', icon: 'refresh', prompt: '현재 제안과 완전히 다른 스타일과 콘셉트로 새로 제안해줘' },
422
+ { id: 'slim', action: 'prompt', label: '비율 슬림 조정', icon: 'aspect_ratio', prompt: '전체적인 가로세로 비율을 좀 더 슬림하고 날렵하게 조정해줘' },
423
+ { id: 'palette', action: 'prompt', label: '색상 테마 변경', icon: 'palette', prompt: '다른 팔레트 색상 테마로 부품 색상을 변경해줘' },
424
+ { id: 'detail', action: 'prompt', label: '디테일 보강', icon: 'extension', prompt: '주요 디테일과 센서 부품을 좀 더 풍부하게 보강해줘' }
425
+ ],
354
426
  args: {
355
427
  baseSource: base ? JSON.stringify(base) : undefined,
356
428
  prompt, figureId: base ? figureIdFrom(args, ctx?.host) : undefined,