@sayren/store-sdk 0.30.13 → 0.30.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/index.d.mts +1168 -197
- package/dist/index.mjs +386 -11
- package/package.json +1 -1
- package/src/client.ts +59 -1
- package/src/index.ts +1 -0
- package/src/schemas/ai.ts +376 -0
- package/src/schemas/app-manifest.ts +1 -1
- package/src/schemas/apps.ts +1 -1
- package/src/schemas/storefront-hosting.ts +30 -6
- package/src/schemas/upload.ts +12 -1
package/src/index.ts
CHANGED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 상점 플랫폼 AI 채팅(이슈 #124) — 대화·메시지·실행과 실행 이벤트 스트림의 계약.
|
|
5
|
+
*
|
|
6
|
+
* 상점 플랫폼 전용이다(`@ConsoleOnly`, 공개 OpenAPI 문서 제외). 메시지를 보내면 api가 실행(run)을 만들어 202로 `streamId`를 돌려주고,
|
|
7
|
+
* 응답은 별도 중계(`streamUrl`, SSE)로 받는다. 실행은 브라우저와 무관하게 서버에서 끝까지 돈다 — 다시 열면 진행 중 실행의 스트림에
|
|
8
|
+
* 처음부터 다시 붙거나(보관 1시간) 확정된 메시지를 읽는다.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** 첨부 하나 — 업로드(`POST /v1/uploads`, 목적 `ai-attachment`)가 돌려준 공개 주소 */
|
|
12
|
+
export const aiAttachmentSchema = z.object({
|
|
13
|
+
url: z.string().url().max(2048),
|
|
14
|
+
name: z.string().trim().min(1).max(200),
|
|
15
|
+
mediaType: z.string().trim().min(1).max(100).describe("image/* 또는 application/pdf"),
|
|
16
|
+
});
|
|
17
|
+
export type AiAttachment = z.infer<typeof aiAttachmentSchema>;
|
|
18
|
+
|
|
19
|
+
export const AI_MESSAGE_MAX_CHARS = 8_000;
|
|
20
|
+
export const AI_MESSAGE_MAX_ATTACHMENTS = 5;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 편집 미리보기에서 고른 요소(#65) — 미리보기 안 선택 스크립트가 만든 값이라 사이트 코드가 꾸밀 수 있다. api는 모델에 넘길 때 데이터로
|
|
24
|
+
* 감싼다. `source`는 그 컴포넌트를 **사용한** 위치다(작업 트리 상대 경로). React 개발 정보가 없으면 null이다
|
|
25
|
+
*/
|
|
26
|
+
export const aiPreviewSelectionSchema = z.object({
|
|
27
|
+
component: z.string().max(120).nullable(),
|
|
28
|
+
source: z
|
|
29
|
+
.object({
|
|
30
|
+
path: z.string().max(300),
|
|
31
|
+
line: z.number().int().positive().max(1_000_000),
|
|
32
|
+
column: z.number().int().positive().max(100_000).optional(),
|
|
33
|
+
})
|
|
34
|
+
.nullable(),
|
|
35
|
+
tag: z.string().max(40),
|
|
36
|
+
text: z.string().max(200),
|
|
37
|
+
pagePath: z.string().max(500),
|
|
38
|
+
});
|
|
39
|
+
export type AiPreviewSelection = z.infer<typeof aiPreviewSelectionSchema>;
|
|
40
|
+
|
|
41
|
+
/** 실행 상태 — 끝난 상태는 `DONE`·`FAILED`·`CANCELLED`다 */
|
|
42
|
+
export const aiRunStatusSchema = z.enum([
|
|
43
|
+
"QUEUED",
|
|
44
|
+
"RUNNING",
|
|
45
|
+
"CANCEL_REQUESTED",
|
|
46
|
+
"DONE",
|
|
47
|
+
"FAILED",
|
|
48
|
+
"CANCELLED",
|
|
49
|
+
]);
|
|
50
|
+
export type AiRunStatus = z.infer<typeof aiRunStatusSchema>;
|
|
51
|
+
|
|
52
|
+
export const AI_RUN_ACTIVE_STATUSES: readonly AiRunStatus[] = [
|
|
53
|
+
"QUEUED",
|
|
54
|
+
"RUNNING",
|
|
55
|
+
"CANCEL_REQUESTED",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** 진행 중 실행의 스트림 접속 정보. `streamToken`은 이 스트림 하나만 읽는 단기 자격(Bearer)이다 */
|
|
59
|
+
export const aiStreamAccessSchema = z.object({
|
|
60
|
+
streamId: z.string(),
|
|
61
|
+
streamUrl: z.string().url(),
|
|
62
|
+
streamToken: z.string(),
|
|
63
|
+
});
|
|
64
|
+
export type AiStreamAccess = z.infer<typeof aiStreamAccessSchema>;
|
|
65
|
+
|
|
66
|
+
export const aiRunSchema = z.object({
|
|
67
|
+
id: z.string().describe("실행 id = streamId"),
|
|
68
|
+
status: aiRunStatusSchema,
|
|
69
|
+
errorCode: z.string().nullable(),
|
|
70
|
+
createdAt: z.string(),
|
|
71
|
+
finishedAt: z.string().nullable(),
|
|
72
|
+
});
|
|
73
|
+
export type AiRun = z.infer<typeof aiRunSchema>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 화면에 그리는 메시지 조각. 모델 원문(tool_use 블록 등)은 api가 보관하고 화면에는 이 형태만 준다.
|
|
77
|
+
* - `text`: 글
|
|
78
|
+
* - `tool`: 도구 호출 한 번(이름은 사람이 읽는 라벨, 결과 요약·링크)
|
|
79
|
+
* - `choices`: 선택지 버튼(가장 최근 것만 누를 수 있다)
|
|
80
|
+
* - `attachment`: 사용자가 올린 첨부
|
|
81
|
+
* - `approval`: 승인 대기로 남은 쓰기
|
|
82
|
+
*/
|
|
83
|
+
export const aiMessagePartSchema = z.discriminatedUnion("type", [
|
|
84
|
+
z.object({ type: z.literal("text"), text: z.string() }),
|
|
85
|
+
z.object({
|
|
86
|
+
type: z.literal("tool"),
|
|
87
|
+
toolCallId: z.string(),
|
|
88
|
+
name: z.string(),
|
|
89
|
+
label: z.string(),
|
|
90
|
+
status: z.enum(["ok", "error", "unknown"]),
|
|
91
|
+
summary: z.string().nullable(),
|
|
92
|
+
}),
|
|
93
|
+
z.object({
|
|
94
|
+
type: z.literal("choices"),
|
|
95
|
+
question: z.string(),
|
|
96
|
+
options: z
|
|
97
|
+
.array(z.object({ label: z.string(), value: z.string() }))
|
|
98
|
+
.min(1)
|
|
99
|
+
.max(6),
|
|
100
|
+
}),
|
|
101
|
+
z.object({ type: z.literal("attachment"), attachment: aiAttachmentSchema }),
|
|
102
|
+
/** 공개 뒤 승인이 필요한 쓰기를 승인 요청으로 남겼다(셀러가 승인함에서 결정한다) */
|
|
103
|
+
z.object({ type: z.literal("approval"), approvalId: z.string(), title: z.string() }),
|
|
104
|
+
/** 사용자가 미리보기에서 고른 요소(#65) */
|
|
105
|
+
z.object({ type: z.literal("selection"), selection: aiPreviewSelectionSchema }),
|
|
106
|
+
]);
|
|
107
|
+
export type AiMessagePart = z.infer<typeof aiMessagePartSchema>;
|
|
108
|
+
|
|
109
|
+
export const aiMessageSchema = z.object({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
role: z.enum(["user", "assistant"]),
|
|
112
|
+
parts: z.array(aiMessagePartSchema),
|
|
113
|
+
runId: z.string().nullable(),
|
|
114
|
+
createdAt: z.string(),
|
|
115
|
+
});
|
|
116
|
+
export type AiMessage = z.infer<typeof aiMessageSchema>;
|
|
117
|
+
|
|
118
|
+
export const aiConversationSummarySchema = z.object({
|
|
119
|
+
id: z.string(),
|
|
120
|
+
title: z.string().nullable(),
|
|
121
|
+
createdAt: z.string(),
|
|
122
|
+
updatedAt: z.string(),
|
|
123
|
+
});
|
|
124
|
+
export type AiConversationSummary = z.infer<typeof aiConversationSummarySchema>;
|
|
125
|
+
|
|
126
|
+
export const aiConversationSchema = aiConversationSummarySchema.extend({
|
|
127
|
+
messages: z.array(aiMessageSchema),
|
|
128
|
+
/** 진행 중 실행(없으면 null) — 스트림에 다시 붙을 때 쓴다 */
|
|
129
|
+
activeRun: aiRunSchema.extend({ stream: aiStreamAccessSchema }).nullable(),
|
|
130
|
+
/** 마지막 실행(진행 중이 아니면 결과 표시용) */
|
|
131
|
+
lastRun: aiRunSchema.nullable(),
|
|
132
|
+
});
|
|
133
|
+
export type AiConversation = z.infer<typeof aiConversationSchema>;
|
|
134
|
+
|
|
135
|
+
export const createAiConversationRequestSchema = z.object({
|
|
136
|
+
/** 웹사이트에서 남긴 초안(#124 M2) — 연결한 사용자만 쓴다. 주면 첫 메시지로 실행을 시작한다 */
|
|
137
|
+
draftId: z.string().max(64).optional(),
|
|
138
|
+
});
|
|
139
|
+
export type CreateAiConversationRequest = z.infer<typeof createAiConversationRequestSchema>;
|
|
140
|
+
|
|
141
|
+
export const createAiConversationResponseSchema = z.object({
|
|
142
|
+
conversation: aiConversationSummarySchema,
|
|
143
|
+
/** `draftId`로 시작했으면 첫 실행 */
|
|
144
|
+
run: aiStreamAccessSchema.nullable(),
|
|
145
|
+
});
|
|
146
|
+
export type CreateAiConversationResponse = z.infer<typeof createAiConversationResponseSchema>;
|
|
147
|
+
|
|
148
|
+
export const sendAiMessageRequestSchema = z
|
|
149
|
+
.object({
|
|
150
|
+
text: z.string().trim().max(AI_MESSAGE_MAX_CHARS),
|
|
151
|
+
attachments: z.array(aiAttachmentSchema).max(AI_MESSAGE_MAX_ATTACHMENTS).default([]),
|
|
152
|
+
/** 선택지 버튼으로 보낸 답이면 그 값(화면 표시는 `text`) */
|
|
153
|
+
choice: z.string().max(200).optional(),
|
|
154
|
+
/** 편집 미리보기에서 고른 요소(#65) — 모델은 이 요소를 기준으로 고친다 */
|
|
155
|
+
selection: aiPreviewSelectionSchema.optional(),
|
|
156
|
+
})
|
|
157
|
+
.refine((body) => body.text.length > 0 || body.attachments.length > 0, {
|
|
158
|
+
message: "내용이나 첨부가 있어야 합니다",
|
|
159
|
+
});
|
|
160
|
+
export type SendAiMessageRequest = z.input<typeof sendAiMessageRequestSchema>;
|
|
161
|
+
|
|
162
|
+
export const sendAiMessageResponseSchema = aiStreamAccessSchema.extend({
|
|
163
|
+
messageId: z.string(),
|
|
164
|
+
});
|
|
165
|
+
export type SendAiMessageResponse = z.infer<typeof sendAiMessageResponseSchema>;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* AI가 바꾸는 화면 영역 — 쓰기 도구가 시작되면 이벤트에 싣고, 패널이 그 화면으로 옮겨 반영된 데이터를 다시 읽는다(셀러가 AI가 하는 일을 같이 본다).
|
|
169
|
+
* 경로는 상점 플랫폼이 정한다(영역 → 라우트). `id`가 있으면 상세 화면이다
|
|
170
|
+
*/
|
|
171
|
+
export const aiFocusAreaSchema = z.enum([
|
|
172
|
+
"products",
|
|
173
|
+
"categories",
|
|
174
|
+
"collections",
|
|
175
|
+
"option_sets",
|
|
176
|
+
"coupons",
|
|
177
|
+
"points",
|
|
178
|
+
"orders",
|
|
179
|
+
"claims",
|
|
180
|
+
"customers",
|
|
181
|
+
"inquiries",
|
|
182
|
+
"customer_inquiries",
|
|
183
|
+
"store_settings",
|
|
184
|
+
"shipping",
|
|
185
|
+
"addresses",
|
|
186
|
+
"payments",
|
|
187
|
+
"login",
|
|
188
|
+
"members",
|
|
189
|
+
"order_flows",
|
|
190
|
+
"webhooks",
|
|
191
|
+
"storefront",
|
|
192
|
+
]);
|
|
193
|
+
export type AiFocusArea = z.infer<typeof aiFocusAreaSchema>;
|
|
194
|
+
|
|
195
|
+
export const aiFocusSchema = z.object({
|
|
196
|
+
area: aiFocusAreaSchema,
|
|
197
|
+
id: z.string().optional(),
|
|
198
|
+
});
|
|
199
|
+
export type AiFocus = z.infer<typeof aiFocusSchema>;
|
|
200
|
+
|
|
201
|
+
// ─── 작업 트리 변경·되돌리기(#65) ─────────────────────────────────────────
|
|
202
|
+
// AI 실행이 스토어프론트 작업 트리를 바꾸면 경로마다 이전·이후를 남긴다. 셀러는 실행 단위로 되돌린다(전부 아니면 전무).
|
|
203
|
+
|
|
204
|
+
/** 변경 기록 보관 기간 — 지나면 내용을 비우고 되돌릴 수 없다(410 `AI_RUN_CHANGES_EXPIRED`) */
|
|
205
|
+
export const AI_RUN_FILE_CHANGES_RETENTION_DAYS = 7;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 실행 하나의 변경 상태
|
|
209
|
+
* - `APPLIED`: 되돌릴 수 있다(진행 중 실행이 없을 때)
|
|
210
|
+
* - `INCOMPLETE`: 결과를 모르는 쓰기·이전 내용이 너무 큰 경로가 있다(되돌릴 수 없다)
|
|
211
|
+
* - `REVERTING`·`REVERTED`·`REVERT_UNKNOWN`(되돌리기 결과를 모름 — 다시 하지 않는다)·`EXPIRED`
|
|
212
|
+
*/
|
|
213
|
+
export const aiRunChangesStatusSchema = z.enum([
|
|
214
|
+
"APPLIED",
|
|
215
|
+
"INCOMPLETE",
|
|
216
|
+
"REVERTING",
|
|
217
|
+
"REVERTED",
|
|
218
|
+
"REVERT_UNKNOWN",
|
|
219
|
+
"EXPIRED",
|
|
220
|
+
]);
|
|
221
|
+
export type AiRunChangesStatus = z.infer<typeof aiRunChangesStatusSchema>;
|
|
222
|
+
|
|
223
|
+
export const aiFileChangeKindSchema = z.enum(["created", "modified", "deleted"]);
|
|
224
|
+
export type AiFileChangeKind = z.infer<typeof aiFileChangeKindSchema>;
|
|
225
|
+
|
|
226
|
+
export const aiRunChangesSchema = z.object({
|
|
227
|
+
runId: z.string(),
|
|
228
|
+
status: aiRunChangesStatusSchema,
|
|
229
|
+
files: z.array(
|
|
230
|
+
z.object({
|
|
231
|
+
path: z.string(),
|
|
232
|
+
change: aiFileChangeKindSchema.describe("결과를 모르는 경로는 `modified`로 보인다"),
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
235
|
+
});
|
|
236
|
+
export type AiRunChanges = z.infer<typeof aiRunChangesSchema>;
|
|
237
|
+
|
|
238
|
+
/** `GET /v1/ai/conversations/{id}/file-changes` — 대화의 실행별 변경(실행 만든 순) */
|
|
239
|
+
export const aiConversationFileChangesSchema = z.object({
|
|
240
|
+
runs: z.array(aiRunChangesSchema),
|
|
241
|
+
});
|
|
242
|
+
export type AiConversationFileChanges = z.infer<typeof aiConversationFileChangesSchema>;
|
|
243
|
+
|
|
244
|
+
/** `GET /v1/ai/runs/{runId}/file-changes` — 변경 보기. 텍스트가 아닌 파일·보관이 끝난 내용은 null이다(diff는 화면이 계산한다) */
|
|
245
|
+
export const aiRunFileDiffSchema = aiRunChangesSchema.extend({
|
|
246
|
+
files: z.array(
|
|
247
|
+
z.object({
|
|
248
|
+
path: z.string(),
|
|
249
|
+
change: aiFileChangeKindSchema,
|
|
250
|
+
before: z.string().nullable(),
|
|
251
|
+
after: z.string().nullable(),
|
|
252
|
+
binary: z.boolean(),
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
});
|
|
256
|
+
export type AiRunFileDiff = z.infer<typeof aiRunFileDiffSchema>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* `POST /v1/ai/runs/{runId}/revert` — 실행이 바꾼 경로를 그 전 내용으로 한 번에 돌린다. 오류: 409 `AI_RUN_IN_PROGRESS`(대화에 진행 중 실행),
|
|
260
|
+
* 409 `AI_RUN_REVERT_CONFLICT`(`details.conflicts` — 그 뒤 바뀐 경로·결과를 모르는 쓰기), 409 `AI_RUN_ALREADY_REVERTED`,
|
|
261
|
+
* 410 `AI_RUN_CHANGES_EXPIRED`, 404 `AI_RUN_CHANGES_NOT_FOUND`, 502 `AI_RUN_REVERT_UNKNOWN`(결과를 모름 — 다시 하지 않는다)
|
|
262
|
+
*/
|
|
263
|
+
export const aiRunRevertResultSchema = z.object({
|
|
264
|
+
runId: z.string(),
|
|
265
|
+
status: z.literal("REVERTED"),
|
|
266
|
+
paths: z.array(z.string()),
|
|
267
|
+
});
|
|
268
|
+
export type AiRunRevertResult = z.infer<typeof aiRunRevertResultSchema>;
|
|
269
|
+
|
|
270
|
+
/** 409 `AI_RUN_REVERT_CONFLICT`의 `error.details` */
|
|
271
|
+
export const aiRunRevertConflictDetailsSchema = z.object({
|
|
272
|
+
conflicts: z.array(z.object({ path: z.string(), reason: z.enum(["CHANGED", "UNKNOWN"]) })),
|
|
273
|
+
});
|
|
274
|
+
export type AiRunRevertConflictDetails = z.infer<typeof aiRunRevertConflictDetailsSchema>;
|
|
275
|
+
|
|
276
|
+
// ─── 실행 이벤트(SSE) ────────────────────────────────────────────────────
|
|
277
|
+
// SSE 한 건 = `id: {seq}` · `event: {type}` · `data: {JSON}`. seq는 실행 안에서 1부터 늘고 끊긴 실행을 이어도 이어진다.
|
|
278
|
+
|
|
279
|
+
export const aiStreamEventSchema = z.discriminatedUnion("type", [
|
|
280
|
+
/** 글 조각(약 100ms 묶음). 같은 `messageId`의 `text`에 이어 붙인다 */
|
|
281
|
+
z.object({ type: z.literal("delta"), messageId: z.string(), text: z.string() }),
|
|
282
|
+
z.object({
|
|
283
|
+
type: z.literal("tool_start"),
|
|
284
|
+
messageId: z.string(),
|
|
285
|
+
toolCallId: z.string(),
|
|
286
|
+
name: z.string(),
|
|
287
|
+
label: z.string(),
|
|
288
|
+
/** 쓰기 도구가 바꾸는 화면(읽기 도구는 없다) */
|
|
289
|
+
focus: aiFocusSchema.optional(),
|
|
290
|
+
}),
|
|
291
|
+
z.object({
|
|
292
|
+
type: z.literal("tool_result"),
|
|
293
|
+
messageId: z.string(),
|
|
294
|
+
toolCallId: z.string(),
|
|
295
|
+
status: z.enum(["ok", "error", "unknown"]),
|
|
296
|
+
summary: z.string().nullable(),
|
|
297
|
+
}),
|
|
298
|
+
z.object({
|
|
299
|
+
type: z.literal("choices"),
|
|
300
|
+
messageId: z.string(),
|
|
301
|
+
question: z.string(),
|
|
302
|
+
options: z.array(z.object({ label: z.string(), value: z.string() })),
|
|
303
|
+
}),
|
|
304
|
+
/** 공개 뒤 승인이 필요한 쓰기를 승인 요청으로 남겼다 */
|
|
305
|
+
z.object({
|
|
306
|
+
type: z.literal("approval"),
|
|
307
|
+
messageId: z.string(),
|
|
308
|
+
approvalId: z.string(),
|
|
309
|
+
title: z.string(),
|
|
310
|
+
}),
|
|
311
|
+
/** 메시지 하나가 확정됐다(이 시점의 내용은 대화 조회와 같다) */
|
|
312
|
+
z.object({ type: z.literal("message"), message: aiMessageSchema }),
|
|
313
|
+
z.object({ type: z.literal("error"), code: z.string(), message: z.string() }),
|
|
314
|
+
/** 실행이 끝났다. 뒤에 이벤트가 없다 */
|
|
315
|
+
z.object({ type: z.literal("done"), status: aiRunStatusSchema }),
|
|
316
|
+
]);
|
|
317
|
+
export type AiStreamEvent = z.infer<typeof aiStreamEventSchema>;
|
|
318
|
+
|
|
319
|
+
/** 중계에 쌓이는 이벤트 한 건 — 실행기 → 중계 `POST /internal/streams/{id}/events` */
|
|
320
|
+
export const aiStreamRecordSchema = z.object({
|
|
321
|
+
seq: z.number().int().positive(),
|
|
322
|
+
event: aiStreamEventSchema,
|
|
323
|
+
});
|
|
324
|
+
export type AiStreamRecord = z.infer<typeof aiStreamRecordSchema>;
|
|
325
|
+
|
|
326
|
+
// ─── 웹사이트 초안(#124 M2) ──────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
export const STORE_DRAFT_MAX_ATTACHMENTS = 5;
|
|
329
|
+
/** 초안 첨부 하나의 최대 크기(바이트) */
|
|
330
|
+
export const STORE_DRAFT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
331
|
+
/** 연결하지 않은 초안을 지우기까지 */
|
|
332
|
+
export const STORE_DRAFT_RETENTION_DAYS = 7;
|
|
333
|
+
|
|
334
|
+
export const createStoreDraftRequestSchema = z.object({
|
|
335
|
+
text: z.string().trim().min(1).max(AI_MESSAGE_MAX_CHARS),
|
|
336
|
+
attachments: z
|
|
337
|
+
.array(
|
|
338
|
+
z.object({
|
|
339
|
+
filename: z.string().trim().min(1).max(200),
|
|
340
|
+
size: z.number().int().positive().max(STORE_DRAFT_ATTACHMENT_MAX_BYTES),
|
|
341
|
+
}),
|
|
342
|
+
)
|
|
343
|
+
.max(STORE_DRAFT_MAX_ATTACHMENTS)
|
|
344
|
+
.default([]),
|
|
345
|
+
});
|
|
346
|
+
export type CreateStoreDraftRequest = z.input<typeof createStoreDraftRequestSchema>;
|
|
347
|
+
|
|
348
|
+
export const createStoreDraftResponseSchema = z.object({
|
|
349
|
+
draftId: z.string(),
|
|
350
|
+
/** 첨부마다 브라우저가 파일을 그대로 PUT할 자리(요청 순서와 같다) */
|
|
351
|
+
uploads: z.array(z.object({ uploadUrl: z.string().url(), url: z.string().url() })),
|
|
352
|
+
/** 다음 화면 — 상점 플랫폼 `/start?draft=…` */
|
|
353
|
+
continueUrl: z.string().url(),
|
|
354
|
+
});
|
|
355
|
+
export type CreateStoreDraftResponse = z.infer<typeof createStoreDraftResponseSchema>;
|
|
356
|
+
|
|
357
|
+
export const storeDraftSchema = z.object({
|
|
358
|
+
id: z.string(),
|
|
359
|
+
text: z.string(),
|
|
360
|
+
attachments: z.array(aiAttachmentSchema),
|
|
361
|
+
createdAt: z.string(),
|
|
362
|
+
/** 이 초안으로 시작한 상점(없으면 null) */
|
|
363
|
+
storeId: z.string().nullable(),
|
|
364
|
+
});
|
|
365
|
+
export type StoreDraft = z.infer<typeof storeDraftSchema>;
|
|
366
|
+
|
|
367
|
+
/** 로그인 전 초안 미리보기(`GET /v1/public/store-drafts/{id}`) — 로그인 화면이 「저장한 설명」을 보인다. 첨부는 이름만 */
|
|
368
|
+
export const storeDraftPreviewSchema = z.object({
|
|
369
|
+
id: z.string(),
|
|
370
|
+
text: z.string(),
|
|
371
|
+
attachments: z.array(z.object({ name: z.string(), mediaType: z.string() })),
|
|
372
|
+
expiresAt: z.string(),
|
|
373
|
+
/** 이미 상점에서 쓴 초안이면 true(다시 시작할 수 없다) */
|
|
374
|
+
used: z.boolean(),
|
|
375
|
+
});
|
|
376
|
+
export type StoreDraftPreview = z.infer<typeof storeDraftPreviewSchema>;
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./webhook-event";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* 서드파티 앱 매니페스트 `
|
|
11
|
+
* 서드파티 앱 매니페스트 `manifest.json` (이슈 #22) — CLI(`sayren app`)·api(빌드 `VERIFY`·버전 판정·동의 화면 자료)·런타임이
|
|
12
12
|
* 같은 원천을 쓴다. 매니페스트는 정적 JSON이고 빌드 없이 읽는다.
|
|
13
13
|
*
|
|
14
14
|
* 기능 하나는 **capability** 하나다(`APP_CAPABILITIES`). capability는 매니페스트 조각·동의 문단·넓어짐 판정(재동의 조건)을 한 곳에 둔다.
|
package/src/schemas/apps.ts
CHANGED
|
@@ -445,7 +445,7 @@ export type DeveloperAppCreate = z.infer<typeof developerAppCreateSchema>;
|
|
|
445
445
|
|
|
446
446
|
/** `PUT /platform/v1/developer/apps/{appId}/versions` — CLI `app deploy`가 올리는 번들 */
|
|
447
447
|
export const appArtifactUploadSchema = z.object({
|
|
448
|
-
manifest: z.unknown().describe("
|
|
448
|
+
manifest: z.unknown().describe("manifest.json 원문(JSON)"),
|
|
449
449
|
packageJson: z.string().max(256 * 1024),
|
|
450
450
|
lockfile: z
|
|
451
451
|
.string()
|
|
@@ -118,6 +118,14 @@ export function storefrontEditPreviewLabelOf(siteId: string): string {
|
|
|
118
118
|
return `${siteId.replaceAll("_", "-")}${STOREFRONT_EDIT_PREVIEW_SUFFIX}`;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* 버전 미리보기 호스트 이름의 라벨 — 올라간 버전 하나를 보는 고유 주소(`sfs_ab12cd34`, 3 → `sfs-ab12cd34-v3-preview`). 편집 미리보기와 같은
|
|
123
|
+
* `-preview` 공간이라 슬러그가 될 수 없다. 복제본·대조는 `storefrontEditPreviewLabelOf`와 같다.
|
|
124
|
+
*/
|
|
125
|
+
export function storefrontVersionPreviewLabelOf(siteId: string, versionNumber: number): string {
|
|
126
|
+
return `${siteId.replaceAll("_", "-")}-v${versionNumber}${STOREFRONT_EDIT_PREVIEW_SUFFIX}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
export const storefrontSlugSchema = z
|
|
122
130
|
.string()
|
|
123
131
|
.regex(
|
|
@@ -268,6 +276,12 @@ export const storefrontVersionSchema = z.object({
|
|
|
268
276
|
"만든 경로 (`CONSOLE` 상점 플랫폼 · `AI` AI 에이전트(MCP) · `CLI` `sayren` CLI · `API` 관리 API · `SYSTEM` 자동). 늘어날 수 있다",
|
|
269
277
|
),
|
|
270
278
|
createdAt: z.string().describe("만든 시각 (ISO 8601)"),
|
|
279
|
+
previewable: z
|
|
280
|
+
.boolean()
|
|
281
|
+
.optional()
|
|
282
|
+
.describe(
|
|
283
|
+
"미리보기 주소로 열 수 있는가 (`POST /v1/storefront/site/preview-link`의 `versionId`). 보관 기간이 지난 버전은 false",
|
|
284
|
+
),
|
|
271
285
|
});
|
|
272
286
|
export type StorefrontVersion = z.infer<typeof storefrontVersionSchema>;
|
|
273
287
|
|
|
@@ -515,12 +529,19 @@ export const storefrontSlugAvailabilityQuerySchema = z.object({
|
|
|
515
529
|
});
|
|
516
530
|
|
|
517
531
|
/**
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
532
|
+
* 버전 미리보기 링크 — 상점 플랫폼에서 로그인한 OWNER·ADMIN만 받는다(PAT·에이전트 불가). 버전마다 미리보기 전용 주소가 있고
|
|
533
|
+
* (오픈 준비 중이어도 실제 화면), 링크를 5분 안에 열면 그 브라우저에 그 주소 전용 12시간 쿠키가 생긴다. 사이트 주소는 오픈 전이면
|
|
534
|
+
* 누구에게나 「오픈 준비 중」이다. 미리보기 주소에 쿠키 없이 들어오면 상점 플랫폼 로그인과 OWNER·ADMIN 확인을 거친다.
|
|
521
535
|
*/
|
|
522
536
|
export const createStorefrontPreviewLinkRequestSchema = z.object({
|
|
523
537
|
path: z.string().max(512).optional().describe("열 경로 (`/`로 시작). 생략하면 `/`"),
|
|
538
|
+
versionId: z
|
|
539
|
+
.string()
|
|
540
|
+
.max(64)
|
|
541
|
+
.optional()
|
|
542
|
+
.describe(
|
|
543
|
+
"볼 버전. 생략하면 발행 버전이다. 보관 기간이 지난 버전(`previewable: false`)은 409 `STOREFRONT_VERSION_NOT_PREVIEWABLE`",
|
|
544
|
+
),
|
|
524
545
|
});
|
|
525
546
|
export type CreateStorefrontPreviewLinkRequest = z.infer<
|
|
526
547
|
typeof createStorefrontPreviewLinkRequestSchema
|
|
@@ -1328,9 +1349,12 @@ export type StorefrontVersionCreated = z.infer<typeof storefrontVersionCreatedSc
|
|
|
1328
1349
|
* 받는다(PAT·에이전트 불가). 5분 안에 열면 그 브라우저에 미리보기 호스트 전용 쿠키(12시간)가 생긴다. 편집 환경이
|
|
1329
1350
|
* 없으면 409 `EDIT_SESSION_REQUIRED`다(편집 환경이 `STARTING`이면 링크는 주고, 미리보기 화면이 기동 대기를 보여 준다).
|
|
1330
1351
|
*/
|
|
1331
|
-
export const createStorefrontEditPreviewLinkRequestSchema =
|
|
1332
|
-
|
|
1333
|
-
|
|
1352
|
+
export const createStorefrontEditPreviewLinkRequestSchema = z.object({
|
|
1353
|
+
path: z.string().max(512).optional().describe("열 경로 (`/`로 시작). 생략하면 `/`"),
|
|
1354
|
+
});
|
|
1355
|
+
export type CreateStorefrontEditPreviewLinkRequest = z.infer<
|
|
1356
|
+
typeof createStorefrontEditPreviewLinkRequestSchema
|
|
1357
|
+
>;
|
|
1334
1358
|
|
|
1335
1359
|
// ─── 소스 내려받기·올리기 (#60, `sayren` CLI) ─────────────────────────────
|
|
1336
1360
|
//
|
package/src/schemas/upload.ts
CHANGED
|
@@ -6,8 +6,14 @@ import { z } from "zod";
|
|
|
6
6
|
* - `product-image`: 상품 목록·상세의 상품 이미지. 목록 썸네일로 쓰이므로 정지 이미지만 받는다.
|
|
7
7
|
* - `description`: 상세설명 본문에 삽입하는 미디어. 동영상과 svg도 받는다.
|
|
8
8
|
* - `flow-attachment`: 주문 흐름 행동의 첨부(시안 등, 이슈 #115). 구매자에게 보이므로 정지 이미지와 PDF만 받는다.
|
|
9
|
+
* - `ai-attachment`: 상점 플랫폼 AI 채팅에 붙이는 자료(이슈 #124). 모델이 읽는 정지 이미지와 PDF만 받는다.
|
|
9
10
|
*/
|
|
10
|
-
export const uploadPurposeSchema = z.enum([
|
|
11
|
+
export const uploadPurposeSchema = z.enum([
|
|
12
|
+
"product-image",
|
|
13
|
+
"description",
|
|
14
|
+
"flow-attachment",
|
|
15
|
+
"ai-attachment",
|
|
16
|
+
]);
|
|
11
17
|
|
|
12
18
|
export type UploadPurpose = z.infer<typeof uploadPurposeSchema>;
|
|
13
19
|
|
|
@@ -39,6 +45,11 @@ export const UPLOAD_PURPOSE_LIMITS: Record<UploadPurpose, UploadPurposeLimit> =
|
|
|
39
45
|
extensions: ["jpg", "jpeg", "png", "webp", "avif", "gif", "pdf"],
|
|
40
46
|
maxBytes: 20 * MB,
|
|
41
47
|
},
|
|
48
|
+
// 모델이 받는 형식(jpeg·png·webp·gif, PDF)만 받는다
|
|
49
|
+
"ai-attachment": {
|
|
50
|
+
extensions: ["jpg", "jpeg", "png", "webp", "gif", "pdf"],
|
|
51
|
+
maxBytes: 10 * MB,
|
|
52
|
+
},
|
|
42
53
|
};
|
|
43
54
|
|
|
44
55
|
/** 파일명의 소문자 확장자. 확장자가 없으면 `""` */
|