@things-factory/figure-service 10.1.9 → 10.1.15
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/dist-server/service/figure/figure-propose.js +2 -1
- package/dist-server/service/figure/figure-propose.js.map +1 -1
- package/dist-server/service/figure/figure-query.d.ts +15 -0
- package/dist-server/service/figure/figure-query.js +67 -0
- package/dist-server/service/figure/figure-query.js.map +1 -1
- package/dist-server/service/figure/figure-source.d.ts +1 -1
- package/dist-server/service/figure/figure-source.js +8 -1
- package/dist-server/service/figure/figure-source.js.map +1 -1
- package/dist-server/service/figure/figure-tools.js +118 -23
- package/dist-server/service/figure/figure-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/logs/.08636eb59927f12972f6774f5947c8507b3564c2-audit.json +15 -0
- package/logs/application-2026-09-14-23.log +0 -0
- package/package.json +4 -3
- package/server/service/figure/figure-ai-flow.test.ts +1 -3
- package/server/service/figure/figure-blueprint-query.test.ts +178 -0
- package/server/service/figure/figure-propose.ts +3 -1
- package/server/service/figure/figure-query.ts +68 -0
- package/server/service/figure/figure-source.test.ts +15 -9
- package/server/service/figure/figure-source.ts +11 -3
- package/server/service/figure/figure-tools.test.ts +93 -4
- package/server/service/figure/figure-tools.ts +126 -24
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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 (!
|
|
138
|
-
|
|
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
|
/**
|
|
@@ -144,20 +202,36 @@ function feedbackForTool(host: any, figureId?: string): unknown[] | undefined {
|
|
|
144
202
|
* 모델이 대상을 안 줬으면 **저작면이 지금 열고 있는 도형**이다. 사용자에게 식별자를 되묻지
|
|
145
203
|
* 않는다 — 화면에 그 도형이 떠 있는데 「어느 도형입니까」를 묻는 것은 문맥을 안 읽은 것이다.
|
|
146
204
|
*/
|
|
147
|
-
function figureIdFrom(args: any, host: any): string {
|
|
205
|
+
function figureIdFrom(args: any, host: any): string | undefined {
|
|
148
206
|
const said = typeof args?.figureId === 'string' ? args.figureId.trim() : ''
|
|
149
207
|
if (said) return said
|
|
150
208
|
|
|
151
209
|
const open = typeof host?.figureId === 'string' ? host.figureId.trim() : ''
|
|
152
210
|
if (open) return open
|
|
153
211
|
|
|
154
|
-
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function missingFigureTarget() {
|
|
216
|
+
return {
|
|
217
|
+
error: 'FIGURE_TARGET_REQUIRED',
|
|
218
|
+
message: '현재 화면에는 선택된 도형이 없습니다.',
|
|
219
|
+
nextStep: 'For an existing figure, call listFigures and match the user’s requested name or type; ask which figure only if ambiguous. For a new figure, call proposeFigureCandidate with create: true and the user’s prompt. Do not invent an id or repeat this call without a target.'
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function unsavedFigure(host: any): any | undefined {
|
|
224
|
+
if (host?.editorMode !== 'new' || !host?.draftSource) return undefined
|
|
225
|
+
const row = { id: host.figureId, type: host.draftSource.type || 'FIGURE', name: '', state: 'draft' }
|
|
226
|
+
// Reuse size, shape and vocabulary validation before trusting browser content.
|
|
227
|
+
const current = sourceForTool(row, host)
|
|
228
|
+
return { ...row, source: JSON.stringify(current.source) }
|
|
155
229
|
}
|
|
156
230
|
|
|
157
231
|
const FIGURE_ID_ARG = {
|
|
158
232
|
figureId: {
|
|
159
233
|
type: 'string',
|
|
160
|
-
description: 'The figure to read. Omit
|
|
234
|
+
description: 'The figure to read. Omit when host context has an open figureId or an unsaved new editor draft. On the catalog, first resolve the user’s requested figure with listFigures.'
|
|
161
235
|
}
|
|
162
236
|
} as const
|
|
163
237
|
|
|
@@ -206,10 +280,11 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
206
280
|
schema: { type: 'object', properties: { ...FIGURE_ID_ARG } },
|
|
207
281
|
builder: async (args: any, ctx: any) => {
|
|
208
282
|
const domainId = requireDomainId(ctx?.state)
|
|
209
|
-
const figureId = figureIdFrom(args, ctx?.
|
|
210
|
-
const row = await
|
|
283
|
+
const figureId = figureIdFrom(args, ctx?.host)
|
|
284
|
+
const row = await resolveFigureTarget(figureId, domainId, ctx?.host)
|
|
285
|
+
if (!row) return missingFigureTarget()
|
|
211
286
|
|
|
212
|
-
const current = sourceForTool(row, ctx?.
|
|
287
|
+
const current = sourceForTool(row, ctx?.host)
|
|
213
288
|
if (!current.source) {
|
|
214
289
|
// 저장된 정본이 없는 상태가 실제로 있다(새로 만들고 저장 안 한 것). 지어내지 않는다.
|
|
215
290
|
return { id: row.id, type: row.type, name: row.name, source: null, cost: null }
|
|
@@ -240,10 +315,11 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
240
315
|
schema: { type: 'object', properties: { ...FIGURE_ID_ARG } },
|
|
241
316
|
builder: async (args: any, ctx: any) => {
|
|
242
317
|
const domainId = requireDomainId(ctx?.state)
|
|
243
|
-
const figureId = figureIdFrom(args, ctx?.
|
|
244
|
-
const row = await
|
|
318
|
+
const figureId = figureIdFrom(args, ctx?.host)
|
|
319
|
+
const row = await resolveFigureTarget(figureId, domainId, ctx?.host)
|
|
320
|
+
if (!row) return missingFigureTarget()
|
|
245
321
|
|
|
246
|
-
const current = sourceForTool(row, ctx?.
|
|
322
|
+
const current = sourceForTool(row, ctx?.host)
|
|
247
323
|
const { inspectSource } = getDeps()
|
|
248
324
|
return { ...inspectSource(current.source ? JSON.stringify(current.source) : null), draft: current.draft }
|
|
249
325
|
}
|
|
@@ -288,15 +364,15 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
288
364
|
후보가 온다.
|
|
289
365
|
*/
|
|
290
366
|
let base: unknown
|
|
291
|
-
if (!args?.create) {
|
|
292
|
-
const open = typeof args?.figureId === 'string' ? args.figureId : ctx?.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
base = sourceForTool(row, ctx?.
|
|
367
|
+
if (!args?.create && args?.figureId !== null) {
|
|
368
|
+
const open = typeof args?.figureId === 'string' ? args.figureId : ctx?.host?.figureId
|
|
369
|
+
const row = await resolveFigureTarget(open ? open.trim() : undefined, domainId, ctx?.host)
|
|
370
|
+
if (row) {
|
|
371
|
+
base = sourceForTool(row, ctx?.host).source ?? undefined
|
|
296
372
|
}
|
|
297
373
|
}
|
|
298
374
|
|
|
299
|
-
const palette = Array.isArray(ctx?.
|
|
375
|
+
const palette = Array.isArray(ctx?.host?.palette) ? ctx.host.palette : []
|
|
300
376
|
if (palette.length === 0) {
|
|
301
377
|
/*
|
|
302
378
|
팔레트를 모르면 모델이 색을 지어내고, 그 후보는 그리는 시점에 `resolveToken` 이 던진다 —
|
|
@@ -306,14 +382,24 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
306
382
|
}
|
|
307
383
|
|
|
308
384
|
const { proposeFigure } = getDeps()
|
|
309
|
-
const feedback = feedbackForTool(ctx?.
|
|
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
|
+
|
|
310
395
|
const result = await proposeFigure({
|
|
311
396
|
prompt,
|
|
312
397
|
palette,
|
|
313
398
|
base,
|
|
314
399
|
type: typeof args?.type === 'string' ? args.type : undefined,
|
|
315
400
|
feedback,
|
|
316
|
-
refine: !!feedback?.length
|
|
401
|
+
refine: !!feedback?.length,
|
|
402
|
+
image: hostImage
|
|
317
403
|
})
|
|
318
404
|
|
|
319
405
|
/*
|
|
@@ -330,7 +416,19 @@ function figureToolSpecs(): ToolSpec[] {
|
|
|
330
416
|
reason:
|
|
331
417
|
`등급 ${result.score.grade} · 삼각형 ${result.cost.triangles} · ` +
|
|
332
418
|
`한 도면 ${result.cost.at.drawCalls}회 · ${result.attempts}번에 만들었다`,
|
|
333
|
-
|
|
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
|
+
],
|
|
426
|
+
args: {
|
|
427
|
+
baseSource: base ? JSON.stringify(base) : undefined,
|
|
428
|
+
prompt, figureId: base ? figureIdFrom(args, ctx?.host) : undefined,
|
|
429
|
+
draftType: base && ctx?.host?.editorMode === 'new' && !figureIdFrom(args, ctx?.host)
|
|
430
|
+
? (base as any).type : undefined
|
|
431
|
+
},
|
|
334
432
|
source: result.source,
|
|
335
433
|
score: result.score,
|
|
336
434
|
cost: result.cost,
|
|
@@ -356,10 +454,14 @@ export function registerFigureToolCategories(): void {
|
|
|
356
454
|
description: '저작 중인 Figure 의 정본·비용·발행 판정 조회와 후보 제안(저장 아님).',
|
|
357
455
|
guidance: [
|
|
358
456
|
'- **도형을 만들거나 고쳐 달라는 요청에는 반드시 `proposeFigureCandidate` 를 호출한다.**',
|
|
457
|
+
' 후보는 도구 호출 결과에서만 생긴다.',
|
|
359
458
|
' 호출 없이 "후보를 만들었습니다" · "적용 버튼을 누르세요" 라고 쓰는 것은 **거짓이다** — 화면에 후보 칸이 생기지 않으므로 사용자는 존재하지 않는 버튼을 찾는다.',
|
|
360
|
-
' 후보
|
|
459
|
+
' 도크에서는 후보 카드의 버튼을 눌러야 모델러 비교 화면이 열린다. 아직 열지 않은 비교 화면이 이미 보인다고 말하지 않는다.',
|
|
460
|
+
' 요청한 부품만 바뀌었다고 추측하지 않는다. 후보와 원본을 비교한 근거가 없으면 변경 범위는 비교 화면에서 확인하도록 안내한다.',
|
|
361
461
|
'- 정본을 직접 저장하지 않는다. 후보만 내고, 받을지 버릴지는 저작자가 화면에서 정한다.',
|
|
462
|
+
'- 좌표와 크기는 X/Y/Z 축 이름으로 정확하게 표시한다. 확인되지 않은 가로·세로·높이 의미를 임의로 붙이지 않는다.',
|
|
362
463
|
'- **고치라는 요청이면 먼저 `getFigureSource` 를 부른다.** 읽지 않고 고치면 짐작을 상대로 후보를 내게 되고, 부품 이름이 갈려 이미 배치된 보드의 바인딩이 끊긴다.',
|
|
464
|
+
'- 목록 화면에는 열린 도형이 없다. 기존 도형을 조회·수정할 때는 `listFigures`에서 사용자가 말한 이름·타입의 id를 찾는다. 대상이 모호할 때만 어느 도형인지 묻는다. 새 제작 요청에는 기존 정본을 조회하지 않고 `proposeFigureCandidate`를 create: true로 호출한다.',
|
|
363
465
|
'- 타입 이름을 지어내지 않는다 — `listFigures` 로 확인한다. **타입 이름은 저장되는 식별자**이고 보드가 그 이름으로 도형을 찾는다.',
|
|
364
466
|
'- 발행이 안 되는 이유를 물으면 `inspectFigure` 를 부른다. 판정 없이 "발행할 수 있습니다"라고 말하지 않는다.',
|
|
365
467
|
' 판정의 각 항목은 **왜 문제인지와 무엇을 바꾸면 되는지**를 함께 담고 있다. 그대로 전한다 — 요약하면 저작자가 어느 부품의 어느 축을 고칠지 다시 찾아야 한다.',
|