@yoonion/mimi-seed-mcp 0.18.1 → 0.19.1
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 +4 -4
- package/assets/agent-guide.md +13 -9
- package/dist/appstore/analytics.d.ts +106 -0
- package/dist/appstore/analytics.js +242 -0
- package/dist/checks/billing.d.ts +30 -0
- package/dist/checks/billing.js +225 -0
- package/dist/checks/risks.d.ts +4 -1
- package/dist/checks/risks.js +55 -5
- package/dist/firebase/remote-config.d.ts +132 -0
- package/dist/firebase/remote-config.js +160 -0
- package/dist/lib/googleapis-lite.d.ts +1 -0
- package/dist/lib/googleapis-lite.js +3 -0
- package/dist/registers/appstore.js +12 -0
- package/dist/registers/checks.js +16 -2
- package/dist/registers/firebase.js +13 -0
- package/dist/registers/video.js +38 -5
- package/dist/video/captions.d.ts +34 -0
- package/dist/video/captions.js +98 -0
- package/dist/video/project.d.ts +19 -2
- package/dist/video/project.js +45 -2
- package/dist/video/render.d.ts +1 -6
- package/dist/video/render.js +9 -38
- package/dist/video/schemas.d.ts +87 -0
- package/dist/video/schemas.js +11 -0
- package/dist/video/types.d.ts +10 -0
- package/package.json +1 -1
- package/tool-manifest.json +9 -5
package/README.md
CHANGED
|
@@ -79,9 +79,9 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
79
79
|
|
|
80
80
|
| 영역 | 도구 수 | 주요 도구 |
|
|
81
81
|
|------|---------|-----------|
|
|
82
|
-
| App Store Connect |
|
|
82
|
+
| App Store Connect | 64 | `appstore_submit_for_review` / `appstore_get_weekly_insight` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
|
|
83
83
|
| Google Play | 39 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
|
|
84
|
-
| Firebase |
|
|
84
|
+
| Firebase | 21 | `firebase_create_project` / `firebase_get_remote_config_overview` / `firebase_get_android_config` / `firebase_create_ios_app` |
|
|
85
85
|
| AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
|
|
86
86
|
| CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
|
|
87
87
|
| Jenkins (크리덴셜 + 잡) | 10 | `jenkins_create_credential` / `jenkins_upload_keystore` / `jenkins_create_job` / `jenkins_update_job` |
|
|
@@ -94,12 +94,12 @@ export ANTHROPIC_API_KEY=sk-ant-...
|
|
|
94
94
|
| BigQuery | 5 | `bigquery_run_query` / `bigquery_list_datasets` / `bigquery_get_table_schema` |
|
|
95
95
|
| Threads | 7 | `threads_post` / `threads_post_video` / `threads_post_carousel` / `threads_refresh_token` |
|
|
96
96
|
| TikTok Business | 7 | `tiktok_business_plan_video_post` / `tiktok_business_publish_video` / `tiktok_business_get_publish_status` |
|
|
97
|
-
| 점검 / 위험 |
|
|
97
|
+
| 점검 / 위험 | 5 | `playstore_check_submission_risks` / `appstore_check_submission_risks` / `android_check_billing_compliance` / `screenshot_validate` / `release_status` |
|
|
98
98
|
| Instagram | 4 | `instagram_post_image` / `instagram_post_carousel` / `instagram_save_config` |
|
|
99
99
|
| Android 서명 | 3 | `android_signing_setup` / `android_generate_keystore` / `jenkins_upload_playstore_sa` |
|
|
100
100
|
| 인증 | 4 | `mimi_seed_status` / `mimi_seed_auth_start` / `mimi_seed_auth_status` / `mimi_seed_remote_sync_credentials` |
|
|
101
101
|
| AI (Claude) | 2 | `generate_release_notes_from_commits` / `generate_review_reply` |
|
|
102
|
-
| 영상 제작·YouTube |
|
|
102
|
+
| 영상 제작·YouTube | 15 | `youtube_upload_video` / `youtube_get_video_status` / `youtube_update_video_privacy` / `video_plan_from_story` / `video_render` |
|
|
103
103
|
|
|
104
104
|
> 인앱 결제(IAP·구독) 도구는 위 Play Store·App Store 카운트에 포함됩니다 — `appstore_create_inapp_purchase` · `appstore_update_product_review_note` · `appstore_upload_product_review_screenshot` 등.
|
|
105
105
|
> 전체 카탈로그(항상 최신): [`docs/domain/tool-catalog.md`](../../docs/domain/tool-catalog.md)
|
package/assets/agent-guide.md
CHANGED
|
@@ -49,6 +49,7 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
|
|
|
49
49
|
|------|--------------------|
|
|
50
50
|
| First contact / "what's connected?" | `select:mimi_seed_status,mimi_seed_auth_status,mimi_seed_auth_start,mimi_seed_remote_sync_credentials` |
|
|
51
51
|
| Release readiness (either store) | `select:release_status,playstore_check_submission_risks,appstore_check_submission_risks,screenshot_validate` |
|
|
52
|
+
| Android Billing compliance | `select:android_check_billing_compliance,playstore_check_submission_risks` |
|
|
52
53
|
| 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
54
|
| 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
55
|
| Play Store reviews + stats | `select:playstore_list_reviews,playstore_reply_review,playstore_get_statistics,generate_review_reply` |
|
|
@@ -67,10 +68,12 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
|
|
|
67
68
|
| Release notes from commits | `select:generate_release_notes_from_commits,playstore_update_release_notes,appstore_update_whats_new` |
|
|
68
69
|
| 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` |
|
|
69
70
|
| 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` |
|
|
71
|
+
| Remote Config usage + experiments | `select:firebase_get_remote_config_overview,gcp_get_billing_info` |
|
|
70
72
|
| 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` |
|
|
71
73
|
| BigQuery | `select:bigquery_auth_status,bigquery_list_datasets,bigquery_list_tables,bigquery_get_table_schema,bigquery_run_query` |
|
|
72
74
|
| GCP billing (Blaze 여부 · 비용 범위 · 예산) | `select:gcp_get_billing_info,gcp_list_billing_projects,gcp_list_budgets,gcp_create_budget` |
|
|
73
75
|
| Real revenue (스토어 정산 원장 — sandbox·테스터가 섞이지 않는 유일한 창구) | `select:appstore_get_sales_report,appstore_get_finance_report,playstore_list_financial_reports,playstore_get_financial_report` |
|
|
76
|
+
| App Store weekly growth insight | `select:appstore_get_weekly_insight,appstore_get_sales_report` |
|
|
74
77
|
| 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` |
|
|
75
78
|
| 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` |
|
|
76
79
|
| Search Console | `select:gsc_list_sites,gsc_list_sitemaps,gsc_get_sitemap,gsc_submit_sitemap,gsc_inspect_url,gsc_search_analytics` |
|
|
@@ -80,7 +83,7 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
|
|
|
80
83
|
| CI (GitHub/GitLab) | `select:ci_save_config,ci_list_workflows,ci_trigger_build,ci_get_build_status,ci_list_recent_builds,ci_cancel_build` |
|
|
81
84
|
| Android signing / keystore | `select:android_signing_setup,android_generate_keystore,jenkins_upload_keystore,jenkins_upload_playstore_sa` |
|
|
82
85
|
| 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` |
|
|
83
|
-
| 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` |
|
|
86
|
+
| Story → researched video | `select:video_save_plan,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` |
|
|
84
87
|
| YouTube upload / publish | `select:youtube_upload_video,youtube_get_video_status,youtube_update_video_privacy,mimi_seed_auth_start` |
|
|
85
88
|
|
|
86
89
|
---
|
|
@@ -176,8 +179,8 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
|
|
|
176
179
|
| Domain | Representative tools |
|
|
177
180
|
|--------|----------------------|
|
|
178
181
|
| **Google Play** | `playstore_get_app` · `playstore_get_listing` · `playstore_update_listing` · `playstore_list_tracks` · `playstore_update_latest_release_notes` · `playstore_promote_release` · `playstore_submit_release` · `playstore_upload_image` · `playstore_replace_images` · `playstore_check_submission_risks` · `playstore_get_statistics` · `playstore_reply_review` · `playstore_register_service_account` |
|
|
179
|
-
| **App Store Connect** | `appstore_list_apps` · `appstore_list_versions` · `appstore_create_version` · `appstore_update_whats_new` · `appstore_update_localization` · `appstore_list_builds` · `appstore_attach_latest_build` · `appstore_upload_screenshot` · `appstore_submit_for_review` · `appstore_cancel_review` · `appstore_list_beta_groups` · `appstore_reply_review` |
|
|
180
|
-
| **Firebase** | `firebase_create_project` · `firebase_create_android_app` · `firebase_create_ios_app` · `firebase_get_android_config` · `firebase_enable_service` · `firebase_enable_common_services` · `firebase_list_*_apps` |
|
|
182
|
+
| **App Store Connect** | `appstore_list_apps` · `appstore_list_versions` · `appstore_create_version` · `appstore_update_whats_new` · `appstore_update_localization` · `appstore_list_builds` · `appstore_attach_latest_build` · `appstore_upload_screenshot` · `appstore_submit_for_review` · `appstore_cancel_review` · `appstore_list_beta_groups` · `appstore_reply_review` · `appstore_get_weekly_insight` |
|
|
183
|
+
| **Firebase** | `firebase_create_project` · `firebase_create_android_app` · `firebase_create_ios_app` · `firebase_get_android_config` · `firebase_enable_service` · `firebase_enable_common_services` · `firebase_get_remote_config_overview` · `firebase_list_*_apps` |
|
|
181
184
|
| **AdMob** | `admob_create_app` · `admob_create_ad_unit` · `admob_list_ad_units` · `admob_get_today_earnings` · `admob_get_report` |
|
|
182
185
|
| **CI/CD** | `ci_trigger_build` · `ci_get_build_status` · `ci_list_workflows` (**GitHub Actions / GitLab only**) |
|
|
183
186
|
| **Jenkins** (credentials + jobs) | `jenkins_status` · `jenkins_save_config` · `jenkins_create_credential` · `jenkins_upload_keystore` · `jenkins_upload_playstore_sa` · `jenkins_create_job` · `jenkins_update_job` |
|
|
@@ -189,9 +192,9 @@ per-domain inventory is [`docs/domain/tool-catalog.md`](domain/tool-catalog.md).
|
|
|
189
192
|
| **Android signing** | `android_signing_setup` · `android_generate_keystore` · `jenkins_upload_playstore_sa` |
|
|
190
193
|
| **Facebook / Instagram / Threads** | `facebook_post_photo` · `instagram_post_carousel` · `threads_post` · `threads_post_video` · `threads_refresh_token` |
|
|
191
194
|
| **TikTok Business** | `tiktok_business_auth_status` · `tiktok_business_plan_video_post` · `tiktok_business_publish_video` · `tiktok_business_get_publish_status` |
|
|
192
|
-
| **Checks** | `playstore_check_submission_risks` · `appstore_check_submission_risks` · `screenshot_validate` · `release_status` |
|
|
195
|
+
| **Checks** | `playstore_check_submission_risks` · `appstore_check_submission_risks` · `android_check_billing_compliance` · `screenshot_validate` · `release_status` |
|
|
193
196
|
| **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` |
|
|
194
|
-
| **Video production** | `youtube_upload_video` · `youtube_get_video_status` · `youtube_update_video_privacy` · `video_plan_from_story` · `video_research_youtube` · `video_search_stock_assets` · `video_synthesize_research` · `video_generate_image` · `video_build_timeline` · `video_render` · `video_job_status` · `video_validate` |
|
|
197
|
+
| **Video production** | `youtube_upload_video` · `youtube_get_video_status` · `youtube_update_video_privacy` · `video_save_plan` · `video_plan_from_story` · `video_research_youtube` · `video_search_stock_assets` · `video_synthesize_research` · `video_generate_image` · `video_build_timeline` · `video_render` · `video_job_status` · `video_validate` |
|
|
195
198
|
|
|
196
199
|
---
|
|
197
200
|
|
|
@@ -218,15 +221,16 @@ For visual production, use the bundled `video-create-publish` skill. It requires
|
|
|
218
221
|
aspect-ratio-specific human-safe crops, video-native motion, an original-resolution frame review, and a saved
|
|
219
222
|
contact sheet; codec validation alone is not a quality pass.
|
|
220
223
|
|
|
221
|
-
1. `
|
|
224
|
+
1. `video_save_plan` — save an agent-authored storyboard (no API key; free-path default). `video_plan_from_story` is the metered alternative when `ANTHROPIC_API_KEY` is configured.
|
|
222
225
|
2. `video_research_youtube` — collect reference-only metadata; never treat a result as a render asset.
|
|
223
226
|
3. `video_search_stock_assets` — find licensed Pexels candidates.
|
|
224
227
|
4. `video_synthesize_research` — combine metadata with any direct human/agent observations. Treat its output as
|
|
225
228
|
metadata-bounded guidance, not proof that the source videos were watched.
|
|
226
229
|
5. Preview then confirm `video_download_stock_assets`; use `video_generate_image(confirm=false)` before any paid
|
|
227
|
-
generation and call it again with `confirm=true` only after approval.
|
|
228
|
-
`
|
|
229
|
-
|
|
230
|
+
generation and call it again with `confirm=true` only after approval. On the free path, generate scene images
|
|
231
|
+
with the local `codex` CLI (ChatGPT subscription) instead and register them — like all user-owned media —
|
|
232
|
+
through `video_add_local_asset` with the ownership/license basis.
|
|
233
|
+
6. `video_build_timeline` — provenance-gated scene assignment. Captions burn in the shorts style (bold outline, `**keyword**` highlight); tune with `captionStyle`.
|
|
230
234
|
7. Preview then confirm `video_render`; poll `video_job_status`, then run `video_validate` on the completed MP4.
|
|
231
235
|
|
|
232
236
|
> **Mimi Seed does not compile app binaries.** It manages metadata, store releases, and
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type ReportRow } from './sales.js';
|
|
2
|
+
interface WeeklyMetrics {
|
|
3
|
+
date: string;
|
|
4
|
+
impressions: number;
|
|
5
|
+
productPageViews: number;
|
|
6
|
+
downloads: number;
|
|
7
|
+
proceeds: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildWeeklyInsight(input: {
|
|
10
|
+
engagement: ReportRow[];
|
|
11
|
+
downloads: ReportRow[];
|
|
12
|
+
purchases: ReportRow[];
|
|
13
|
+
}): {
|
|
14
|
+
status: string;
|
|
15
|
+
weeks: WeeklyMetrics[];
|
|
16
|
+
recommendation: string;
|
|
17
|
+
current?: undefined;
|
|
18
|
+
previous?: undefined;
|
|
19
|
+
changes?: undefined;
|
|
20
|
+
insight?: undefined;
|
|
21
|
+
} | {
|
|
22
|
+
status: string;
|
|
23
|
+
current: WeeklyMetrics;
|
|
24
|
+
previous: WeeklyMetrics;
|
|
25
|
+
changes: {
|
|
26
|
+
impressions: number | null;
|
|
27
|
+
productPageViews: number | null;
|
|
28
|
+
downloads: number | null;
|
|
29
|
+
proceeds: number | null;
|
|
30
|
+
productPageRate: number | null;
|
|
31
|
+
downloadRate: number | null;
|
|
32
|
+
revenuePerDownload: number | null;
|
|
33
|
+
};
|
|
34
|
+
insight: {
|
|
35
|
+
area: string;
|
|
36
|
+
changePercent: number | null;
|
|
37
|
+
recommendation: string;
|
|
38
|
+
};
|
|
39
|
+
weeks?: undefined;
|
|
40
|
+
recommendation?: undefined;
|
|
41
|
+
};
|
|
42
|
+
export declare function getWeeklyInsight(input: {
|
|
43
|
+
appId: string;
|
|
44
|
+
createIfMissing?: boolean;
|
|
45
|
+
confirmCreate?: boolean;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
status: string;
|
|
48
|
+
message: string;
|
|
49
|
+
nextAction: string;
|
|
50
|
+
requestId?: undefined;
|
|
51
|
+
} | {
|
|
52
|
+
status: string;
|
|
53
|
+
message: string;
|
|
54
|
+
nextAction?: undefined;
|
|
55
|
+
requestId?: undefined;
|
|
56
|
+
} | {
|
|
57
|
+
status: string;
|
|
58
|
+
requestId: string;
|
|
59
|
+
message: string;
|
|
60
|
+
nextAction?: undefined;
|
|
61
|
+
} | {
|
|
62
|
+
status: string;
|
|
63
|
+
weeks: WeeklyMetrics[];
|
|
64
|
+
recommendation: string;
|
|
65
|
+
current?: undefined;
|
|
66
|
+
previous?: undefined;
|
|
67
|
+
changes?: undefined;
|
|
68
|
+
insight?: undefined;
|
|
69
|
+
requestId: string;
|
|
70
|
+
reports: {
|
|
71
|
+
engagement: string | null;
|
|
72
|
+
downloads: string | null;
|
|
73
|
+
purchases: string | null;
|
|
74
|
+
};
|
|
75
|
+
message?: undefined;
|
|
76
|
+
nextAction?: undefined;
|
|
77
|
+
} | {
|
|
78
|
+
status: string;
|
|
79
|
+
current: WeeklyMetrics;
|
|
80
|
+
previous: WeeklyMetrics;
|
|
81
|
+
changes: {
|
|
82
|
+
impressions: number | null;
|
|
83
|
+
productPageViews: number | null;
|
|
84
|
+
downloads: number | null;
|
|
85
|
+
proceeds: number | null;
|
|
86
|
+
productPageRate: number | null;
|
|
87
|
+
downloadRate: number | null;
|
|
88
|
+
revenuePerDownload: number | null;
|
|
89
|
+
};
|
|
90
|
+
insight: {
|
|
91
|
+
area: string;
|
|
92
|
+
changePercent: number | null;
|
|
93
|
+
recommendation: string;
|
|
94
|
+
};
|
|
95
|
+
weeks?: undefined;
|
|
96
|
+
recommendation?: undefined;
|
|
97
|
+
requestId: string;
|
|
98
|
+
reports: {
|
|
99
|
+
engagement: string | null;
|
|
100
|
+
downloads: string | null;
|
|
101
|
+
purchases: string | null;
|
|
102
|
+
};
|
|
103
|
+
message?: undefined;
|
|
104
|
+
nextAction?: undefined;
|
|
105
|
+
}>;
|
|
106
|
+
export {};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import zlib from 'node:zlib';
|
|
3
|
+
import { getReportsAuthHeaders } from './auth.js';
|
|
4
|
+
import { V1_BASE, apiRequest, authHeadersOrThrow } from './http.js';
|
|
5
|
+
import { fetchWithTimeout, HTTP_TRANSFER_TIMEOUT_MS } from '../lib/http.js';
|
|
6
|
+
import { parseTsv } from './sales.js';
|
|
7
|
+
const MAX_SEGMENT_BYTES = 50 * 1024 * 1024;
|
|
8
|
+
async function reportHeaders() {
|
|
9
|
+
const auth = await getReportsAuthHeaders();
|
|
10
|
+
if (!auth)
|
|
11
|
+
throw new Error('App Store Connect reportsKey 또는 기본 API 키가 필요합니다.');
|
|
12
|
+
return auth.headers;
|
|
13
|
+
}
|
|
14
|
+
async function reportGet(path, params) {
|
|
15
|
+
const query = params ? `?${new URLSearchParams(params).toString()}` : '';
|
|
16
|
+
return apiRequest(V1_BASE, `${path}${query}`, await reportHeaders(), { method: 'GET' });
|
|
17
|
+
}
|
|
18
|
+
async function createOngoingRequest(appId) {
|
|
19
|
+
const response = await apiRequest(V1_BASE, '/analyticsReportRequests', await authHeadersOrThrow(), {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
data: {
|
|
24
|
+
type: 'analyticsReportRequests',
|
|
25
|
+
attributes: { accessType: 'ONGOING' },
|
|
26
|
+
relationships: { app: { data: { type: 'apps', id: appId } } },
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
return response.data;
|
|
31
|
+
}
|
|
32
|
+
async function downloadSegment(segment) {
|
|
33
|
+
const attributes = segment.attributes ?? {};
|
|
34
|
+
if (!attributes.url)
|
|
35
|
+
return [];
|
|
36
|
+
if ((attributes.sizeInBytes ?? 0) > MAX_SEGMENT_BYTES) {
|
|
37
|
+
throw new Error(`Analytics segment ${segment.id} is larger than the 50 MB safety limit.`);
|
|
38
|
+
}
|
|
39
|
+
const response = await fetchWithTimeout(attributes.url, {}, HTTP_TRANSFER_TIMEOUT_MS);
|
|
40
|
+
if (!response.ok)
|
|
41
|
+
throw new Error(`Analytics segment download failed (${response.status}).`);
|
|
42
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
43
|
+
if (attributes.checksum) {
|
|
44
|
+
const actual = crypto.createHash('md5').update(bytes).digest('hex');
|
|
45
|
+
if (actual !== attributes.checksum.toLowerCase()) {
|
|
46
|
+
throw new Error(`Analytics segment checksum mismatch for ${segment.id}.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let text;
|
|
50
|
+
try {
|
|
51
|
+
text = zlib.gunzipSync(bytes).toString('utf8');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
text = bytes.toString('utf8');
|
|
55
|
+
}
|
|
56
|
+
return parseTsv(text);
|
|
57
|
+
}
|
|
58
|
+
async function rowsForReport(reportId) {
|
|
59
|
+
const instances = await reportGet(`/analyticsReports/${reportId}/instances`, { 'filter[granularity]': 'WEEKLY', limit: '200' });
|
|
60
|
+
const latest = (instances.data ?? [])
|
|
61
|
+
.filter((row) => row.attributes?.processingDate)
|
|
62
|
+
.sort((a, b) => (b.attributes?.processingDate ?? '').localeCompare(a.attributes?.processingDate ?? ''))
|
|
63
|
+
.slice(0, 2);
|
|
64
|
+
const allRows = [];
|
|
65
|
+
for (const instance of latest) {
|
|
66
|
+
const segments = await reportGet(`/analyticsReportInstances/${instance.id}/segments`, { limit: '200' });
|
|
67
|
+
for (const segment of segments.data ?? [])
|
|
68
|
+
allRows.push(...await downloadSegment(segment));
|
|
69
|
+
}
|
|
70
|
+
return allRows;
|
|
71
|
+
}
|
|
72
|
+
function numberFrom(row, names) {
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
const raw = row[name];
|
|
75
|
+
if (raw === undefined || raw === '')
|
|
76
|
+
continue;
|
|
77
|
+
const value = Number.parseFloat(raw.replace(/,/g, ''));
|
|
78
|
+
if (Number.isFinite(value))
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
function dateOf(row) {
|
|
84
|
+
return row.Date || row['Start Date'] || row['Week Start Date'] || null;
|
|
85
|
+
}
|
|
86
|
+
function ensureWeek(map, date) {
|
|
87
|
+
let week = map.get(date);
|
|
88
|
+
if (!week) {
|
|
89
|
+
week = { date, impressions: 0, productPageViews: 0, downloads: 0, proceeds: 0 };
|
|
90
|
+
map.set(date, week);
|
|
91
|
+
}
|
|
92
|
+
return week;
|
|
93
|
+
}
|
|
94
|
+
function percentageChange(current, previous) {
|
|
95
|
+
if (previous <= 0)
|
|
96
|
+
return null;
|
|
97
|
+
return ((current - previous) / previous) * 100;
|
|
98
|
+
}
|
|
99
|
+
export function buildWeeklyInsight(input) {
|
|
100
|
+
const weeks = new Map();
|
|
101
|
+
for (const row of input.engagement) {
|
|
102
|
+
const date = dateOf(row);
|
|
103
|
+
if (!date)
|
|
104
|
+
continue;
|
|
105
|
+
const week = ensureWeek(weeks, date);
|
|
106
|
+
const event = (row.Event ?? '').toLowerCase();
|
|
107
|
+
const pageType = (row['Page Type'] ?? '').toLowerCase();
|
|
108
|
+
const count = numberFrom(row, ['Counts', 'Count']);
|
|
109
|
+
if (event === 'impression')
|
|
110
|
+
week.impressions += count;
|
|
111
|
+
if (event === 'page view' && (pageType === 'product page' || !pageType))
|
|
112
|
+
week.productPageViews += count;
|
|
113
|
+
}
|
|
114
|
+
for (const row of input.downloads) {
|
|
115
|
+
const date = dateOf(row);
|
|
116
|
+
if (!date)
|
|
117
|
+
continue;
|
|
118
|
+
ensureWeek(weeks, date).downloads += numberFrom(row, ['Counts', 'Count', 'Downloads', 'Units']);
|
|
119
|
+
}
|
|
120
|
+
for (const row of input.purchases) {
|
|
121
|
+
const date = dateOf(row);
|
|
122
|
+
if (!date)
|
|
123
|
+
continue;
|
|
124
|
+
ensureWeek(weeks, date).proceeds += numberFrom(row, [
|
|
125
|
+
'Developer Proceeds',
|
|
126
|
+
'Proceeds',
|
|
127
|
+
'Estimated Proceeds',
|
|
128
|
+
'Sales',
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
const ordered = [...weeks.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
132
|
+
if (ordered.length < 2) {
|
|
133
|
+
return {
|
|
134
|
+
status: 'collecting',
|
|
135
|
+
weeks: ordered,
|
|
136
|
+
recommendation: 'Analytics 주간 데이터가 두 기간 이상 쌓인 뒤 다시 실행하세요.',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const previous = ordered.at(-2);
|
|
140
|
+
const current = ordered.at(-1);
|
|
141
|
+
const previousPageRate = previous.impressions > 0 ? previous.productPageViews / previous.impressions : 0;
|
|
142
|
+
const currentPageRate = current.impressions > 0 ? current.productPageViews / current.impressions : 0;
|
|
143
|
+
const previousDownloadRate = previous.productPageViews > 0 ? previous.downloads / previous.productPageViews : 0;
|
|
144
|
+
const currentDownloadRate = current.productPageViews > 0 ? current.downloads / current.productPageViews : 0;
|
|
145
|
+
const previousRevenuePerDownload = previous.downloads > 0 ? previous.proceeds / previous.downloads : 0;
|
|
146
|
+
const currentRevenuePerDownload = current.downloads > 0 ? current.proceeds / current.downloads : 0;
|
|
147
|
+
const candidates = [
|
|
148
|
+
{
|
|
149
|
+
area: 'product_page',
|
|
150
|
+
changePercent: percentageChange(currentPageRate, previousPageRate),
|
|
151
|
+
recommendation: '제품 페이지 진입률이 하락했습니다. 아이콘·스크린샷 첫 장·부제 중 하나를 바꿔 Product Page Optimization 테스트를 시작하세요.',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
area: 'acquisition',
|
|
155
|
+
changePercent: percentageChange(currentDownloadRate, previousDownloadRate),
|
|
156
|
+
recommendation: '제품 페이지 조회 대비 다운로드 전환이 하락했습니다. 유입 소스별 전환을 나누고 가장 큰 하락 소스에 맞춘 커스텀 제품 페이지를 만드세요.',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
area: 'monetization',
|
|
160
|
+
changePercent: percentageChange(currentRevenuePerDownload, previousRevenuePerDownload),
|
|
161
|
+
recommendation: '다운로드당 수익이 하락했습니다. 구매 리포트에서 상품별 하락을 확인하고 가격·오퍼·구독 전환 중 한 가지를 실험하세요.',
|
|
162
|
+
},
|
|
163
|
+
].filter((candidate) => candidate.changePercent !== null)
|
|
164
|
+
.sort((a, b) => (a.changePercent ?? 0) - (b.changePercent ?? 0));
|
|
165
|
+
const selected = candidates[0];
|
|
166
|
+
return {
|
|
167
|
+
status: 'ready',
|
|
168
|
+
current,
|
|
169
|
+
previous,
|
|
170
|
+
changes: {
|
|
171
|
+
impressions: percentageChange(current.impressions, previous.impressions),
|
|
172
|
+
productPageViews: percentageChange(current.productPageViews, previous.productPageViews),
|
|
173
|
+
downloads: percentageChange(current.downloads, previous.downloads),
|
|
174
|
+
proceeds: percentageChange(current.proceeds, previous.proceeds),
|
|
175
|
+
productPageRate: percentageChange(currentPageRate, previousPageRate),
|
|
176
|
+
downloadRate: percentageChange(currentDownloadRate, previousDownloadRate),
|
|
177
|
+
revenuePerDownload: percentageChange(currentRevenuePerDownload, previousRevenuePerDownload),
|
|
178
|
+
},
|
|
179
|
+
insight: selected
|
|
180
|
+
? {
|
|
181
|
+
area: selected.area,
|
|
182
|
+
changePercent: selected.changePercent,
|
|
183
|
+
recommendation: selected.recommendation,
|
|
184
|
+
}
|
|
185
|
+
: {
|
|
186
|
+
area: 'data_quality',
|
|
187
|
+
changePercent: null,
|
|
188
|
+
recommendation: '비교 가능한 분모 데이터가 부족합니다. 표준 Engagement·Downloads·Purchases 리포트가 생성되는지 확인하세요.',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export async function getWeeklyInsight(input) {
|
|
193
|
+
const requests = await reportGet(`/apps/${input.appId}/analyticsReportRequests`, { 'filter[accessType]': 'ONGOING', limit: '10' });
|
|
194
|
+
const active = (requests.data ?? []).find((row) => !row.attributes?.stoppedDueToInactivity);
|
|
195
|
+
if (!active) {
|
|
196
|
+
if (!input.createIfMissing) {
|
|
197
|
+
return {
|
|
198
|
+
status: 'setup_required',
|
|
199
|
+
message: 'ONGOING Analytics report request가 없습니다.',
|
|
200
|
+
nextAction: 'createIfMissing=true와 confirmCreate=true로 다시 호출하세요.',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (!input.confirmCreate) {
|
|
204
|
+
return {
|
|
205
|
+
status: 'confirmation_required',
|
|
206
|
+
message: 'Analytics report request 생성은 App Store Connect에 지속 리소스를 만듭니다.',
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const created = await createOngoingRequest(input.appId);
|
|
210
|
+
return {
|
|
211
|
+
status: 'provisioning',
|
|
212
|
+
requestId: created.id,
|
|
213
|
+
message: 'ONGOING Analytics report request를 만들었습니다. 최초 데이터는 보통 24~48시간 뒤 생성됩니다.',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const reports = await reportGet(`/analyticsReportRequests/${active.id}/reports`, { limit: '200' });
|
|
217
|
+
const standard = (reports.data ?? []).filter((row) => !/Detailed/i.test(row.attributes?.name ?? ''));
|
|
218
|
+
const engagement = standard.find((row) => /Discovery and Engagement/i.test(row.attributes?.name ?? ''));
|
|
219
|
+
const downloads = standard.find((row) => /App Store Downloads/i.test(row.attributes?.name ?? ''));
|
|
220
|
+
const purchases = standard.find((row) => /App Store Purchases/i.test(row.attributes?.name ?? ''));
|
|
221
|
+
if (!engagement && !downloads && !purchases) {
|
|
222
|
+
return {
|
|
223
|
+
status: 'collecting',
|
|
224
|
+
requestId: active.id,
|
|
225
|
+
message: '요청은 활성 상태지만 주간 Engagement·Downloads·Purchases 리포트가 아직 생성되지 않았습니다.',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const [engagementRows, downloadRows, purchaseRows] = await Promise.all([
|
|
229
|
+
engagement ? rowsForReport(engagement.id) : [],
|
|
230
|
+
downloads ? rowsForReport(downloads.id) : [],
|
|
231
|
+
purchases ? rowsForReport(purchases.id) : [],
|
|
232
|
+
]);
|
|
233
|
+
return {
|
|
234
|
+
requestId: active.id,
|
|
235
|
+
reports: {
|
|
236
|
+
engagement: engagement?.attributes?.name ?? null,
|
|
237
|
+
downloads: downloads?.attributes?.name ?? null,
|
|
238
|
+
purchases: purchases?.attributes?.name ?? null,
|
|
239
|
+
},
|
|
240
|
+
...buildWeeklyInsight({ engagement: engagementRows, downloads: downloadRows, purchases: purchaseRows }),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type BillingComplianceStatus = 'pass' | 'warning' | 'blocker' | 'unresolved' | 'not_used';
|
|
2
|
+
export interface BillingEvidence {
|
|
3
|
+
file: string;
|
|
4
|
+
module: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
expression?: string;
|
|
7
|
+
source: 'literal' | 'variable' | 'version_catalog' | 'unresolved';
|
|
8
|
+
}
|
|
9
|
+
export interface BillingComplianceResult {
|
|
10
|
+
projectPath: string;
|
|
11
|
+
checkedAt: string;
|
|
12
|
+
status: BillingComplianceStatus;
|
|
13
|
+
detectedVersions: string[];
|
|
14
|
+
evidence: BillingEvidence[];
|
|
15
|
+
policy: {
|
|
16
|
+
minimumSupportedMajor: number;
|
|
17
|
+
submissionDeadline: string;
|
|
18
|
+
extensionDeadline: string;
|
|
19
|
+
latestKnownMajor: number;
|
|
20
|
+
sourceUrl: string;
|
|
21
|
+
};
|
|
22
|
+
summary: string;
|
|
23
|
+
actions: string[];
|
|
24
|
+
upgrade: {
|
|
25
|
+
installCommand: string;
|
|
26
|
+
prompt: string;
|
|
27
|
+
automaticExecution: false;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function checkBillingCompliance(projectPath: string, now?: Date): Promise<BillingComplianceResult>;
|