@yoonion/mimi-seed-mcp 0.13.14 → 0.13.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.
- package/README.md +2 -2
- package/assets/agent-guide.md +39 -14
- package/dist/appstore/declarations.d.ts +73 -0
- package/dist/appstore/declarations.js +154 -0
- package/dist/appstore/release.d.ts +58 -0
- package/dist/appstore/release.js +173 -0
- package/dist/playstore/tools.d.ts +5 -0
- package/dist/playstore/tools.js +23 -0
- package/dist/registers/appstore.js +332 -0
- package/dist/registers/playstore.js +54 -0
- package/package.json +1 -1
- package/tool-manifest.json +13 -3
package/README.md
CHANGED
|
@@ -78,8 +78,8 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
78
78
|
|
|
79
79
|
| 영역 | 도구 수 | 주요 도구 |
|
|
80
80
|
|------|---------|-----------|
|
|
81
|
-
| App Store Connect |
|
|
82
|
-
| Google Play |
|
|
81
|
+
| App Store Connect | 50 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
|
|
82
|
+
| Google Play | 33 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
|
|
83
83
|
| Firebase | 20 | `firebase_create_project` / `firebase_create_android_app` / `firebase_get_android_config` / `firebase_create_ios_app` |
|
|
84
84
|
| AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
|
|
85
85
|
| CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
|
package/assets/agent-guide.md
CHANGED
|
@@ -16,7 +16,7 @@ and which actions are irreversible.
|
|
|
16
16
|
|
|
17
17
|
## 0. The one thing that trips every agent: deferred tools
|
|
18
18
|
|
|
19
|
-
Mimi Seed exposes **150+ MCP tools** across
|
|
19
|
+
Mimi Seed exposes **150+ MCP tools** across every store, cloud, CI, social, and video domain (full inventory:
|
|
20
20
|
[`docs/domain/tool-catalog.md`](domain/tool-catalog.md)). Harnesses that lazy-load large tool catalogs —
|
|
21
21
|
**Claude Code most notably** — register these tools as **deferred**: the tool *names*
|
|
22
22
|
are visible (in a system reminder), but the **input schemas are not loaded**. If you
|
|
@@ -42,20 +42,38 @@ tools directly — but the *call order* and *safety rules* below still apply.
|
|
|
42
42
|
|
|
43
43
|
### Ready-made `select:` batches
|
|
44
44
|
|
|
45
|
+
Every registered tool appears in at least one batch below (test-enforced), so a task you can name is a batch
|
|
46
|
+
you can paste. Pick the row for the job; batching two rows in one `select:` call is fine.
|
|
47
|
+
|
|
45
48
|
| Goal | `ToolSearch` query |
|
|
46
49
|
|------|--------------------|
|
|
47
50
|
| First contact / "what's connected?" | `select:mimi_seed_status,mimi_seed_auth_status,mimi_seed_auth_start,mimi_seed_remote_sync_credentials` |
|
|
48
|
-
|
|
|
49
|
-
| Play Store
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
51
|
+
| Release readiness (either store) | `select:release_status,playstore_check_submission_risks,appstore_check_submission_risks,screenshot_validate` |
|
|
52
|
+
| Play Store release | `select:playstore_get_app,playstore_list_tracks,playstore_update_release_notes,playstore_update_latest_release_notes,playstore_promote_release,playstore_submit_release,playstore_check_submission_risks,playstore_plan_release` |
|
|
53
|
+
| Play Store listing + images | `select:playstore_get_listing,playstore_update_listing,playstore_update_details,playstore_upload_image,playstore_list_images,playstore_replace_images,playstore_delete_all_images` |
|
|
54
|
+
| Play Store reviews + stats | `select:playstore_list_reviews,playstore_reply_review,playstore_get_statistics,generate_review_reply` |
|
|
55
|
+
| Play Store IAP / subscriptions | `select:playstore_list_products,playstore_list_inapp_products,playstore_list_subscriptions,playstore_create_onetime_product,playstore_create_subscription,playstore_update_product,playstore_update_product_listing,playstore_update_subscription_listing,playstore_update_product_state,playstore_delete_product` |
|
|
56
|
+
| App Store / TestFlight | `select:appstore_list_apps,appstore_verify_credentials,appstore_get_app,appstore_list_versions,appstore_create_version,appstore_get_metadata,appstore_update_whats_new,appstore_list_builds,appstore_attach_build,appstore_attach_latest_build,appstore_list_beta_groups,appstore_submit_for_review,appstore_check_submission_risks,appstore_plan_release` |
|
|
57
|
+
| App Store release control (after approval) | `select:appstore_release_status,appstore_release_version,appstore_update_release_type,appstore_phased_release,appstore_list_versions` |
|
|
58
|
+
| Pre-submission declarations (both stores) | `select:appstore_get_age_rating,appstore_update_age_rating,appstore_declare_encryption,appstore_get_availability,appstore_set_territory_availability,playstore_upload_data_safety` |
|
|
59
|
+
| App Store review submission (the bundle) | `select:appstore_list_review_submissions,appstore_add_version_to_review_submission,appstore_remove_review_submission_item,appstore_update_version_string,appstore_cancel_review` |
|
|
60
|
+
| App Store screenshots | `select:appstore_list_app_info_localizations,appstore_get_metadata,appstore_list_screenshots,appstore_upload_screenshot,appstore_delete_screenshot,appstore_delete_screenshot_set,screenshot_validate` |
|
|
61
|
+
| App Store app info + review notes | `select:appstore_get_app_info,appstore_update_app_info_localization,appstore_create_app_info_localization,appstore_update_localization,appstore_get_review_notes,appstore_update_review_notes` |
|
|
62
|
+
| App Store reviews | `select:appstore_list_reviews,appstore_reply_review,generate_review_reply` |
|
|
63
|
+
| App Store IAP (products + review metadata) | `select:appstore_list_products,appstore_create_inapp_purchase,appstore_create_subscription,appstore_update_product,appstore_list_product_localizations,appstore_update_product_localization,appstore_update_product_review_note,appstore_upload_product_review_screenshot,appstore_add_product_to_review,appstore_delete_product` |
|
|
53
64
|
| Release notes from commits | `select:generate_release_notes_from_commits,playstore_update_release_notes,appstore_update_whats_new` |
|
|
54
|
-
| Firebase setup | `select:firebase_list_projects,firebase_get_project,firebase_create_project,firebase_create_android_app,firebase_create_ios_app,firebase_get_android_config,firebase_enable_common_services` |
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
65
|
+
| Firebase setup | `select:firebase_list_projects,firebase_get_project,firebase_create_project,firebase_create_android_app,firebase_create_ios_app,firebase_get_android_config,firebase_get_ios_config,firebase_enable_common_services` |
|
|
66
|
+
| Firebase apps + services (incl. web) | `select:firebase_list_android_apps,firebase_list_ios_apps,firebase_list_web_apps,firebase_create_web_app,firebase_get_web_config,firebase_enable_service,firebase_list_enabled_services,firebase_delete_android_app,firebase_delete_ios_app,firebase_delete_web_app` |
|
|
67
|
+
| Analytics wiring (Firebase ↔ GA4 ↔ BigQuery) | `select:firebase_link_analytics,firebase_get_analytics_details,ga4_list_account_summaries,ga4_list_properties,ga4_create_property,ga4_list_data_streams,ga4_create_data_stream,ga4_plan_bigquery_link,ga4_create_bigquery_link,ga4_run_report` |
|
|
68
|
+
| BigQuery | `select:bigquery_auth_status,bigquery_list_datasets,bigquery_list_tables,bigquery_get_table_schema,bigquery_run_query` |
|
|
69
|
+
| AdMob | `select:admob_list_accounts,admob_list_apps,admob_create_app,admob_create_ad_unit,admob_list_ad_units,admob_get_today_earnings,admob_get_report` |
|
|
70
|
+
| Google Ads (UAC) | `select:googleads_config_status,googleads_save_config,googleads_list_accessible_customers,googleads_list_campaigns,googleads_get_campaign_report,googleads_get_uac_report` |
|
|
71
|
+
| Search Console | `select:gsc_list_sites,gsc_list_sitemaps,gsc_get_sitemap,gsc_submit_sitemap,gsc_inspect_url,gsc_search_analytics` |
|
|
72
|
+
| Social posting (Facebook / Instagram / Threads) | `select:facebook_current_config,facebook_save_config,facebook_list_pages,facebook_get_page,facebook_post_photo,facebook_post_multi_photo,instagram_save_config,instagram_get_account,instagram_post_image,instagram_post_carousel,threads_current_config,threads_save_config,threads_refresh_token,threads_get_account,threads_post,threads_post_carousel` |
|
|
73
|
+
| Jenkins credentials + jobs | `select:jenkins_status,jenkins_save_config,jenkins_list_credentials,jenkins_create_credential,jenkins_delete_credential,jenkins_upload_keystore,jenkins_upload_playstore_sa,jenkins_list_jobs,jenkins_get_job_config,jenkins_create_job,jenkins_update_job` |
|
|
74
|
+
| CI (GitHub/GitLab) | `select:ci_save_config,ci_list_workflows,ci_trigger_build,ci_get_build_status,ci_list_recent_builds,ci_cancel_build` |
|
|
75
|
+
| Android signing / keystore | `select:android_signing_setup,android_generate_keystore,jenkins_upload_keystore,jenkins_upload_playstore_sa` |
|
|
76
|
+
| Service account end-to-end | `select:iam_list_service_accounts,iam_create_service_account,iam_list_keys,iam_create_key,iam_add_iam_policy_binding,setup_playstore_connection,playstore_register_service_account,playstore_verify_service_account,playstore_list_service_accounts,playstore_delete_service_account` |
|
|
59
77
|
| Story → researched video | `select:video_plan_from_story,video_research_youtube,video_search_stock_assets,video_synthesize_research,video_download_stock_assets,video_generate_image,video_add_local_asset,video_build_timeline,video_render,video_job_status,video_validate` |
|
|
60
78
|
| YouTube upload / publish | `select:youtube_upload_video,youtube_get_video_status,youtube_update_video_privacy,mimi_seed_auth_start` |
|
|
61
79
|
|
|
@@ -156,7 +174,10 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
|
|
|
156
174
|
| **Jenkins** (credentials + jobs) | `jenkins_status` · `jenkins_save_config` · `jenkins_create_credential` · `jenkins_upload_keystore` · `jenkins_upload_playstore_sa` · `jenkins_create_job` · `jenkins_update_job` |
|
|
157
175
|
| **Google Cloud IAM** | `iam_create_service_account` · `iam_create_key` · `iam_add_iam_policy_binding` |
|
|
158
176
|
| **BigQuery** | `bigquery_run_query` · `bigquery_list_datasets` · `bigquery_get_table_schema` |
|
|
177
|
+
| **GA4** | `ga4_list_properties` · `ga4_create_property` · `ga4_create_data_stream` · `ga4_run_report` · `ga4_plan_bigquery_link` · `ga4_create_bigquery_link` |
|
|
178
|
+
| **Google Ads (UAC)** | `googleads_list_campaigns` · `googleads_get_uac_report` · `googleads_get_campaign_report` · `googleads_config_status` |
|
|
159
179
|
| **Search Console** | `gsc_inspect_url` · `gsc_search_analytics` · `gsc_submit_sitemap` |
|
|
180
|
+
| **Android signing** | `android_signing_setup` · `android_generate_keystore` · `jenkins_upload_playstore_sa` |
|
|
160
181
|
| **Facebook / Instagram / Threads** | `facebook_post_photo` · `instagram_post_carousel` · `threads_post` · `threads_refresh_token` |
|
|
161
182
|
| **Checks** | `playstore_check_submission_risks` · `appstore_check_submission_risks` · `screenshot_validate` · `release_status` |
|
|
162
183
|
| **AI / Auth** | `generate_release_notes_from_commits` · `generate_review_reply` · `mimi_seed_status` · `mimi_seed_auth_start` · `mimi_seed_auth_status` · `mimi_seed_remote_sync_credentials` |
|
|
@@ -183,6 +204,10 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
|
|
|
183
204
|
`playstore_update_release_notes` / `appstore_update_whats_new`.
|
|
184
205
|
|
|
185
206
|
### Story → researched video
|
|
207
|
+
For visual production, use the bundled `video-create-publish` skill. It requires an explicit typography system,
|
|
208
|
+
aspect-ratio-specific human-safe crops, video-native motion, an original-resolution frame review, and a saved
|
|
209
|
+
contact sheet; codec validation alone is not a quality pass.
|
|
210
|
+
|
|
186
211
|
1. `video_plan_from_story` — create the project and scene plan.
|
|
187
212
|
2. `video_research_youtube` — collect reference-only metadata; never treat a result as a render asset.
|
|
188
213
|
3. `video_search_stock_assets` — find licensed Pexels candidates.
|
|
@@ -226,9 +251,9 @@ General rules:
|
|
|
226
251
|
- **Draft-app track constraint (Play).** Until an app has its first non-internal
|
|
227
252
|
publish, only the **`internal`** track can be `completed`. `alpha`/`beta`/`production`
|
|
228
253
|
reject anything but `draft` → error: *"Only releases with status draft may be created
|
|
229
|
-
on draft app."* Closed/open testing also needs the **App Content** declarations
|
|
230
|
-
|
|
231
|
-
|
|
254
|
+
on draft app."* Closed/open testing also needs the **App Content** declarations. **Data safety** is
|
|
255
|
+
uploadable as a CSV (`playstore_upload_data_safety`); **content rating and target
|
|
256
|
+
audience remain Console-only** — no API sets them.
|
|
232
257
|
- **A `403` on one write but not another is usually NOT a permissions gap.** Every
|
|
233
258
|
`playstore_*` write resolves the same credential (`requirePlayStoreAuth`), so if
|
|
234
259
|
`playstore_upload_image` succeeds but `playstore_update_listing` returns `403`, the
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Apple 이 쓰는 빈도 척도. 필드마다 같은 enum 을 쓴다. */
|
|
2
|
+
export type Frequency = 'NONE' | 'INFREQUENT_OR_MILD' | 'FREQUENT_OR_INTENSE' | 'INFREQUENT' | 'FREQUENT';
|
|
3
|
+
export interface AgeRatingDeclaration {
|
|
4
|
+
[key: string]: string | boolean | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function getAgeRating(appId: string): Promise<{
|
|
7
|
+
appInfoId: string;
|
|
8
|
+
declarationId?: string;
|
|
9
|
+
declaration: AgeRatingDeclaration;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* 연령 등급 설문 갱신 (PATCH /v1/ageRatingDeclarations/{id}).
|
|
13
|
+
* 넘긴 필드만 바뀐다 — Apple 이 부분 갱신을 허용하므로 전체를 다시 보낼 필요가 없다.
|
|
14
|
+
*/
|
|
15
|
+
export declare function updateAgeRating(args: {
|
|
16
|
+
appId: string;
|
|
17
|
+
declaration: AgeRatingDeclaration;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
declarationId: string;
|
|
20
|
+
declaration: AgeRatingDeclaration;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* 수출 규정 선언 생성 (POST /v1/appEncryptionDeclarations).
|
|
24
|
+
* buildIds 를 주면 그 빌드들에 바로 연결한다 (POST …/relationships/builds).
|
|
25
|
+
*
|
|
26
|
+
* Info.plist 의 ITSAppUsesNonExemptEncryption 으로 해결되는 경우가 더 많다 —
|
|
27
|
+
* 이 도구는 그게 없어서 ASC 가 선언을 요구할 때의 경로다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function declareEncryption(args: {
|
|
30
|
+
appId: string;
|
|
31
|
+
appDescription: string;
|
|
32
|
+
containsProprietaryCryptography: boolean;
|
|
33
|
+
containsThirdPartyCryptography: boolean;
|
|
34
|
+
availableOnFrenchStore: boolean;
|
|
35
|
+
buildIds?: string[];
|
|
36
|
+
}): Promise<{
|
|
37
|
+
declarationId: string;
|
|
38
|
+
state?: string;
|
|
39
|
+
attachedBuilds: number;
|
|
40
|
+
}>;
|
|
41
|
+
export interface TerritoryRow {
|
|
42
|
+
/** territoryAvailability 리소스 id — PATCH 할 때 그대로 쓴다. */
|
|
43
|
+
id: string;
|
|
44
|
+
available?: boolean;
|
|
45
|
+
releaseDate?: string;
|
|
46
|
+
preOrderEnabled?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 현재 판매 지역 상태 (GET /v1/apps/{id}/appAvailabilityV2 → territoryAvailabilities).
|
|
50
|
+
* 지역이 175개라 기본은 판매 중인 곳만 세고, `includeTerritories` 로 목록을 받는다.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getAvailability(args: {
|
|
53
|
+
appId: string;
|
|
54
|
+
includeTerritories?: boolean;
|
|
55
|
+
limit?: number;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
availabilityId?: string;
|
|
58
|
+
availableInNewTerritories?: boolean;
|
|
59
|
+
availableCount: number;
|
|
60
|
+
unavailableCount: number;
|
|
61
|
+
territories?: TerritoryRow[];
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* 지역별 판매 여부·출시일 변경 (PATCH /v1/territoryAvailabilities/{id}).
|
|
65
|
+
* id 는 appstore_get_availability 결과에서 온다 — 추측해서 만들지 말 것.
|
|
66
|
+
*/
|
|
67
|
+
export declare function setTerritoryAvailability(args: {
|
|
68
|
+
territories: TerritoryRow[];
|
|
69
|
+
}): Promise<Array<{
|
|
70
|
+
id: string;
|
|
71
|
+
ok: boolean;
|
|
72
|
+
error?: string;
|
|
73
|
+
}>>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// 심사 제출 **전에** 채워야 하는 선언들 — 비어 있으면 제출 자체가 막히거나 심사에서 반려된다.
|
|
2
|
+
//
|
|
3
|
+
// 연령 등급 (ageRatingDeclarations) : appInfo 에 딸린 단일 리소스. PATCH 로만 바꾼다
|
|
4
|
+
// 수출 규정 (appEncryptionDeclarations) : 앱 단위로 만들고 빌드에 붙인다
|
|
5
|
+
// 판매 지역 (appAvailabilityV2 / territoryAvailabilities): 지역별 available·출시일
|
|
6
|
+
//
|
|
7
|
+
// Play 쪽 대응물(데이터 안전 CSV)은 playstore/tools.ts 에 있다 — 자격증명 계통이 달라서 파일을 나눴다.
|
|
8
|
+
import { V1_BASE, V2_BASE, apiRequest, authHeadersOrThrow } from './http.js';
|
|
9
|
+
async function get(base, path, params) {
|
|
10
|
+
const headers = await authHeadersOrThrow();
|
|
11
|
+
const query = params ? `?${new URLSearchParams(params).toString()}` : '';
|
|
12
|
+
return apiRequest(base, `${path}${query}`, headers, { method: 'GET' });
|
|
13
|
+
}
|
|
14
|
+
async function send(base, method, path, body) {
|
|
15
|
+
const headers = await authHeadersOrThrow();
|
|
16
|
+
return apiRequest(base, path, headers, {
|
|
17
|
+
method,
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify(body),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** 연령 등급은 appInfo 에 딸려 있다. 편집 가능한 appInfo 를 우선 고른다. */
|
|
23
|
+
async function resolveAppInfoId(appId) {
|
|
24
|
+
const data = await get(V1_BASE, `/apps/${appId}/appInfos`, { 'fields[appInfos]': 'state' });
|
|
25
|
+
const infos = data.data ?? [];
|
|
26
|
+
if (infos.length === 0)
|
|
27
|
+
throw new Error(`앱 ${appId} 의 appInfo 를 찾지 못했다.`);
|
|
28
|
+
const editable = infos.find((i) => {
|
|
29
|
+
const s = i.attributes?.state ?? i.attributes?.appStoreState;
|
|
30
|
+
return s && s !== 'READY_FOR_DISTRIBUTION' && s !== 'REPLACED_WITH_NEW_INFO';
|
|
31
|
+
});
|
|
32
|
+
return (editable ?? infos[0]).id;
|
|
33
|
+
}
|
|
34
|
+
export async function getAgeRating(appId) {
|
|
35
|
+
const appInfoId = await resolveAppInfoId(appId);
|
|
36
|
+
const data = await get(V1_BASE, `/appInfos/${appInfoId}/ageRatingDeclaration`);
|
|
37
|
+
return {
|
|
38
|
+
appInfoId,
|
|
39
|
+
declarationId: data.data?.id,
|
|
40
|
+
declaration: data.data?.attributes ?? {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 연령 등급 설문 갱신 (PATCH /v1/ageRatingDeclarations/{id}).
|
|
45
|
+
* 넘긴 필드만 바뀐다 — Apple 이 부분 갱신을 허용하므로 전체를 다시 보낼 필요가 없다.
|
|
46
|
+
*/
|
|
47
|
+
export async function updateAgeRating(args) {
|
|
48
|
+
const { appId, declaration } = args;
|
|
49
|
+
const attributes = Object.fromEntries(Object.entries(declaration).filter(([, v]) => v !== undefined));
|
|
50
|
+
if (Object.keys(attributes).length === 0) {
|
|
51
|
+
throw new Error('바꿀 항목이 하나도 없다 — declaration 에 최소 한 필드는 넣어야 한다.');
|
|
52
|
+
}
|
|
53
|
+
const current = await getAgeRating(appId);
|
|
54
|
+
if (!current.declarationId) {
|
|
55
|
+
throw new Error([
|
|
56
|
+
'❌ 이 앱에는 연령 등급 선언 리소스가 없다.',
|
|
57
|
+
'App Store Connect 에서 앱을 한 번 연 뒤 다시 시도하거나, appInfo 상태를 확인할 것.',
|
|
58
|
+
].join('\n'));
|
|
59
|
+
}
|
|
60
|
+
await send(V1_BASE, 'PATCH', `/ageRatingDeclarations/${current.declarationId}`, {
|
|
61
|
+
data: { type: 'ageRatingDeclarations', id: current.declarationId, attributes },
|
|
62
|
+
});
|
|
63
|
+
const after = await getAgeRating(appId);
|
|
64
|
+
return { declarationId: current.declarationId, declaration: after.declaration };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 수출 규정 선언 생성 (POST /v1/appEncryptionDeclarations).
|
|
68
|
+
* buildIds 를 주면 그 빌드들에 바로 연결한다 (POST …/relationships/builds).
|
|
69
|
+
*
|
|
70
|
+
* Info.plist 의 ITSAppUsesNonExemptEncryption 으로 해결되는 경우가 더 많다 —
|
|
71
|
+
* 이 도구는 그게 없어서 ASC 가 선언을 요구할 때의 경로다.
|
|
72
|
+
*/
|
|
73
|
+
export async function declareEncryption(args) {
|
|
74
|
+
const { appId, buildIds = [], ...attributes } = args;
|
|
75
|
+
const created = await send(V1_BASE, 'POST', '/appEncryptionDeclarations', {
|
|
76
|
+
data: {
|
|
77
|
+
type: 'appEncryptionDeclarations',
|
|
78
|
+
attributes,
|
|
79
|
+
relationships: { app: { data: { type: 'apps', id: appId } } },
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const declarationId = created.data?.id;
|
|
83
|
+
if (!declarationId)
|
|
84
|
+
throw new Error(`수출 규정 선언 생성 응답에 id 가 없다: ${JSON.stringify(created)}`);
|
|
85
|
+
if (buildIds.length > 0) {
|
|
86
|
+
await send(V1_BASE, 'POST', `/appEncryptionDeclarations/${declarationId}/relationships/builds`, {
|
|
87
|
+
data: buildIds.map((id) => ({ type: 'builds', id })),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
declarationId,
|
|
92
|
+
state: created.data?.attributes?.state,
|
|
93
|
+
attachedBuilds: buildIds.length,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 현재 판매 지역 상태 (GET /v1/apps/{id}/appAvailabilityV2 → territoryAvailabilities).
|
|
98
|
+
* 지역이 175개라 기본은 판매 중인 곳만 세고, `includeTerritories` 로 목록을 받는다.
|
|
99
|
+
*/
|
|
100
|
+
export async function getAvailability(args) {
|
|
101
|
+
const { appId, includeTerritories = false, limit = 200 } = args;
|
|
102
|
+
const av = await get(V1_BASE, `/apps/${appId}/appAvailabilityV2`);
|
|
103
|
+
const availabilityId = av.data?.id;
|
|
104
|
+
if (!availabilityId) {
|
|
105
|
+
return { availableCount: 0, unavailableCount: 0, availableInNewTerritories: undefined };
|
|
106
|
+
}
|
|
107
|
+
const rows = await get(V2_BASE, `/appAvailabilities/${availabilityId}/territoryAvailabilities`, {
|
|
108
|
+
limit: String(Math.min(limit, 200)),
|
|
109
|
+
});
|
|
110
|
+
const list = (rows.data ?? []).map((t) => ({
|
|
111
|
+
id: t.id,
|
|
112
|
+
available: t.attributes?.available,
|
|
113
|
+
releaseDate: t.attributes?.releaseDate,
|
|
114
|
+
preOrderEnabled: t.attributes?.preOrderEnabled,
|
|
115
|
+
}));
|
|
116
|
+
return {
|
|
117
|
+
availabilityId,
|
|
118
|
+
availableInNewTerritories: av.data?.attributes?.availableInNewTerritories,
|
|
119
|
+
availableCount: list.filter((t) => t.available).length,
|
|
120
|
+
unavailableCount: list.filter((t) => t.available === false).length,
|
|
121
|
+
...(includeTerritories ? { territories: list } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* 지역별 판매 여부·출시일 변경 (PATCH /v1/territoryAvailabilities/{id}).
|
|
126
|
+
* id 는 appstore_get_availability 결과에서 온다 — 추측해서 만들지 말 것.
|
|
127
|
+
*/
|
|
128
|
+
export async function setTerritoryAvailability(args) {
|
|
129
|
+
const results = [];
|
|
130
|
+
for (const t of args.territories) {
|
|
131
|
+
const attributes = {};
|
|
132
|
+
if (t.available !== undefined)
|
|
133
|
+
attributes.available = t.available;
|
|
134
|
+
if (t.releaseDate !== undefined)
|
|
135
|
+
attributes.releaseDate = t.releaseDate;
|
|
136
|
+
if (t.preOrderEnabled !== undefined)
|
|
137
|
+
attributes.preOrderEnabled = t.preOrderEnabled;
|
|
138
|
+
if (Object.keys(attributes).length === 0) {
|
|
139
|
+
results.push({ id: t.id, ok: false, error: '바꿀 필드가 없음' });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
await send(V1_BASE, 'PATCH', `/territoryAvailabilities/${t.id}`, {
|
|
144
|
+
data: { type: 'territoryAvailabilities', id: t.id, attributes },
|
|
145
|
+
});
|
|
146
|
+
results.push({ id: t.id, ok: true });
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
// 한 지역 실패가 나머지를 막지 않게 한다 — 어디까지 됐는지 그대로 보고한다.
|
|
150
|
+
results.push({ id: t.id, ok: false, error: err.message });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type AppleReleaseType = 'MANUAL' | 'AFTER_APPROVAL' | 'SCHEDULED';
|
|
2
|
+
export type PhasedReleaseState = 'INACTIVE' | 'ACTIVE' | 'PAUSED' | 'COMPLETE';
|
|
3
|
+
export type PhasedReleaseAction = 'status' | 'enable' | 'pause' | 'resume' | 'complete' | 'disable';
|
|
4
|
+
export interface VersionReleaseSummary {
|
|
5
|
+
versionId: string;
|
|
6
|
+
versionString?: string;
|
|
7
|
+
state?: string;
|
|
8
|
+
releaseType?: string;
|
|
9
|
+
earliestReleaseDate?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PhasedReleaseSummary {
|
|
12
|
+
id: string;
|
|
13
|
+
state?: PhasedReleaseState;
|
|
14
|
+
/** 1~7. Apple 의 7일 램프에서 현재 며칠째인지. */
|
|
15
|
+
currentDayNumber?: number;
|
|
16
|
+
startDate?: string;
|
|
17
|
+
totalPauseDuration?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function getVersionSummary(versionId: string): Promise<VersionReleaseSummary>;
|
|
20
|
+
export declare function stateNote(state?: string): string;
|
|
21
|
+
/** 버전 상태 + 단계적 출시 상태를 한 번에. 쓰기 전 미리보기용. */
|
|
22
|
+
export declare function getReleaseStatus(versionId: string): Promise<{
|
|
23
|
+
version: VersionReleaseSummary;
|
|
24
|
+
phased: PhasedReleaseSummary | null;
|
|
25
|
+
note: string;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* 개발자 출시 대기 중인 버전을 지금 출시한다 (POST /v1/appStoreVersionReleaseRequests).
|
|
29
|
+
* 콘솔의 "이 버전 출시" 버튼과 같은 동작 — 되돌릴 수 없다.
|
|
30
|
+
*/
|
|
31
|
+
export declare function requestRelease(versionId: string): Promise<VersionReleaseSummary>;
|
|
32
|
+
/**
|
|
33
|
+
* 이미 만들어진 버전의 출시 방식을 바꾼다 (PATCH /v1/appStoreVersions/{id}).
|
|
34
|
+
* MANUAL 로 만들어 두고 "역시 승인되면 바로 내보내자"로 바꾸는 경우가 대부분.
|
|
35
|
+
* SCHEDULED 는 earliestReleaseDate(ISO 8601, 미래)가 함께 필요하다.
|
|
36
|
+
*/
|
|
37
|
+
export declare function updateReleaseType(args: {
|
|
38
|
+
versionId: string;
|
|
39
|
+
releaseType: AppleReleaseType;
|
|
40
|
+
earliestReleaseDate?: string;
|
|
41
|
+
}): Promise<VersionReleaseSummary>;
|
|
42
|
+
/**
|
|
43
|
+
* 단계적 출시(7일 램프) 제어.
|
|
44
|
+
*
|
|
45
|
+
* - enable : 없으면 만들고(ACTIVE), PAUSED 면 재개한다
|
|
46
|
+
* - pause : 일시중지 (되돌릴 수 있음)
|
|
47
|
+
* - resume : 재개
|
|
48
|
+
* - complete : 남은 사용자에게 즉시 전체 공개 — 되돌릴 수 없다
|
|
49
|
+
* - disable : 단계적 출시 자체를 제거. 출시 전이면 일반(전체) 출시로, 출시 후면 즉시 전체 공개가 된다
|
|
50
|
+
*/
|
|
51
|
+
export declare function setPhasedRelease(args: {
|
|
52
|
+
versionId: string;
|
|
53
|
+
action: Exclude<PhasedReleaseAction, 'status'>;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
action: string;
|
|
56
|
+
phased: PhasedReleaseSummary | null;
|
|
57
|
+
version: VersionReleaseSummary;
|
|
58
|
+
}>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// App Store 출시 제어 — 심사 통과 **이후**의 단계.
|
|
2
|
+
//
|
|
3
|
+
// 버전 생성 때 releaseType 을 정하는 것(appstore_create_version)까지는 이미 되지만,
|
|
4
|
+
// 그 뒤 세 가지가 API 로 안 됐다:
|
|
5
|
+
// 1. 이미 PENDING_DEVELOPER_RELEASE 로 대기 중인 버전을 "지금 출시" (releaseRequests)
|
|
6
|
+
// 2. 만들어 둔 버전의 releaseType 을 나중에 바꾸기 (PATCH appStoreVersions)
|
|
7
|
+
// 3. 단계적 출시 시작·일시중지·재개·즉시완료 (phasedRelease)
|
|
8
|
+
// Play 는 userFraction/halted 로 3번이 되는데 iOS 만 비어 있었다.
|
|
9
|
+
import { V1_BASE, apiRequest, authHeadersOrThrow } from './http.js';
|
|
10
|
+
/** 각 상태가 "지금 출시" 요청을 받을 수 있는지 + 사람이 읽을 설명. */
|
|
11
|
+
const STATE_NOTE = {
|
|
12
|
+
PENDING_DEVELOPER_RELEASE: '심사 통과 후 개발자 출시 대기 — 지금 출시할 수 있다.',
|
|
13
|
+
PENDING_APPLE_RELEASE: '예약 출시 대기 — Apple 이 earliestReleaseDate 에 출시한다.',
|
|
14
|
+
READY_FOR_SALE: '이미 출시됨.',
|
|
15
|
+
WAITING_FOR_REVIEW: '심사 대기열에 있음.',
|
|
16
|
+
IN_REVIEW: '심사 진행 중.',
|
|
17
|
+
PREPARE_FOR_SUBMISSION: '아직 제출 전 — appstore_submit_for_review 먼저.',
|
|
18
|
+
DEVELOPER_REJECTED: '개발자가 회수함 — 다시 제출해야 한다.',
|
|
19
|
+
REJECTED: 'Apple 이 거절함 — 수정 후 재제출.',
|
|
20
|
+
METADATA_REJECTED: '메타데이터 거절 — 수정 후 재제출.',
|
|
21
|
+
};
|
|
22
|
+
async function get(path, params) {
|
|
23
|
+
const headers = await authHeadersOrThrow();
|
|
24
|
+
const query = params ? `?${new URLSearchParams(params).toString()}` : '';
|
|
25
|
+
return apiRequest(V1_BASE, `${path}${query}`, headers, { method: 'GET' });
|
|
26
|
+
}
|
|
27
|
+
async function send(method, path, body) {
|
|
28
|
+
const headers = await authHeadersOrThrow();
|
|
29
|
+
return apiRequest(V1_BASE, path, headers, {
|
|
30
|
+
method,
|
|
31
|
+
...(body === undefined
|
|
32
|
+
? {}
|
|
33
|
+
: { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function getVersionSummary(versionId) {
|
|
37
|
+
const data = await get(`/appStoreVersions/${versionId}`, { 'fields[appStoreVersions]': 'versionString,appStoreState,releaseType,earliestReleaseDate' });
|
|
38
|
+
const a = data.data?.attributes ?? {};
|
|
39
|
+
return {
|
|
40
|
+
versionId,
|
|
41
|
+
versionString: a.versionString,
|
|
42
|
+
state: a.appStoreState,
|
|
43
|
+
releaseType: a.releaseType,
|
|
44
|
+
earliestReleaseDate: a.earliestReleaseDate,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** to-one 관계는 없을 때 200 + data:null 로 오기도 하고 404 로 오기도 한다. 둘 다 "없음"으로 본다. */
|
|
48
|
+
async function getPhasedRelease(versionId) {
|
|
49
|
+
try {
|
|
50
|
+
const data = await get(`/appStoreVersions/${versionId}/appStoreVersionPhasedRelease`);
|
|
51
|
+
if (!data.data)
|
|
52
|
+
return null;
|
|
53
|
+
const a = (data.data.attributes ?? {});
|
|
54
|
+
return {
|
|
55
|
+
id: data.data.id,
|
|
56
|
+
state: a.phasedReleaseState,
|
|
57
|
+
currentDayNumber: a.currentDayNumber,
|
|
58
|
+
startDate: a.startDate,
|
|
59
|
+
totalPauseDuration: a.totalPauseDuration,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (/404|not found|NOT_FOUND/i.test(err.message))
|
|
64
|
+
return null;
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function stateNote(state) {
|
|
69
|
+
return (state && STATE_NOTE[state]) || '';
|
|
70
|
+
}
|
|
71
|
+
/** 버전 상태 + 단계적 출시 상태를 한 번에. 쓰기 전 미리보기용. */
|
|
72
|
+
export async function getReleaseStatus(versionId) {
|
|
73
|
+
const version = await getVersionSummary(versionId);
|
|
74
|
+
const phased = await getPhasedRelease(versionId);
|
|
75
|
+
return { version, phased, note: stateNote(version.state) };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 개발자 출시 대기 중인 버전을 지금 출시한다 (POST /v1/appStoreVersionReleaseRequests).
|
|
79
|
+
* 콘솔의 "이 버전 출시" 버튼과 같은 동작 — 되돌릴 수 없다.
|
|
80
|
+
*/
|
|
81
|
+
export async function requestRelease(versionId) {
|
|
82
|
+
const version = await getVersionSummary(versionId);
|
|
83
|
+
if (version.state !== 'PENDING_DEVELOPER_RELEASE') {
|
|
84
|
+
throw new Error([
|
|
85
|
+
`❌ 지금 출시할 수 없는 상태다: ${version.state ?? '(알 수 없음)'}`,
|
|
86
|
+
stateNote(version.state),
|
|
87
|
+
'',
|
|
88
|
+
'출시 요청은 심사를 통과해 PENDING_DEVELOPER_RELEASE 로 대기 중인 버전에만 보낼 수 있다.',
|
|
89
|
+
]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join('\n'));
|
|
92
|
+
}
|
|
93
|
+
await send('POST', '/appStoreVersionReleaseRequests', {
|
|
94
|
+
data: {
|
|
95
|
+
type: 'appStoreVersionReleaseRequests',
|
|
96
|
+
relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } } },
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return getVersionSummary(versionId);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 이미 만들어진 버전의 출시 방식을 바꾼다 (PATCH /v1/appStoreVersions/{id}).
|
|
103
|
+
* MANUAL 로 만들어 두고 "역시 승인되면 바로 내보내자"로 바꾸는 경우가 대부분.
|
|
104
|
+
* SCHEDULED 는 earliestReleaseDate(ISO 8601, 미래)가 함께 필요하다.
|
|
105
|
+
*/
|
|
106
|
+
export async function updateReleaseType(args) {
|
|
107
|
+
const { versionId, releaseType, earliestReleaseDate } = args;
|
|
108
|
+
if (releaseType === 'SCHEDULED' && !earliestReleaseDate) {
|
|
109
|
+
throw new Error('releaseType=SCHEDULED 에는 earliestReleaseDate(ISO 8601, 미래 시각)가 필요하다.');
|
|
110
|
+
}
|
|
111
|
+
const attributes = { releaseType };
|
|
112
|
+
// SCHEDULED 가 아닌데 날짜가 남아 있으면 Apple 이 400 을 낸다 — 명시적으로 비운다.
|
|
113
|
+
if (releaseType === 'SCHEDULED')
|
|
114
|
+
attributes.earliestReleaseDate = earliestReleaseDate;
|
|
115
|
+
else if (earliestReleaseDate === undefined)
|
|
116
|
+
attributes.earliestReleaseDate = null;
|
|
117
|
+
await send('PATCH', `/appStoreVersions/${versionId}`, {
|
|
118
|
+
data: { type: 'appStoreVersions', id: versionId, attributes },
|
|
119
|
+
});
|
|
120
|
+
return getVersionSummary(versionId);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 단계적 출시(7일 램프) 제어.
|
|
124
|
+
*
|
|
125
|
+
* - enable : 없으면 만들고(ACTIVE), PAUSED 면 재개한다
|
|
126
|
+
* - pause : 일시중지 (되돌릴 수 있음)
|
|
127
|
+
* - resume : 재개
|
|
128
|
+
* - complete : 남은 사용자에게 즉시 전체 공개 — 되돌릴 수 없다
|
|
129
|
+
* - disable : 단계적 출시 자체를 제거. 출시 전이면 일반(전체) 출시로, 출시 후면 즉시 전체 공개가 된다
|
|
130
|
+
*/
|
|
131
|
+
export async function setPhasedRelease(args) {
|
|
132
|
+
const { versionId, action } = args;
|
|
133
|
+
const version = await getVersionSummary(versionId);
|
|
134
|
+
const current = await getPhasedRelease(versionId);
|
|
135
|
+
if (action === 'enable') {
|
|
136
|
+
if (!current) {
|
|
137
|
+
const created = await send('POST', '/appStoreVersionPhasedReleases', {
|
|
138
|
+
data: {
|
|
139
|
+
type: 'appStoreVersionPhasedReleases',
|
|
140
|
+
attributes: { phasedReleaseState: 'ACTIVE' },
|
|
141
|
+
relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } } },
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
void created;
|
|
145
|
+
}
|
|
146
|
+
else if (current.state === 'PAUSED') {
|
|
147
|
+
await patchState(current.id, 'ACTIVE');
|
|
148
|
+
}
|
|
149
|
+
return { action, phased: await getPhasedRelease(versionId), version };
|
|
150
|
+
}
|
|
151
|
+
if (!current) {
|
|
152
|
+
throw new Error([
|
|
153
|
+
`❌ 이 버전에는 단계적 출시가 설정돼 있지 않다 (버전 상태: ${version.state ?? '알 수 없음'}).`,
|
|
154
|
+
'먼저 action="enable" 로 켜야 한다.',
|
|
155
|
+
].join('\n'));
|
|
156
|
+
}
|
|
157
|
+
if (action === 'disable') {
|
|
158
|
+
await send('DELETE', `/appStoreVersionPhasedReleases/${current.id}`);
|
|
159
|
+
return { action, phased: null, version };
|
|
160
|
+
}
|
|
161
|
+
const next = action === 'pause' ? 'PAUSED' : action === 'resume' ? 'ACTIVE' : 'COMPLETE';
|
|
162
|
+
await patchState(current.id, next);
|
|
163
|
+
return { action, phased: await getPhasedRelease(versionId), version };
|
|
164
|
+
}
|
|
165
|
+
async function patchState(phasedReleaseId, state) {
|
|
166
|
+
await send('PATCH', `/appStoreVersionPhasedReleases/${phasedReleaseId}`, {
|
|
167
|
+
data: {
|
|
168
|
+
type: 'appStoreVersionPhasedReleases',
|
|
169
|
+
id: phasedReleaseId,
|
|
170
|
+
attributes: { phasedReleaseState: state },
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -168,6 +168,11 @@ export declare function updatePurchaseOptionState(auth: OAuth2Client | JWT, pack
|
|
|
168
168
|
state: string | undefined;
|
|
169
169
|
}[];
|
|
170
170
|
}>;
|
|
171
|
+
export declare function uploadDataSafety(auth: OAuth2Client | JWT, packageName: string, safetyLabelsCsv: string): Promise<{
|
|
172
|
+
packageName: string;
|
|
173
|
+
bytes: number;
|
|
174
|
+
lines: number;
|
|
175
|
+
}>;
|
|
171
176
|
export type ServiceAccountVerifyResult = {
|
|
172
177
|
ok: true;
|
|
173
178
|
clientEmail: string;
|
package/dist/playstore/tools.js
CHANGED
|
@@ -650,6 +650,29 @@ export async function updatePurchaseOptionState(auth, packageName, productId, pu
|
|
|
650
650
|
})));
|
|
651
651
|
return { productId, purchaseOptionId, action, states };
|
|
652
652
|
}
|
|
653
|
+
// ─── 인앱 상품 / 구독 생성은 @onesub/providers로 위임 ───
|
|
654
|
+
// IAP·구독 CRUD는 onesub의 도메인 (결제 영역). 이 파일에는 메타·이미지·릴리스·리뷰만 남김.
|
|
655
|
+
// 옛 createOnetimeProduct / createSubscription 구현은 onesub 위임으로 대체됐어 (index.ts 참고).
|
|
656
|
+
// ─── 데이터 안전(Safety Labels) 선언 ───
|
|
657
|
+
//
|
|
658
|
+
// 오랫동안 Console 전용이라고 알려졌지만 API 가 생겼다 (POST applications/{pkg}/dataSafety).
|
|
659
|
+
// 입력은 **Play Console 이 내려주는 CSV 원문**이고, 기존 제출을 통째로 덮어쓴다 —
|
|
660
|
+
// 부분 갱신이 아니므로 항상 최신 전체 CSV 를 보내야 한다.
|
|
661
|
+
// 콘텐츠 등급·타깃 연령 설문은 여전히 API 가 없다 (Console 전용).
|
|
662
|
+
export async function uploadDataSafety(auth, packageName, safetyLabelsCsv) {
|
|
663
|
+
const csv = safetyLabelsCsv.trim();
|
|
664
|
+
if (!csv)
|
|
665
|
+
throw new Error('safetyLabels CSV 가 비어 있다.');
|
|
666
|
+
if (!csv.includes(',')) {
|
|
667
|
+
throw new Error('CSV 로 보이지 않는다 — Play Console 에서 받은 데이터 안전 CSV 원문을 그대로 넣을 것.');
|
|
668
|
+
}
|
|
669
|
+
await publisher().applications.dataSafety({
|
|
670
|
+
auth,
|
|
671
|
+
packageName,
|
|
672
|
+
requestBody: { safetyLabels: csv },
|
|
673
|
+
});
|
|
674
|
+
return { packageName, bytes: Buffer.byteLength(csv, 'utf8'), lines: csv.split('\n').length };
|
|
675
|
+
}
|
|
653
676
|
export async function verifyServiceAccountJson(serviceAccountJson, packageName) {
|
|
654
677
|
let parsed;
|
|
655
678
|
try {
|
|
@@ -3,6 +3,8 @@ import * as appstore from '../appstore/tools.js';
|
|
|
3
3
|
import * as appstoreScreenshots from '../appstore/screenshots.js';
|
|
4
4
|
import * as appstoreProductReview from '../appstore/product-review.js';
|
|
5
5
|
import * as appstoreProductLocalization from '../appstore/product-localization.js';
|
|
6
|
+
import * as appstoreRelease from '../appstore/release.js';
|
|
7
|
+
import * as appstoreDeclarations from '../appstore/declarations.js';
|
|
6
8
|
import { createAppleOneTimePurchase, createAppleSubscription, updateAppleProduct, deleteAppleProduct, listAppleProducts, } from '@onesub/providers';
|
|
7
9
|
import { requireAppStoreCreds } from '../helpers.js';
|
|
8
10
|
import { buildAppStoreReleasePlan } from '../checks/plan.js';
|
|
@@ -869,4 +871,334 @@ export function registerAppstoreTools(server) {
|
|
|
869
871
|
}],
|
|
870
872
|
};
|
|
871
873
|
});
|
|
874
|
+
// ─── 심사 통과 이후: 출시 제어 ───
|
|
875
|
+
// 버전 생성 시 releaseType 을 정하는 것까지는 appstore_create_version 이 한다.
|
|
876
|
+
// 그 뒤 "지금 출시 / 출시 방식 변경 / 단계적 출시" 세 가지가 API 로 안 돼서 콘솔을 열어야 했다.
|
|
877
|
+
server.tool('appstore_release_status', [
|
|
878
|
+
'버전의 출시 상태를 한 번에 읽는다 — 읽기 전용.',
|
|
879
|
+
'appStoreState(PENDING_DEVELOPER_RELEASE / READY_FOR_SALE …), releaseType(MANUAL / AFTER_APPROVAL / SCHEDULED),',
|
|
880
|
+
'earliestReleaseDate, 그리고 단계적 출시가 켜져 있으면 그 상태(ACTIVE/PAUSED/COMPLETE)와 현재 며칠째인지.',
|
|
881
|
+
'출시 관련 쓰기 도구를 부르기 전에 이걸로 먼저 확인할 것.',
|
|
882
|
+
].join(' '), {
|
|
883
|
+
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
|
|
884
|
+
}, async ({ versionId }) => {
|
|
885
|
+
const { version, phased, note } = await appstoreRelease.getReleaseStatus(versionId);
|
|
886
|
+
const lines = [
|
|
887
|
+
`버전 ${version.versionString ?? version.versionId}`,
|
|
888
|
+
` 상태: ${version.state ?? '알 수 없음'}${note ? ` — ${note}` : ''}`,
|
|
889
|
+
` 출시 방식: ${version.releaseType ?? '(미지정 — Apple 기본값)'}`,
|
|
890
|
+
];
|
|
891
|
+
if (version.earliestReleaseDate)
|
|
892
|
+
lines.push(` 예약 시각: ${version.earliestReleaseDate}`);
|
|
893
|
+
if (phased) {
|
|
894
|
+
lines.push(` 단계적 출시: ${phased.state ?? '?'}` +
|
|
895
|
+
(phased.currentDayNumber ? ` (${phased.currentDayNumber}일째/7일)` : '') +
|
|
896
|
+
(phased.startDate ? ` · 시작 ${phased.startDate}` : ''));
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
lines.push(' 단계적 출시: 꺼짐');
|
|
900
|
+
}
|
|
901
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
902
|
+
});
|
|
903
|
+
server.tool('appstore_release_version', [
|
|
904
|
+
'심사를 통과해 개발자 출시 대기(PENDING_DEVELOPER_RELEASE) 중인 버전을 지금 출시한다 — POST /v1/appStoreVersionReleaseRequests.',
|
|
905
|
+
'콘솔의 "이 버전 출시" 버튼과 같은 동작.',
|
|
906
|
+
'⚠️ 비가역: 실행 즉시 App Store 에 공개된다. 되돌리려면 새 버전을 내거나 판매 중단해야 한다.',
|
|
907
|
+
'안전 가드: confirm 생략/false 면 현재 상태만 보여주는 dry-run.',
|
|
908
|
+
'releaseType=AFTER_APPROVAL 로 만든 버전은 승인 시 자동 출시되므로 이 도구가 필요 없다 — MANUAL 로 대기 중인 버전용이다.',
|
|
909
|
+
].join(' '), {
|
|
910
|
+
versionId: z.string().describe('App Store 버전 ID (appstore_list_versions 결과)'),
|
|
911
|
+
confirm: z.boolean().optional().describe('true 명시 시에만 실제 출시. 생략/false 면 dry-run.'),
|
|
912
|
+
}, async ({ versionId, confirm }) => {
|
|
913
|
+
if (!confirm) {
|
|
914
|
+
const { version, phased, note } = await appstoreRelease.getReleaseStatus(versionId);
|
|
915
|
+
const ready = version.state === 'PENDING_DEVELOPER_RELEASE';
|
|
916
|
+
return {
|
|
917
|
+
content: [{
|
|
918
|
+
type: 'text',
|
|
919
|
+
text: [
|
|
920
|
+
'🛑 출시 dry-run — 아직 출시하지 않았다.',
|
|
921
|
+
` 버전: ${version.versionString ?? versionId}`,
|
|
922
|
+
` 상태: ${version.state ?? '알 수 없음'}${note ? ` — ${note}` : ''}`,
|
|
923
|
+
phased ? ` 단계적 출시: ${phased.state ?? '?'}` : ' 단계적 출시: 꺼짐',
|
|
924
|
+
'',
|
|
925
|
+
ready
|
|
926
|
+
? '실제 출시하려면 confirm: true 로 다시 호출. 실행 즉시 공개된다.'
|
|
927
|
+
: '지금은 출시할 수 없는 상태다. PENDING_DEVELOPER_RELEASE 여야 한다.',
|
|
928
|
+
].join('\n'),
|
|
929
|
+
}],
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const after = await appstoreRelease.requestRelease(versionId);
|
|
933
|
+
return {
|
|
934
|
+
content: [{
|
|
935
|
+
type: 'text',
|
|
936
|
+
text: [
|
|
937
|
+
'✅ 출시 요청 전송',
|
|
938
|
+
` 버전: ${after.versionString ?? versionId}`,
|
|
939
|
+
` 상태: ${after.state ?? '조회 실패'}`,
|
|
940
|
+
'App Store 반영에는 보통 수십 분~수 시간이 걸린다. appstore_release_status 로 확인.',
|
|
941
|
+
].join('\n'),
|
|
942
|
+
}],
|
|
943
|
+
};
|
|
944
|
+
});
|
|
945
|
+
server.tool('appstore_update_release_type', [
|
|
946
|
+
'이미 만들어진 버전의 출시 방식을 바꾼다 — PATCH /v1/appStoreVersions/{id}.',
|
|
947
|
+
'MANUAL(개발자가 직접 출시) / AFTER_APPROVAL(승인되면 자동 출시) / SCHEDULED(지정 시각 출시).',
|
|
948
|
+
'SCHEDULED 는 earliestReleaseDate(ISO 8601, 미래 시각)가 함께 필요하다.',
|
|
949
|
+
'편집 가능한 상태에서만 통한다 — 이미 READY_FOR_SALE 이면 바꿀 수 없다.',
|
|
950
|
+
'MANUAL 로 만들어 둔 버전을 "승인되면 알아서 나가게" 바꾸는 용도가 대부분이다.',
|
|
951
|
+
].join(' '), {
|
|
952
|
+
versionId: z.string().describe('App Store 버전 ID'),
|
|
953
|
+
releaseType: z
|
|
954
|
+
.enum(['MANUAL', 'AFTER_APPROVAL', 'SCHEDULED'])
|
|
955
|
+
.describe('출시 방식'),
|
|
956
|
+
earliestReleaseDate: z
|
|
957
|
+
.string()
|
|
958
|
+
.optional()
|
|
959
|
+
.describe('SCHEDULED 일 때 필수. ISO 8601 UTC (예: 2026-08-01T09:00:00Z)'),
|
|
960
|
+
}, async ({ versionId, releaseType, earliestReleaseDate }) => {
|
|
961
|
+
const after = await appstoreRelease.updateReleaseType({ versionId, releaseType, earliestReleaseDate });
|
|
962
|
+
return {
|
|
963
|
+
content: [{
|
|
964
|
+
type: 'text',
|
|
965
|
+
text: [
|
|
966
|
+
'✅ 출시 방식 변경',
|
|
967
|
+
` 버전: ${after.versionString ?? versionId}`,
|
|
968
|
+
` 출시 방식: ${after.releaseType ?? releaseType}`,
|
|
969
|
+
after.earliestReleaseDate ? ` 예약 시각: ${after.earliestReleaseDate}` : '',
|
|
970
|
+
` 상태: ${after.state ?? '알 수 없음'}`,
|
|
971
|
+
].filter(Boolean).join('\n'),
|
|
972
|
+
}],
|
|
973
|
+
};
|
|
974
|
+
});
|
|
975
|
+
server.tool('appstore_phased_release', [
|
|
976
|
+
'iOS 단계적 출시(7일 램프)를 제어한다 — appStoreVersionPhasedReleases.',
|
|
977
|
+
'action: status(조회) / enable(켜기·PAUSED면 재개) / pause(일시중지) / resume(재개) / complete(즉시 전체 공개) / disable(단계적 출시 제거).',
|
|
978
|
+
'Play 의 userFraction·halted 에 해당하는 iOS 쪽 장치다.',
|
|
979
|
+
'⚠️ complete 와 disable 은 남은 사용자 전체에게 즉시 공개되며 되돌릴 수 없다 — confirm: true 필요.',
|
|
980
|
+
'pause/resume/enable 은 되돌릴 수 있어 confirm 없이 실행된다.',
|
|
981
|
+
].join(' '), {
|
|
982
|
+
versionId: z.string().describe('App Store 버전 ID'),
|
|
983
|
+
action: z
|
|
984
|
+
.enum(['status', 'enable', 'pause', 'resume', 'complete', 'disable'])
|
|
985
|
+
.describe('수행할 동작'),
|
|
986
|
+
confirm: z
|
|
987
|
+
.boolean()
|
|
988
|
+
.optional()
|
|
989
|
+
.describe('complete / disable 에만 필요. 생략/false 면 현재 상태만 반환하는 dry-run.'),
|
|
990
|
+
}, async ({ versionId, action, confirm }) => {
|
|
991
|
+
if (action === 'status' || ((action === 'complete' || action === 'disable') && !confirm)) {
|
|
992
|
+
const { version, phased, note } = await appstoreRelease.getReleaseStatus(versionId);
|
|
993
|
+
const header = action === 'status'
|
|
994
|
+
? '단계적 출시 상태'
|
|
995
|
+
: `🛑 ${action} dry-run — 아직 실행하지 않았다.`;
|
|
996
|
+
return {
|
|
997
|
+
content: [{
|
|
998
|
+
type: 'text',
|
|
999
|
+
text: [
|
|
1000
|
+
header,
|
|
1001
|
+
` 버전: ${version.versionString ?? versionId} (${version.state ?? '?'}${note ? ` — ${note}` : ''})`,
|
|
1002
|
+
phased
|
|
1003
|
+
? ` 단계적 출시: ${phased.state ?? '?'}` +
|
|
1004
|
+
(phased.currentDayNumber ? ` (${phased.currentDayNumber}일째/7일)` : '')
|
|
1005
|
+
: ' 단계적 출시: 꺼짐',
|
|
1006
|
+
action === 'status'
|
|
1007
|
+
? ''
|
|
1008
|
+
: '실행하려면 confirm: true 로 다시 호출. 남은 사용자 전체에게 즉시 공개된다.',
|
|
1009
|
+
].filter(Boolean).join('\n'),
|
|
1010
|
+
}],
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const { phased } = await appstoreRelease.setPhasedRelease({ versionId, action });
|
|
1014
|
+
return {
|
|
1015
|
+
content: [{
|
|
1016
|
+
type: 'text',
|
|
1017
|
+
text: [
|
|
1018
|
+
`✅ 단계적 출시: ${action}`,
|
|
1019
|
+
phased ? ` 현재 상태: ${phased.state ?? '?'}` : ' 단계적 출시 제거됨 (전체 공개)',
|
|
1020
|
+
].join('\n'),
|
|
1021
|
+
}],
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
1024
|
+
// ─── 심사 제출 전 선언 ───
|
|
1025
|
+
// 비어 있으면 제출이 막히거나 심사에서 반려되는 항목들. Console 에서만 되는 줄 알았던 것들이다.
|
|
1026
|
+
server.tool('appstore_get_age_rating', [
|
|
1027
|
+
'앱의 연령 등급 설문(ageRatingDeclaration) 현재 값을 읽는다 — 읽기 전용.',
|
|
1028
|
+
'appInfo 에 딸린 단일 리소스이며, 편집 가능한 appInfo 를 자동으로 고른다.',
|
|
1029
|
+
'수정 전에 이걸로 현재 답변을 먼저 확인할 것.',
|
|
1030
|
+
].join(' '), { appId: z.string().describe('App Store 앱 ID (appstore_list_apps 결과)') }, async ({ appId }) => {
|
|
1031
|
+
const { appInfoId, declarationId, declaration } = await appstoreDeclarations.getAgeRating(appId);
|
|
1032
|
+
const filled = Object.entries(declaration).filter(([, v]) => v !== undefined && v !== null);
|
|
1033
|
+
return {
|
|
1034
|
+
content: [{
|
|
1035
|
+
type: 'text',
|
|
1036
|
+
text: [
|
|
1037
|
+
`연령 등급 선언 (appInfo ${appInfoId}${declarationId ? ` · declaration ${declarationId}` : ' · 선언 없음'})`,
|
|
1038
|
+
...filled.map(([k, v]) => ` ${k}: ${v}`),
|
|
1039
|
+
filled.length === 0 ? ' (아직 답변된 항목 없음)' : '',
|
|
1040
|
+
].filter(Boolean).join('\n'),
|
|
1041
|
+
}],
|
|
1042
|
+
};
|
|
1043
|
+
});
|
|
1044
|
+
const FREQ = z.enum(['NONE', 'INFREQUENT_OR_MILD', 'FREQUENT_OR_INTENSE', 'INFREQUENT', 'FREQUENT']);
|
|
1045
|
+
const freq = (what) => FREQ.optional().describe(`${what} — NONE / INFREQUENT_OR_MILD / FREQUENT_OR_INTENSE`);
|
|
1046
|
+
server.tool('appstore_update_age_rating', [
|
|
1047
|
+
'연령 등급 설문을 갱신한다 — PATCH /v1/ageRatingDeclarations/{id}.',
|
|
1048
|
+
'넘긴 필드만 바뀐다 (부분 갱신). 전체를 다시 보낼 필요 없다.',
|
|
1049
|
+
'미완성이면 심사 제출이 막히므로 첫 출시 전 반드시 채워야 한다.',
|
|
1050
|
+
'Play 의 콘텐츠 등급 설문과 달리 Apple 은 이렇게 API 로 답변할 수 있다.',
|
|
1051
|
+
].join(' '), {
|
|
1052
|
+
appId: z.string().describe('App Store 앱 ID'),
|
|
1053
|
+
violenceCartoonOrFantasy: freq('만화/판타지 폭력'),
|
|
1054
|
+
violenceRealistic: freq('사실적 폭력'),
|
|
1055
|
+
violenceRealisticProlongedGraphicOrSadistic: freq('지속적·잔혹한 사실적 폭력'),
|
|
1056
|
+
profanityOrCrudeHumor: freq('욕설/저속한 유머'),
|
|
1057
|
+
matureOrSuggestiveThemes: freq('성인/암시적 주제'),
|
|
1058
|
+
horrorOrFearThemes: freq('공포/공포 유발 주제'),
|
|
1059
|
+
sexualContentOrNudity: freq('성적 콘텐츠 또는 노출'),
|
|
1060
|
+
sexualContentGraphicAndNudity: freq('노골적 성적 콘텐츠·노출'),
|
|
1061
|
+
alcoholTobaccoOrDrugUseOrReferences: freq('음주/흡연/약물'),
|
|
1062
|
+
medicalOrTreatmentInformation: freq('의료/치료 정보'),
|
|
1063
|
+
gamblingSimulated: freq('모의 도박'),
|
|
1064
|
+
contests: freq('경품/콘테스트'),
|
|
1065
|
+
gunsOrOtherWeapons: freq('총기·무기'),
|
|
1066
|
+
gambling: z.boolean().optional().describe('실제 도박 포함 여부'),
|
|
1067
|
+
lootBox: z.boolean().optional().describe('확률형 아이템(루트박스) 포함 여부'),
|
|
1068
|
+
unrestrictedWebAccess: z.boolean().optional().describe('제한 없는 웹 접근'),
|
|
1069
|
+
userGeneratedContent: z.boolean().optional().describe('사용자 생성 콘텐츠'),
|
|
1070
|
+
messagingAndChat: z.boolean().optional().describe('메시지/채팅 기능'),
|
|
1071
|
+
advertising: z.boolean().optional().describe('광고 포함'),
|
|
1072
|
+
healthOrWellnessTopics: z.boolean().optional().describe('건강/웰니스 주제'),
|
|
1073
|
+
parentalControls: z.boolean().optional().describe('자녀 보호 기능 제공'),
|
|
1074
|
+
ageAssurance: z.boolean().optional().describe('연령 확인 장치 제공'),
|
|
1075
|
+
kidsAgeBand: z
|
|
1076
|
+
.enum(['FIVE_AND_UNDER', 'SIX_TO_EIGHT', 'NINE_TO_ELEVEN'])
|
|
1077
|
+
.optional()
|
|
1078
|
+
.describe('키즈 카테고리 대상 연령대 (키즈 앱만)'),
|
|
1079
|
+
ageRatingOverrideV2: z
|
|
1080
|
+
.enum(['NONE', 'NINE_PLUS', 'THIRTEEN_PLUS', 'SIXTEEN_PLUS', 'EIGHTEEN_PLUS', 'UNRATED'])
|
|
1081
|
+
.optional()
|
|
1082
|
+
.describe('산출 등급을 더 높게 덮어쓸 때만'),
|
|
1083
|
+
koreaAgeRatingOverride: z
|
|
1084
|
+
.enum(['NONE', 'FIFTEEN_PLUS', 'NINETEEN_PLUS'])
|
|
1085
|
+
.optional()
|
|
1086
|
+
.describe('한국 등급 별도 지정 (게임물관리위원회 등급을 반영해야 할 때)'),
|
|
1087
|
+
developerAgeRatingInfoUrl: z.string().optional().describe('등급 근거 안내 URL'),
|
|
1088
|
+
}, async ({ appId, ...declaration }) => {
|
|
1089
|
+
const { declarationId, declaration: after } = await appstoreDeclarations.updateAgeRating({
|
|
1090
|
+
appId,
|
|
1091
|
+
declaration: declaration,
|
|
1092
|
+
});
|
|
1093
|
+
const changed = Object.keys(declaration).filter((k) => declaration[k] !== undefined);
|
|
1094
|
+
return {
|
|
1095
|
+
content: [{
|
|
1096
|
+
type: 'text',
|
|
1097
|
+
text: [
|
|
1098
|
+
`✅ 연령 등급 갱신 (${changed.length}개 항목) — declaration ${declarationId}`,
|
|
1099
|
+
...changed.map((k) => ` ${k}: ${String(after[k])}`),
|
|
1100
|
+
].join('\n'),
|
|
1101
|
+
}],
|
|
1102
|
+
};
|
|
1103
|
+
});
|
|
1104
|
+
server.tool('appstore_declare_encryption', [
|
|
1105
|
+
'수출 규정(암호화) 선언을 만들고 선택한 빌드에 연결한다 — POST /v1/appEncryptionDeclarations.',
|
|
1106
|
+
'Info.plist 의 ITSAppUsesNonExemptEncryption 이 있으면 보통 이 선언이 필요 없다 —',
|
|
1107
|
+
'그게 없어서 ASC 가 "수출 규정 정보 누락"으로 제출을 막을 때 쓰는 경로다.',
|
|
1108
|
+
'⚠️ 법적 신고 성격의 선언이다. 값은 반드시 사용자에게 확인받고 넣을 것.',
|
|
1109
|
+
].join(' '), {
|
|
1110
|
+
appId: z.string().describe('App Store 앱 ID'),
|
|
1111
|
+
appDescription: z.string().describe('앱이 사용하는 암호화에 대한 설명 (심사용)'),
|
|
1112
|
+
containsProprietaryCryptography: z.boolean().describe('자체 개발한 암호화를 포함하는가'),
|
|
1113
|
+
containsThirdPartyCryptography: z.boolean().describe('서드파티 암호화를 포함하는가'),
|
|
1114
|
+
availableOnFrenchStore: z.boolean().describe('프랑스 스토어에 배포하는가 (프랑스는 별도 신고 규정)'),
|
|
1115
|
+
buildIds: z
|
|
1116
|
+
.array(z.string())
|
|
1117
|
+
.optional()
|
|
1118
|
+
.describe('이 선언을 연결할 빌드 ID들 (appstore_list_builds 결과). 생략 시 선언만 생성'),
|
|
1119
|
+
}, async ({ appId, buildIds, ...attrs }) => {
|
|
1120
|
+
const r = await appstoreDeclarations.declareEncryption({ appId, buildIds, ...attrs });
|
|
1121
|
+
return {
|
|
1122
|
+
content: [{
|
|
1123
|
+
type: 'text',
|
|
1124
|
+
text: [
|
|
1125
|
+
'✅ 수출 규정 선언 생성',
|
|
1126
|
+
` declarationId: ${r.declarationId}`,
|
|
1127
|
+
` 상태: ${r.state ?? '(응답에 없음)'}`,
|
|
1128
|
+
` 연결된 빌드: ${r.attachedBuilds}개`,
|
|
1129
|
+
r.state === 'IN_REVIEW' ? ' Apple 검토 중 — 승인되면 빌드에 반영된다.' : '',
|
|
1130
|
+
].filter(Boolean).join('\n'),
|
|
1131
|
+
}],
|
|
1132
|
+
};
|
|
1133
|
+
});
|
|
1134
|
+
server.tool('appstore_get_availability', [
|
|
1135
|
+
'앱의 판매 지역 상태를 읽는다 — GET /v1/apps/{id}/appAvailabilityV2.',
|
|
1136
|
+
'판매 중/중지 지역 수와 신규 지역 자동 포함 여부를 보여준다.',
|
|
1137
|
+
'includeTerritories=true 면 지역별 행(id·available·releaseDate)까지 — 이 id 가 변경 도구의 입력이다.',
|
|
1138
|
+
].join(' '), {
|
|
1139
|
+
appId: z.string().describe('App Store 앱 ID'),
|
|
1140
|
+
includeTerritories: z.boolean().optional().describe('지역 목록까지 반환 (기본 false)'),
|
|
1141
|
+
}, async ({ appId, includeTerritories }) => {
|
|
1142
|
+
const r = await appstoreDeclarations.getAvailability({ appId, includeTerritories });
|
|
1143
|
+
const lines = [
|
|
1144
|
+
`판매 지역 (availability ${r.availabilityId ?? '없음'})`,
|
|
1145
|
+
` 판매 중: ${r.availableCount}개 · 중지: ${r.unavailableCount}개`,
|
|
1146
|
+
` 신규 지역 자동 포함: ${r.availableInNewTerritories === undefined ? '알 수 없음' : r.availableInNewTerritories}`,
|
|
1147
|
+
];
|
|
1148
|
+
if (r.territories) {
|
|
1149
|
+
for (const t of r.territories) {
|
|
1150
|
+
lines.push(` ${t.id}: ${t.available ? '판매' : '중지'}` +
|
|
1151
|
+
(t.releaseDate ? ` · 출시일 ${t.releaseDate}` : '') +
|
|
1152
|
+
(t.preOrderEnabled ? ' · 사전주문' : ''));
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1156
|
+
});
|
|
1157
|
+
server.tool('appstore_set_territory_availability', [
|
|
1158
|
+
'지역별 판매 여부·출시일을 바꾼다 — PATCH /v1/territoryAvailabilities/{id}.',
|
|
1159
|
+
'id 는 appstore_get_availability(includeTerritories=true) 결과에서 가져올 것 — 지역 코드를 추측해 넣지 말 것.',
|
|
1160
|
+
'한 지역이 실패해도 나머지는 계속 진행하고, 지역별 성공/실패를 그대로 보고한다.',
|
|
1161
|
+
'⚠️ available=false 는 그 지역에서 앱을 내리는 동작이다.',
|
|
1162
|
+
].join(' '), {
|
|
1163
|
+
territories: z
|
|
1164
|
+
.array(z.object({
|
|
1165
|
+
id: z.string().describe('territoryAvailability 리소스 id'),
|
|
1166
|
+
available: z.boolean().optional().describe('판매 여부'),
|
|
1167
|
+
releaseDate: z.string().optional().describe('출시일 (YYYY-MM-DD)'),
|
|
1168
|
+
preOrderEnabled: z.boolean().optional().describe('사전주문 활성화'),
|
|
1169
|
+
}))
|
|
1170
|
+
.min(1)
|
|
1171
|
+
.describe('변경할 지역 목록'),
|
|
1172
|
+
confirm: z.boolean().optional().describe('true 명시 시에만 실제 변경. 생략/false 면 변경 예정 목록만 반환.'),
|
|
1173
|
+
}, async ({ territories, confirm }) => {
|
|
1174
|
+
if (!confirm) {
|
|
1175
|
+
return {
|
|
1176
|
+
content: [{
|
|
1177
|
+
type: 'text',
|
|
1178
|
+
text: [
|
|
1179
|
+
`🛑 dry-run — ${territories.length}개 지역, 아직 바꾸지 않았다.`,
|
|
1180
|
+
...territories.map((t) => ` ${t.id}: ` +
|
|
1181
|
+
[
|
|
1182
|
+
t.available === undefined ? '' : `판매=${t.available}`,
|
|
1183
|
+
t.releaseDate ? `출시일=${t.releaseDate}` : '',
|
|
1184
|
+
t.preOrderEnabled === undefined ? '' : `사전주문=${t.preOrderEnabled}`,
|
|
1185
|
+
].filter(Boolean).join(' · ')),
|
|
1186
|
+
'',
|
|
1187
|
+
'실행하려면 confirm: true 로 다시 호출.',
|
|
1188
|
+
].join('\n'),
|
|
1189
|
+
}],
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
const results = await appstoreDeclarations.setTerritoryAvailability({ territories });
|
|
1193
|
+
const ok = results.filter((r) => r.ok).length;
|
|
1194
|
+
return {
|
|
1195
|
+
content: [{
|
|
1196
|
+
type: 'text',
|
|
1197
|
+
text: [
|
|
1198
|
+
`지역 변경: 성공 ${ok} / 실패 ${results.length - ok}`,
|
|
1199
|
+
...results.filter((r) => !r.ok).map((r) => ` ✗ ${r.id}: ${r.error}`),
|
|
1200
|
+
].join('\n'),
|
|
1201
|
+
}],
|
|
1202
|
+
};
|
|
1203
|
+
});
|
|
872
1204
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { z } from 'zod';
|
|
2
4
|
import * as playstoreRaw from '../playstore/tools.js';
|
|
3
5
|
import { friendlyPlayError } from '../playstore/errors.js';
|
|
@@ -755,4 +757,56 @@ export function registerPlaystoreTools(server) {
|
|
|
755
757
|
}],
|
|
756
758
|
};
|
|
757
759
|
});
|
|
760
|
+
server.tool('playstore_upload_data_safety', [
|
|
761
|
+
'Play 데이터 안전(Safety Labels) 선언을 업로드한다 — POST applications/{packageName}/dataSafety.',
|
|
762
|
+
'오래 Console 전용으로 알려졌던 항목이지만 API 가 있다.',
|
|
763
|
+
'입력은 Play Console 이 내려주는 **CSV 원문**이다 (Console > 앱 콘텐츠 > 데이터 안전 > 내보내기).',
|
|
764
|
+
'⚠️ 기존 제출을 통째로 덮어쓴다 — 부분 갱신이 아니므로 항상 최신 전체 CSV 를 보낼 것.',
|
|
765
|
+
'콘텐츠 등급·타깃 연령 설문은 여전히 API 가 없다 (Console 전용).',
|
|
766
|
+
].join(' '), {
|
|
767
|
+
packageName: z.string().describe('패키지명 (예: com.example.app)'),
|
|
768
|
+
csvPath: z
|
|
769
|
+
.string()
|
|
770
|
+
.optional()
|
|
771
|
+
.describe('데이터 안전 CSV 파일의 절대경로. csv 와 둘 중 하나는 필수'),
|
|
772
|
+
csv: z.string().optional().describe('CSV 원문 문자열. csvPath 와 둘 중 하나는 필수'),
|
|
773
|
+
confirm: z.boolean().optional().describe('true 명시 시에만 업로드. 생략/false 면 요약만 반환.'),
|
|
774
|
+
}, async ({ packageName, csvPath, csv, confirm }) => {
|
|
775
|
+
if (!csv && !csvPath)
|
|
776
|
+
throw new Error('csvPath 또는 csv 중 하나는 필요하다.');
|
|
777
|
+
if (!csv && csvPath && !path.isAbsolute(csvPath)) {
|
|
778
|
+
throw new Error(`csvPath 는 절대경로여야 한다: ${csvPath}`);
|
|
779
|
+
}
|
|
780
|
+
const content = csv ?? fs.readFileSync(csvPath, 'utf8');
|
|
781
|
+
const lines = content.trim().split('\n');
|
|
782
|
+
if (!confirm) {
|
|
783
|
+
return {
|
|
784
|
+
content: [{
|
|
785
|
+
type: 'text',
|
|
786
|
+
text: [
|
|
787
|
+
'🛑 데이터 안전 업로드 dry-run — 아직 보내지 않았다.',
|
|
788
|
+
` 패키지: ${packageName}`,
|
|
789
|
+
` CSV: ${lines.length}줄 / ${Buffer.byteLength(content, 'utf8')} bytes`,
|
|
790
|
+
` 첫 줄: ${lines[0]?.slice(0, 120) ?? '(비어 있음)'}`,
|
|
791
|
+
'',
|
|
792
|
+
'⚠️ 업로드하면 기존 데이터 안전 제출을 통째로 덮어쓴다.',
|
|
793
|
+
'실행하려면 confirm: true 로 다시 호출.',
|
|
794
|
+
].join('\n'),
|
|
795
|
+
}],
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
const auth = await requirePlayStoreAuth(packageName);
|
|
799
|
+
const r = await playstore.uploadDataSafety(auth, packageName, content);
|
|
800
|
+
return {
|
|
801
|
+
content: [{
|
|
802
|
+
type: 'text',
|
|
803
|
+
text: [
|
|
804
|
+
'✅ 데이터 안전 선언 업로드 완료',
|
|
805
|
+
` 패키지: ${r.packageName}`,
|
|
806
|
+
` 전송: ${r.lines}줄 / ${r.bytes} bytes`,
|
|
807
|
+
'Play Console > 앱 콘텐츠 > 데이터 안전에서 반영을 확인할 것.',
|
|
808
|
+
].join('\n'),
|
|
809
|
+
}],
|
|
810
|
+
};
|
|
811
|
+
});
|
|
758
812
|
}
|
package/package.json
CHANGED
package/tool-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "등록된 MCP 도구 + 도메인 메타데이터(label·credential·summary)의 SSOT. src/__tests__/tool-manifest.test.ts 가 실제 서버 등록 목록과 diff 하고 메타데이터 정합성을 검사한다. 도구 추가/삭제/개명 시 이 파일을 함께 갱신할 것 — 산문 문서에는 정확한 개수를 쓰지 말고 이 파일을 가리킬 것. mimi-seed://tools/catalog 리소스가 이 파일을 그대로 서빙한다.",
|
|
3
|
-
"total":
|
|
3
|
+
"total": 199,
|
|
4
4
|
"domains": {
|
|
5
5
|
"admob": {
|
|
6
6
|
"label": "AdMob",
|
|
@@ -80,7 +80,16 @@
|
|
|
80
80
|
"appstore_remove_review_submission_item",
|
|
81
81
|
"appstore_add_version_to_review_submission",
|
|
82
82
|
"appstore_update_version_string",
|
|
83
|
-
"appstore_cancel_review"
|
|
83
|
+
"appstore_cancel_review",
|
|
84
|
+
"appstore_release_status",
|
|
85
|
+
"appstore_release_version",
|
|
86
|
+
"appstore_update_release_type",
|
|
87
|
+
"appstore_phased_release",
|
|
88
|
+
"appstore_get_age_rating",
|
|
89
|
+
"appstore_update_age_rating",
|
|
90
|
+
"appstore_declare_encryption",
|
|
91
|
+
"appstore_get_availability",
|
|
92
|
+
"appstore_set_territory_availability"
|
|
84
93
|
]
|
|
85
94
|
},
|
|
86
95
|
"auth": {
|
|
@@ -287,7 +296,8 @@
|
|
|
287
296
|
"playstore_update_product_state",
|
|
288
297
|
"playstore_update_product",
|
|
289
298
|
"playstore_delete_product",
|
|
290
|
-
"setup_playstore_connection"
|
|
299
|
+
"setup_playstore_connection",
|
|
300
|
+
"playstore_upload_data_safety"
|
|
291
301
|
]
|
|
292
302
|
},
|
|
293
303
|
"threads": {
|