@yoonion/mimi-seed-mcp 0.13.6 → 0.13.8
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/appstore/errors.js +29 -2
- package/dist/appstore/tools.d.ts +1 -0
- package/dist/appstore/tools.js +60 -24
- package/dist/registers/appstore.js +9 -3
- package/package.json +1 -1
package/dist/appstore/errors.js
CHANGED
|
@@ -18,8 +18,17 @@ const CODE_HINTS = {
|
|
|
18
18
|
// 버전 상태가 편집 불가 (READY_FOR_SALE / WAITING_FOR_REVIEW 등)
|
|
19
19
|
ENTITY_STATE_INVALID: '버전 상태가 편집 가능 단계가 아니에요 (이미 심사중/출시됨). 새 versionString 으로 appstore_create_version 필요할 수 있어요.',
|
|
20
20
|
// 필수 필드 누락
|
|
21
|
+
// ⚠️ Apple 이 실제로 보내는 코드는 점 표기다 (ENTITY_ERROR.ATTRIBUTE.REQUIRED).
|
|
22
|
+
// 아래 언더스코어 키들은 오래 매칭된 적이 없었다 — 점 표기를 정본으로 두고
|
|
23
|
+
// 언더스코어 변형은 혹시 모를 옛 응답용으로 남긴다 (2026-07-21 실측으로 발견).
|
|
24
|
+
'ENTITY_ERROR.ATTRIBUTE.REQUIRED': '필수 필드가 빠졌어요 — source.pointer 위치 확인.',
|
|
21
25
|
ENTITY_ERROR_ATTRIBUTE_REQUIRED: '필수 필드가 빠졌어요 — source.pointer 위치 확인.',
|
|
22
|
-
// 길이
|
|
26
|
+
// 길이 초과 — detail 에 실제 상한이 숫자로 들어온다 ("Max number of characters is (55)").
|
|
27
|
+
'ENTITY_ERROR.ATTRIBUTE.INVALID.TOO_LONG': '길이 초과 — detail 의 괄호 숫자가 Apple 이 강제하는 실제 상한이에요. 그 값에 맞춰 줄이세요.',
|
|
28
|
+
// 심사 중이라 필드가 잠김. 상품이 WAITING_FOR_REVIEW/IN_REVIEW 면 현지화를 못 고친다.
|
|
29
|
+
'ENTITY_ERROR.ATTRIBUTE.INVALID.UNMODIFIABLE': '지금 상태에선 못 고치는 필드예요 — 상품이 심사 중이면 현지화가 잠깁니다. '
|
|
30
|
+
+ '심사 결과를 기다리거나 appstore_cancel_review 로 철회한 뒤 수정하세요.',
|
|
31
|
+
'ENTITY_ERROR.ATTRIBUTE.INVALID': '필드 값이 정책 위반(길이/형식). source.pointer 위치 확인.',
|
|
23
32
|
ENTITY_ERROR_ATTRIBUTE_INVALID: '필드 값이 정책 위반(길이/형식). source.pointer 위치 확인.',
|
|
24
33
|
// 빌드 attach 시 빌드를 못 찾음
|
|
25
34
|
NOT_FOUND: '대상 리소스를 찾을 수 없어요 — id/versionId/buildId 가 유효한지, 동일 앱 소속인지 확인.',
|
|
@@ -45,12 +54,30 @@ function parseAppleErrorBody(body) {
|
|
|
45
54
|
return null;
|
|
46
55
|
}
|
|
47
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* 코드 → hint. 정확히 일치하는 게 없으면 점 표기를 뒤에서부터 잘라 상위 코드로 폴백한다.
|
|
59
|
+
* Apple 은 `ENTITY_ERROR.ATTRIBUTE.INVALID.TOO_LONG` 처럼 세분화된 코드를 계속 늘리는데,
|
|
60
|
+
* 그때마다 키를 추가하지 않아도 `...INVALID` 수준의 안내는 나가야 한다.
|
|
61
|
+
*/
|
|
62
|
+
function resolveHint(code) {
|
|
63
|
+
if (!code)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (CODE_HINTS[code])
|
|
66
|
+
return CODE_HINTS[code];
|
|
67
|
+
const parts = code.split('.');
|
|
68
|
+
for (let i = parts.length - 1; i > 0; i -= 1) {
|
|
69
|
+
const hint = CODE_HINTS[parts.slice(0, i).join('.')];
|
|
70
|
+
if (hint)
|
|
71
|
+
return hint;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
48
75
|
/** 단일 Apple error → 한 줄 사람용 표기. */
|
|
49
76
|
function formatAppleError(e) {
|
|
50
77
|
const code = e.code ?? 'UNKNOWN';
|
|
51
78
|
const detail = e.detail ?? e.title ?? '(no detail)';
|
|
52
79
|
const pointer = e.source?.pointer ?? e.source?.parameter;
|
|
53
|
-
const hint = e.code
|
|
80
|
+
const hint = resolveHint(e.code);
|
|
54
81
|
const parts = [`[${code}] ${detail}`];
|
|
55
82
|
if (pointer)
|
|
56
83
|
parts.push(`@ ${pointer}`);
|
package/dist/appstore/tools.d.ts
CHANGED
|
@@ -166,6 +166,7 @@ export declare function submitVersionForReview(versionId: string): Promise<{
|
|
|
166
166
|
versionId: string;
|
|
167
167
|
reusedSubmission: boolean;
|
|
168
168
|
itemAttached: boolean;
|
|
169
|
+
recoveredFromStaleSubmission: boolean;
|
|
169
170
|
state: any;
|
|
170
171
|
}>;
|
|
171
172
|
export declare function addProductToReviewSubmission(args: {
|
package/dist/appstore/tools.js
CHANGED
|
@@ -599,38 +599,73 @@ export async function buildSubmitForReviewPreview(versionId) {
|
|
|
599
599
|
whatsNewByLocale,
|
|
600
600
|
};
|
|
601
601
|
}
|
|
602
|
+
async function createReviewSubmission(appId, platform) {
|
|
603
|
+
const created = await apiPost('/reviewSubmissions', {
|
|
604
|
+
data: {
|
|
605
|
+
type: 'reviewSubmissions',
|
|
606
|
+
attributes: { platform },
|
|
607
|
+
relationships: {
|
|
608
|
+
app: { data: { type: 'apps', id: appId } },
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
const submissionId = created?.data?.id;
|
|
613
|
+
if (!submissionId) {
|
|
614
|
+
throw new Error(`reviewSubmission 생성 응답에 id가 없어: ${JSON.stringify(created)}`);
|
|
615
|
+
}
|
|
616
|
+
return submissionId;
|
|
617
|
+
}
|
|
618
|
+
function isItemAddRejected(error) {
|
|
619
|
+
const cause = error?.cause;
|
|
620
|
+
if (cause?.status !== 409)
|
|
621
|
+
return false;
|
|
622
|
+
return (cause.parsedErrors ?? []).some((e) => (e.code ?? '').startsWith('STATE_ERROR'));
|
|
623
|
+
}
|
|
602
624
|
export async function submitVersionForReview(versionId) {
|
|
603
625
|
const { appId, platform } = await getVersionAppAndPlatform(versionId);
|
|
604
|
-
// 1.
|
|
626
|
+
// 1. 열린 reviewSubmission이 있으면 재사용, 없으면 새로 생성
|
|
605
627
|
let submissionId = await findOpenReviewSubmission(appId, platform);
|
|
606
|
-
|
|
628
|
+
let reusedSubmission = Boolean(submissionId);
|
|
629
|
+
// findOpenReviewSubmission 은 WAITING_FOR_REVIEW 도 잡아온다. 그 상태의 실제 진행도는
|
|
630
|
+
// API 의 state 필드보다 앞서 있을 수 있어(실측: 이미 심사 큐를 탄 옛 제출), 항목 추가
|
|
631
|
+
// 자체를 거부당하는 경우가 있다 — 아래 recoveredFromStaleSubmission 이 그 케이스다.
|
|
632
|
+
let recoveredFromStaleSubmission = false;
|
|
607
633
|
if (!submissionId) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
type: 'reviewSubmissions',
|
|
611
|
-
attributes: { platform },
|
|
612
|
-
relationships: {
|
|
613
|
-
app: { data: { type: 'apps', id: appId } },
|
|
614
|
-
},
|
|
615
|
-
},
|
|
616
|
-
});
|
|
617
|
-
submissionId = created?.data?.id;
|
|
618
|
-
if (!submissionId) {
|
|
619
|
-
throw new Error(`reviewSubmission 생성 응답에 id가 없어: ${JSON.stringify(created)}`);
|
|
620
|
-
}
|
|
634
|
+
submissionId = await createReviewSubmission(appId, platform);
|
|
635
|
+
reusedSubmission = false;
|
|
621
636
|
}
|
|
622
637
|
// 2. 버전을 reviewSubmissionItems로 attach (이미 붙어있으면 skip)
|
|
623
|
-
|
|
638
|
+
let alreadyAttached = reusedSubmission ? await isVersionAttached(submissionId, versionId) : false;
|
|
624
639
|
if (!alreadyAttached) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
640
|
+
try {
|
|
641
|
+
await apiPost('/reviewSubmissionItems', {
|
|
642
|
+
data: {
|
|
643
|
+
type: 'reviewSubmissionItems',
|
|
644
|
+
relationships: {
|
|
645
|
+
reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
|
|
646
|
+
appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
|
|
647
|
+
},
|
|
631
648
|
},
|
|
632
|
-
}
|
|
633
|
-
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
if (!reusedSubmission || !isItemAddRejected(error))
|
|
653
|
+
throw error;
|
|
654
|
+
// 재사용하려던 묶음이 실제로는 잠겨 있었다 — 새 묶음을 만들어 한 번만 재시도한다.
|
|
655
|
+
submissionId = await createReviewSubmission(appId, platform);
|
|
656
|
+
reusedSubmission = false;
|
|
657
|
+
recoveredFromStaleSubmission = true;
|
|
658
|
+
alreadyAttached = false;
|
|
659
|
+
await apiPost('/reviewSubmissionItems', {
|
|
660
|
+
data: {
|
|
661
|
+
type: 'reviewSubmissionItems',
|
|
662
|
+
relationships: {
|
|
663
|
+
reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
|
|
664
|
+
appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
}
|
|
634
669
|
}
|
|
635
670
|
// 3. PATCH submitted=true → state: CREATED → WAITING_FOR_REVIEW
|
|
636
671
|
const submitted = await apiPatch(`/reviewSubmissions/${submissionId}`, {
|
|
@@ -647,6 +682,7 @@ export async function submitVersionForReview(versionId) {
|
|
|
647
682
|
versionId,
|
|
648
683
|
reusedSubmission,
|
|
649
684
|
itemAttached: !alreadyAttached,
|
|
685
|
+
recoveredFromStaleSubmission,
|
|
650
686
|
state: submitted?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW',
|
|
651
687
|
};
|
|
652
688
|
}
|
|
@@ -511,13 +511,19 @@ export function registerAppstoreTools(server) {
|
|
|
511
511
|
});
|
|
512
512
|
server.tool('appstore_update_product_localization', 'App Store IAP/구독 상품의 현지화(표시 이름·설명)를 로케일 단위로 upsert — 있으면 수정, 없으면 생성. ' +
|
|
513
513
|
'현지화가 비면 상품이 MISSING_METADATA 에서 안 풀려 심사에 넣을 수 없다 (리뷰 노트·스크린샷과는 별개 리소스). ' +
|
|
514
|
-
'locale 은 App Store 표기(ko, en-US, ja, zh-Hant)를 쓴다.
|
|
514
|
+
'locale 은 App Store 표기(ko, en-US, ja, zh-Hant)를 쓴다. ' +
|
|
515
|
+
'길이 상한은 리소스마다 다르고 Apple 이 강제한다 — 구독은 실측으로 name 35 / description 55 다. ' +
|
|
516
|
+
'초과하면 Apple 이 "Max number of characters is (N)" 으로 실제 상한을 알려주므로 그 값에 맞춰 줄이면 된다. ' +
|
|
517
|
+
'⚠️ 심사 중인 상품은 현지화가 잠겨 UNMODIFIABLE 로 거부된다 — 결과를 기다리거나 철회 후 수정한다.', {
|
|
515
518
|
appId: z.string().describe('App Store 앱 ID (숫자형, appstore_list_apps 결과)'),
|
|
516
519
|
productId: z.string().describe('상품 ID (appstore_list_products 결과)'),
|
|
517
520
|
productType: z.enum(['subscription', 'consumable', 'non_consumable']).describe('상품 유형'),
|
|
518
521
|
locale: z.string().describe('로케일 (예: ko, en-US, ja, zh-Hant)'),
|
|
519
|
-
|
|
520
|
-
|
|
522
|
+
// 실제 상한은 Apple 이 리소스별로 강제한다(구독 name 35 / description 55 실측).
|
|
523
|
+
// 여기서 좁게 잡으면 정상 문구를 클라이언트가 먼저 거부한다 — 실제로 45로 잡아
|
|
524
|
+
// 55자짜리 정상 설명을 막았다. 오타 수준만 걸러내는 넉넉한 상한만 둔다.
|
|
525
|
+
name: z.string().max(200).optional().describe('표시 이름. 새 로케일 생성 시 필수 (구독 실측 상한 35자)'),
|
|
526
|
+
description: z.string().max(500).optional().describe('설명. 생략하면 기존 값 유지 (구독 실측 상한 55자)'),
|
|
521
527
|
}, async ({ appId, productId, productType, locale, name, description }) => {
|
|
522
528
|
const creds = requireAppStoreCreds();
|
|
523
529
|
const products = await listAppleProducts({
|
package/package.json
CHANGED