Skip to content

Instantly share code, notes, and snippets.

@rxliuli
Created August 21, 2026 16:36
Show Gist options
  • Select an option

  • Save rxliuli/2042f9c3643075cf67d096eb219b1fad to your computer and use it in GitHub Desktop.

Select an option

Save rxliuli/2042f9c3643075cf67d096eb219b1fad to your computer and use it in GitHub Desktop.
App Store Connect 批量补填 Social Media / Contact 字段

App Store Connect 批量补填元数据

处理两类历史遗留问题:

  • App Age Ratings 的 Social Media 字段:ASC 的年龄评级问卷新增了「Social Media」和「Social Media Disabled for Users Under 13」两个问题,老 app 都没答过(接口里是 null),不填提审会卡。
  • Contact Information:版本页 App Review Information 下的姓名、电话、邮箱,老的版本经常空着。

思路:直接用 ASC 网页端自己的接口(/iris/v1/...)批量检查和补填,不用一个个点界面。操作的都是自己账号下的 app,和手动点网页等效。

用之前

  • 浏览器登录 appstoreconnect.apple.com
  • 账号有 App Manager / Admin / Account Holder 角色(能改 App Information)
  • F12 打开控制台,把脚本粘进去回车;用 AI 助手的,让它在你已登录的浏览器里执行

流程

  1. check.js,看哪些 app 缺字段(只读,不改任何东西)
  2. 改好 fix.js 开头的 FILL 常量(改成你自己的信息),跑它补填。建议先设 ONLY_APP 用一个 app 试跑
  3. verify.js 复查

fix.js 会做什么

对每个活跃 app:

  1. Age rating:找到编辑中的 appInfo(PREPARE_FOR_SUBMISSION / WAITING_FOR_REVIEW 等),把两个 Social Media 字段改成 FILL 里的值
  2. Contact:找编辑中的版本(inflight)。有就直接填;没有就按平台自动建一个 patch 版本(最新版本号 +1,如 0.19.3 → 0.19.4),再填
  3. 已移除的 app(网页里看不到、接口里 removed: true)自动跳过,它们改不了

新建的版本是空的占位版本:没有 build、没有提交。下次发版直接用这个版本传 build 就行。

接口速查

用途 方法 路径
所有 app GET /iris/v1/apps?limit=200
app 的 appInfos(含 age rating) GET /iris/v1/apps/{appId}/appInfos?include=ageRatingDeclaration
app 的版本列表 GET /iris/v1/apps/{appId}/appStoreVersions?limit=20
版本的 age rating GET /iris/v1/appStoreVersions/{versionId}/ageRatingDeclaration
版本的 review detail GET /iris/v1/appStoreVersions/{versionId}/appStoreReviewDetail
改 age rating PATCH /iris/v1/ageRatingDeclarations/{id}
改 contact PATCH /iris/v1/appStoreReviewDetails/{id}
新建版本 POST /iris/v1/appStoreVersions

请求都带 credentials: 'include';POST / PATCH 带 Content-Type: application/json

踩过的坑

  • /iris/v1 是内部接口,苹果不保证稳定,跟着网页前端走。哪天失效了,F12 抓一下页面操作对应的请求,改改路径就行
  • age rating 是 app 级的,contact 是版本级的。新建版本会自动生成空的 ageRatingDeclaration 和 appStoreReviewDetail,不继承旧值
  • 如果你们的 CI 每次都新建版本号而不是复用 inflight 版本,新版本的 contact 是空的,CI 流程里要自己写一次(PATCH /appStoreReviewDetails/{id}
  • reviewDetail 有两种「空」:返回 404(从没创建过,用 POST)和返回 200 但字段为空(已创建,用 PATCH)。fix.js 两种情况都处理了
  • 已移除的 app 改 age rating 会报 409 not editable,是正常的,别管它
// 检查所有 app 的 Age Ratings(Social Media)和 inflight 版本的 Contact Information
// 只读,不改任何东西。用法:浏览器登录 appstoreconnect.apple.com 后,F12 控制台粘贴执行
const API = 'https://appstoreconnect.apple.com/iris/v1';
const H = { 'Accept': 'application/json' };
// 编辑中的版本状态(inflight,能改字段)
const INFLIGHT = new Set([
'PREPARE_FOR_SUBMISSION', 'WAITING_FOR_REVIEW', 'IN_REVIEW',
'REJECTED', 'DEVELOPER_REJECTED', 'ACCEPTED', 'READY_FOR_REVIEW',
]);
async function get(path) {
const res = await fetch(API + path, { credentials: 'include', headers: H });
if (!res.ok) throw new Error(res.status + ' ' + path);
return res.json();
}
(async () => {
const { data: apps } = await get('/apps?limit=200');
const active = apps.filter(a => !a.attributes.removed);
console.log('共 ' + active.length + ' 个活跃 app\n');
for (const app of active) {
console.log('== ' + app.attributes.name + ' [' + app.id + ']');
try {
// 1. Age rating:找编辑中的 appInfo
const ai = await get('/apps/' + app.id + '/appInfos?include=ageRatingDeclaration');
const editable = (ai.data || []).find(i => INFLIGHT.has(i.attributes.state));
const rel = editable && editable.relationships.ageRatingDeclaration.data;
const ard = rel && (ai.included || []).find(i => i.id === rel.id);
const sm = ard && ard.attributes.socialMedia;
const smr = ard && ard.attributes.socialMediaAgeRestricted;
console.log(' Social Media: ' + (sm == null ? '未填' : sm ? 'YES' : 'NO') +
' | Disabled <13: ' + (smr == null ? '未填' : smr ? 'YES' : 'NO'));
// 2. Contact:找 inflight 版本
const vs = await get('/apps/' + app.id + '/appStoreVersions?limit=10');
const inflight = (vs.data || []).filter(v => INFLIGHT.has(v.attributes.appVersionState || v.attributes.appStoreState));
if (inflight.length === 0) {
console.log(' Contact: 无 inflight 版本');
} else {
for (const v of inflight) {
const rd = await get('/appStoreVersions/' + v.id + '/appStoreReviewDetail');
const a = rd.data && rd.data.attributes;
const ok = a && a.contactFirstName && a.contactEmail;
console.log(' Contact [' + v.attributes.platform + ' ' + v.attributes.versionString + ']: ' + (ok ? '已填' : '未填'));
}
}
} catch (e) {
console.log(' 出错: ' + e.message);
}
console.log('');
}
})();
// 补填 Age Ratings(Social Media)和 inflight 版本的 Contact Information
// 用法:浏览器登录 appstoreconnect.apple.com 后,F12 控制台粘贴执行
// 执行前先改 FILL 里的信息,建议先设 ONLY_APP 用一个 app 试跑
const API = 'https://appstoreconnect.apple.com/iris/v1';
const H = { 'Accept': 'application/json', 'Content-Type': 'application/json' };
// 改成你自己的信息。
// socialMedia / socialMediaAgeRestricted:按 app 实际功能,工具类一般选 NO(false)
const FILL = {
socialMedia: false,
socialMediaAgeRestricted: false,
contactFirstName: '你的名',
contactLastName: '你的姓',
contactPhone: '你的电话',
contactEmail: '你的邮箱',
};
// 试跑:只处理这一个 app,跑通后改成 null
const ONLY_APP = null; // 例:'URL Redirector'
const INFLIGHT = new Set([
'PREPARE_FOR_SUBMISSION', 'WAITING_FOR_REVIEW', 'IN_REVIEW',
'REJECTED', 'DEVELOPER_REJECTED', 'ACCEPTED', 'READY_FOR_REVIEW',
]);
async function get(path) {
const res = await fetch(API + path, { credentials: 'include', headers: H });
if (!res.ok) throw new Error(res.status + ' ' + path);
return res.json();
}
async function send(method, path, body) {
const res = await fetch(API + path, {
method,
credentials: 'include',
headers: H,
body: body ? JSON.stringify(body) : undefined,
});
return res;
}
// 版本号 +1 patch:0.19.3 -> 0.19.4
function bump(v) {
const parts = String(v).split('.');
parts[parts.length - 1] = String((parseInt(parts[parts.length - 1]) || 0) + 1);
return parts.join('.');
}
(async () => {
const { data: apps } = await get('/apps?limit=200');
const targets = apps.filter(a => !a.attributes.removed && (!ONLY_APP || a.attributes.name === ONLY_APP));
for (const app of targets) {
console.log('\n== ' + app.attributes.name + ' [' + app.id + ']');
// 1. Age rating:PATCH 编辑中 appInfo 的 ard
try {
const ai = await get('/apps/' + app.id + '/appInfos?include=ageRatingDeclaration');
const editable = (ai.data || []).find(i => INFLIGHT.has(i.attributes.state));
const rel = editable && editable.relationships.ageRatingDeclaration.data;
if (!rel) {
console.log(' age rating: 没有编辑中的 appInfo,跳过');
} else {
const res = await send('PATCH', '/ageRatingDeclarations/' + rel.id, {
data: {
type: 'ageRatingDeclarations',
id: rel.id,
attributes: {
socialMedia: FILL.socialMedia,
socialMediaAgeRestricted: FILL.socialMediaAgeRestricted,
},
},
});
console.log(' age rating: ' + (res.ok ? 'ok' : res.status));
}
} catch (e) {
console.log(' age rating 出错: ' + e.message);
}
// 2. 找 inflight 版本;没有就按平台建 patch 版本
const vs = await get('/apps/' + app.id + '/appStoreVersions?limit=20');
const inflight = (vs.data || []).filter(v => INFLIGHT.has(v.attributes.appVersionState || v.attributes.appStoreState));
if (inflight.length === 0) {
const latest = {};
for (const v of vs.data || []) {
if (!latest[v.attributes.platform]) latest[v.attributes.platform] = v.attributes.versionString;
}
for (const [platform, ver] of Object.entries(latest)) {
const versionString = bump(ver);
const res = await send('POST', '/appStoreVersions', {
data: {
type: 'appStoreVersions',
attributes: { platform, versionString },
relationships: { app: { data: { type: 'apps', id: app.id } } },
},
});
if (res.ok) {
const j = await res.json();
inflight.push({ id: j.data.id, attributes: { platform, versionString, appVersionState: 'PREPARE_FOR_SUBMISSION' } });
console.log(' 新建版本 ' + platform + ' ' + versionString);
} else {
console.log(' 新建版本 ' + platform + ' ' + versionString + ' 失败: ' + res.status);
}
}
}
// 3. 每个 inflight 版本补 contact
const contact = {
contactFirstName: FILL.contactFirstName,
contactLastName: FILL.contactLastName,
contactPhone: FILL.contactPhone,
contactEmail: FILL.contactEmail,
};
for (const v of inflight) {
const tag = v.attributes.platform + ' ' + v.attributes.versionString;
try {
const rdRes = await fetch(API + '/appStoreVersions/' + v.id + '/appStoreReviewDetail', { credentials: 'include', headers: H });
let method, path, body;
if (rdRes.status === 404) {
// 从没建过 review detail:POST 建一个
method = 'POST';
path = '/appStoreReviewDetails';
body = {
data: {
type: 'appStoreReviewDetails',
attributes: contact,
relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: v.id } } },
},
};
} else {
const j = await rdRes.json();
const id = j.data && j.data.id;
if (!id) {
console.log(' ' + tag + ' contact: 没有 review detail,跳过');
continue;
}
method = 'PATCH';
path = '/appStoreReviewDetails/' + id;
body = { data: { type: 'appStoreReviewDetails', id, attributes: contact } };
}
const res = await send(method, path, body);
console.log(' ' + tag + ' contact: ' + (res.ok ? 'ok' : res.status));
} catch (e) {
console.log(' ' + tag + ' contact 出错: ' + e.message);
}
}
}
console.log('\n完成。跑一遍 verify.js 复查。');
})();
// 复查所有活跃 app 的 Age Ratings(Social Media)和 inflight 版本的 Contact Information
// 只读。用法:fix.js 跑完后,F12 控制台粘贴执行
const API = 'https://appstoreconnect.apple.com/iris/v1';
const H = { 'Accept': 'application/json' };
const INFLIGHT = new Set([
'PREPARE_FOR_SUBMISSION', 'WAITING_FOR_REVIEW', 'IN_REVIEW',
'REJECTED', 'DEVELOPER_REJECTED', 'ACCEPTED', 'READY_FOR_REVIEW',
]);
async function get(path) {
const res = await fetch(API + path, { credentials: 'include', headers: H });
if (!res.ok) throw new Error(res.status + ' ' + path);
return res.json();
}
(async () => {
const { data: apps } = await get('/apps?limit=200');
const active = apps.filter(a => !a.attributes.removed);
let bad = 0;
for (const app of active) {
const problems = [];
try {
// age rating
const ai = await get('/apps/' + app.id + '/appInfos?include=ageRatingDeclaration');
const editable = (ai.data || []).find(i => INFLIGHT.has(i.attributes.state));
const rel = editable && editable.relationships.ageRatingDeclaration.data;
const ard = rel && (ai.included || []).find(i => i.id === rel.id);
const sm = ard && ard.attributes.socialMedia;
const smr = ard && ard.attributes.socialMediaAgeRestricted;
if (sm == null || smr == null) problems.push('age rating');
// contact
const vs = await get('/apps/' + app.id + '/appStoreVersions?limit=10');
const inflight = (vs.data || []).filter(v => INFLIGHT.has(v.attributes.appVersionState || v.attributes.appStoreState));
for (const v of inflight) {
const rd = await get('/appStoreVersions/' + v.id + '/appStoreReviewDetail');
const a = rd.data && rd.data.attributes;
if (!(a && a.contactFirstName && a.contactEmail)) {
problems.push('contact [' + v.attributes.platform + ' ' + v.attributes.versionString + ']');
}
}
} catch (e) {
problems.push('出错: ' + e.message);
}
if (problems.length === 0) {
console.log('✓ ' + app.attributes.name);
} else {
bad++;
console.log('✗ ' + app.attributes.name + ' — ' + problems.join(', '));
}
}
console.log(bad === 0 ? '\n全部通过' : '\n还有 ' + bad + ' 个 app 有问题');
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment