如何批量修改已授权 X 账号资料
如果你只维护一个 X 账号,手动改账号资料通常更省事。真正麻烦的是一组账号一起改:品牌主号、地区支持号、产品线账号、活动账号,名称和简介要统一,头像和 banner 也不能漏。手动点十几次页面不难,但很容易改错账号、漏掉地区字段,或者把旧活动 banner 留在线上。
TwexAPI 的修改用户资料接口适合处理这种重复任务。比较稳的做法不是一上来就跑完整批次,而是先用一个账号验证字段和图片 URL,再把同一套请求包装成小并发脚本,最后保留每个账号的执行结果。
只为你拥有或已获授权管理的 X 账号使用此流程。账号 Cookie 应当像密码一样保管。
批量执行思路
把账号资料批量修改当成一次受控的维护任务,而不是把账号列表丢进循环里直接跑。接口调用本身很简单:用 TwexAPI Bearer Token 调用 POST https://api.twexapi.io/twitter/profile,传入目标账号的 cookie,并且只传这次允许修改的字段。
一个稳妥的批量任务通常有四条边界:
- 先更新一个低风险账号,并在 X 页面上确认展示结果。
- 每个账号保留一条本地配置,
updates里只放这次要改的字段。 - 用 2 到 3 个 worker 小并发执行,让失败原因更容易追踪。
- 为每个账号保存成功状态、失败原因、重试次数和执行时间。
账号资料修改没有单独的预览端点。实际的 dry run 是先人工复核配置,再只更新一个低风险账号,最后到 X 页面确认展示结果。
API 端点
POST https://api.twexapi.io/twitter/profile
Authorization: Bearer <your_twexapi_token>
Content-Type: application/json请求体支持以下字段:
| 字段 | 是否必填 | 用途 |
|---|---|---|
cookie | 是 | 待修改 X 账号的已登录 Cookie 字符串 |
name | 否 | 展示名称 |
website | 否 | 账号资料中的网站 URL |
description | 否 | 账号简介 |
location | 否 | 账号地区 |
profile_image | 否 | 头像图片 URL |
profile_banner | 否 | Banner 图片 URL |
proxy | 见说明 | 本次请求使用的代理 URL |
接口 schema 中 proxy 是可空字段,但字段说明要求使用住宅代理。批量任务里不要把它当成无关紧要的参数:除非 TwexAPI 支持人员确认你的集成可以省略,否则建议每个账号都配置稳定代理。这样即使某个账号失败,也更容易判断是凭据、代理、图片 URL 还是请求字段的问题。
成功修改后,接口会返回:
{
"code": 200,
"msg": "success",
"data": true
}先修改一个 X 账号
先用一个低风险账号走完整流程。把 TwexAPI Token 放到环境变量中,不要写死在代码里:
export TWEXAPI_TOKEN="your_twexapi_bearer_token"然后只传入这次要修改的字段:
1curl --request POST \
2 --url https://api.twexapi.io/twitter/profile \
3 --header "Authorization: Bearer $TWEXAPI_TOKEN" \
4 --header "Content-Type: application/json" \
5 --data '{
6 "cookie": "auth_token=...; ct0=...; twid=...",
7 "proxy": "http://username:password@proxy.example.com:8000",
8 "name": "Acme Support",
9 "description": "Product help, release notes, and service updates.",
10 "location": "New York",
11 "website": "https://example.com/support",
12 "profile_image": "https://example.com/assets/support-avatar.png",
13 "profile_banner": "https://example.com/assets/support-banner.png"
14 }'这一步不是为了省几行代码,而是为了确认三件事:Cookie 仍然有效,代理能正常请求,图片 URL 可以被公开访问。只有单账号请求成功后,再进入批量脚本。
只传你确实想改的值。例如只更新文字资料时,不要传 profile_image 和 profile_banner。批量任务最怕“顺手带上旧字段”,因为脚本会忠实地把旧值写回去。
准备批量账号配置
创建一个本地 profiles.json 文件。每个对象代表一个账号,updates 里只放这次要改的字段。account_id 只是写入报告的本地标签,不会发送给 API。
1[
2 {
3 "account_id": "acme-support-us",
4 "cookie": "auth_token=...; ct0=...; twid=...",
5 "proxy": "http://username:password@us-proxy.example.com:8000",
6 "updates": {
7 "name": "Acme Support US",
8 "description": "Product help and service updates for US customers.",
9 "location": "United States",
10 "website": "https://example.com/us/support",
11 "profile_image": "https://example.com/assets/us-avatar.png",
12 "profile_banner": "https://example.com/assets/us-banner.png"
13 }
14 },
15 {
16 "account_id": "acme-support-jp",
17 "cookie": "auth_token=...; ct0=...; twid=...",
18 "proxy": "http://username:password@jp-proxy.example.com:8000",
19 "updates": {
20 "name": "Acme Support JP",
21 "description": "Product help and service updates for customers in Japan.",
22 "location": "Japan",
23 "website": "https://example.com/jp/support"
24 }
25 }
26]profiles.json 包含账号凭据,不要把它提交到版本库。如果它必须放在本地项目目录中,请将其加入 .gitignore,并限制文件权限:
chmod 600 profiles.json下面的批量脚本只会发送 updates 中明确存在的键。省略字段,就不会修改现有账号资料。只有当你确实要清空字段,并且已经用单账号验证过行为时,才使用显式 null。
Python 批量修改脚本
下面这个脚本做四件事:校验每个账号是否有 cookie 和 updates,过滤不支持的字段,对临时网络错误做最多 3 次重试,并把每个账号的结果写入 profile-update-results.json。如果环境里还没有 requests,先安装依赖:
python -m pip install requests将下面的脚本保存为 batch-update-profiles.py:
1import json
2import os
3import time
4from concurrent.futures import ThreadPoolExecutor, as_completed
5
6import requests
7
8API_URL = "https://api.twexapi.io/twitter/profile"
9ALLOWED_FIELDS = {
10 "name",
11 "website",
12 "description",
13 "location",
14 "profile_image",
15 "profile_banner",
16}
17MAX_ATTEMPTS = 3
18MAX_WORKERS = 3
19
20token = os.environ.get("TWEXAPI_TOKEN")
21if not token:
22 raise RuntimeError("Missing TWEXAPI_TOKEN environment variable.")
23
24def build_body(account):
25 account_id = account.get("account_id", "unnamed-account")
26 cookie = account.get("cookie")
27 updates = account.get("updates")
28
29 if not cookie:
30 raise ValueError(f"{account_id}: missing cookie")
31 if not isinstance(updates, dict) or not updates:
32 raise ValueError(f"{account_id}: updates must be a non-empty object")
33
34 unknown_fields = sorted(set(updates) - ALLOWED_FIELDS)
35 if unknown_fields:
36 raise ValueError(f"{account_id}: unsupported fields: {unknown_fields}")
37
38 body = {"cookie": cookie, **updates}
39 if account.get("proxy"):
40 body["proxy"] = account["proxy"]
41 return body
42
43def update_profile(account):
44 account_id = account.get("account_id", "unnamed-account")
45
46 try:
47 body = build_body(account)
48 except ValueError as error:
49 return {"account_id": account_id, "ok": False, "error": str(error)}
50
51 for attempt in range(1, MAX_ATTEMPTS + 1):
52 try:
53 response = requests.post(
54 API_URL,
55 headers={"Authorization": f"Bearer {token}"},
56 json=body,
57 timeout=45,
58 )
59 payload = response.json()
60
61 if response.ok and payload.get("code") == 200 and payload.get("data") is True:
62 return {"account_id": account_id, "ok": True, "attempts": attempt}
63
64 message = str(payload.get("msg", f"HTTP {response.status_code}"))
65 retryable = response.status_code == 429 or response.status_code >= 500
66 if not retryable:
67 return {"account_id": account_id, "ok": False, "error": message}
68 except (requests.RequestException, ValueError) as error:
69 message = f"{type(error).__name__}: request failed"
70
71 if attempt < MAX_ATTEMPTS:
72 time.sleep(attempt * 2)
73
74 return {
75 "account_id": account_id,
76 "ok": False,
77 "error": message,
78 "attempts": MAX_ATTEMPTS,
79 }
80
81with open("profiles.json", encoding="utf-8") as source:
82 accounts = json.load(source)
83
84if not isinstance(accounts, list) or not accounts:
85 raise RuntimeError("profiles.json must contain a non-empty array.")
86
87results = []
88with ThreadPoolExecutor(max_workers=min(MAX_WORKERS, len(accounts))) as executor:
89 futures = [executor.submit(update_profile, account) for account in accounts]
90 for future in as_completed(futures):
91 result = future.result()
92 results.append(result)
93 print(f"{result['account_id']}: {'updated' if result['ok'] else 'failed'}")
94
95with open("profile-update-results.json", "w", encoding="utf-8") as output:
96 json.dump(results, output, ensure_ascii=False, indent=2)
97
98success_count = sum(1 for result in results if result["ok"])
99print(f"Finished: {success_count}/{len(results)} profiles updated.")运行脚本:
TWEXAPI_TOKEN="your_twexapi_bearer_token" python batch-update-profiles.py第一次运行时,建议只在 profiles.json 里保留 1 到 2 个账号。确认结果文件、X 页面展示和图片加载都正常后,再放回完整列表。
Node.js 批量修改脚本
如果你的团队更常用 Node.js,可以用下面这个版本。Node.js 18 及以上版本已经内置 fetch,无需额外安装依赖。将脚本保存为 batch-update-profiles.mjs:
1import { readFile, writeFile } from "node:fs/promises";
2
3const API_URL = "https://api.twexapi.io/twitter/profile";
4const ALLOWED_FIELDS = new Set([
5 "name",
6 "website",
7 "description",
8 "location",
9 "profile_image",
10 "profile_banner",
11]);
12const MAX_ATTEMPTS = 3;
13const MAX_WORKERS = 3;
14const token = process.env.TWEXAPI_TOKEN;
15
16if (!token) {
17 throw new Error("Missing TWEXAPI_TOKEN environment variable.");
18}
19
20const sleep = (milliseconds) =>
21 new Promise((resolve) => setTimeout(resolve, milliseconds));
22
23function buildBody(account) {
24 const accountId = account.account_id || "unnamed-account";
25 const updates = account.updates;
26
27 if (!account.cookie) {
28 throw new Error(`${accountId}: missing cookie`);
29 }
30 if (!updates || typeof updates !== "object" || Array.isArray(updates)) {
31 throw new Error(`${accountId}: updates must be a non-empty object`);
32 }
33
34 const updateKeys = Object.keys(updates);
35 if (updateKeys.length === 0) {
36 throw new Error(`${accountId}: updates must be a non-empty object`);
37 }
38
39 const unknownFields = updateKeys.filter((key) => !ALLOWED_FIELDS.has(key));
40 if (unknownFields.length > 0) {
41 throw new Error(`${accountId}: unsupported fields: ${unknownFields.join(", ")}`);
42 }
43
44 return {
45 cookie: account.cookie,
46 ...(account.proxy ? { proxy: account.proxy } : {}),
47 ...updates,
48 };
49}
50
51async function updateProfile(account) {
52 const accountId = account.account_id || "unnamed-account";
53 let body;
54
55 try {
56 body = buildBody(account);
57 } catch (error) {
58 return { account_id: accountId, ok: false, error: error.message };
59 }
60
61 let message = "request failed";
62 for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
63 try {
64 const response = await fetch(API_URL, {
65 method: "POST",
66 headers: {
67 Authorization: `Bearer ${token}`,
68 "Content-Type": "application/json",
69 },
70 body: JSON.stringify(body),
71 signal: AbortSignal.timeout(45_000),
72 });
73 const payload = await response.json().catch(() => ({}));
74
75 if (response.ok && payload.code === 200 && payload.data === true) {
76 return { account_id: accountId, ok: true, attempts: attempt };
77 }
78
79 message = String(payload.msg || `HTTP ${response.status}`);
80 const retryable = response.status === 429 || response.status >= 500;
81 if (!retryable) {
82 return { account_id: accountId, ok: false, error: message };
83 }
84 } catch (error) {
85 message = `${error.name}: request failed`;
86 }
87
88 if (attempt < MAX_ATTEMPTS) {
89 await sleep(attempt * 2_000);
90 }
91 }
92
93 return { account_id: accountId, ok: false, error: message, attempts: MAX_ATTEMPTS };
94}
95
96async function runQueue(accounts, workerCount) {
97 const results = new Array(accounts.length);
98 let cursor = 0;
99
100 async function worker() {
101 while (cursor < accounts.length) {
102 const index = cursor;
103 cursor += 1;
104 results[index] = await updateProfile(accounts[index]);
105 console.log(`${results[index].account_id}: ${results[index].ok ? "updated" : "failed"}`);
106 }
107 }
108
109 await Promise.all(
110 Array.from({ length: Math.min(workerCount, accounts.length) }, () => worker()),
111 );
112 return results;
113}
114
115const accounts = JSON.parse(await readFile("profiles.json", "utf8"));
116if (!Array.isArray(accounts) || accounts.length === 0) {
117 throw new Error("profiles.json must contain a non-empty array.");
118}
119
120const results = await runQueue(accounts, MAX_WORKERS);
121await writeFile("profile-update-results.json", JSON.stringify(results, null, 2));
122
123const successCount = results.filter((result) => result.ok).length;
124console.log(`Finished: ${successCount}/${results.length} profiles updated.`);运行脚本:
TWEXAPI_TOKEN="your_twexapi_bearer_token" node batch-update-profiles.mjs上线前检查清单
- 确认这些账号都由你拥有,或已经获得明确管理授权。
- 把
TWEXAPI_TOKEN放在环境变量或密钥管理系统中,不要写入脚本。 - 把 X Cookie 和带认证信息的代理 URL 放在版本库之外。
- 不要在日志、报错信息或结果报告中输出 Cookie 与代理凭据。
- 先跑 1 个账号,再跑一个小批次,确认无误后才扩大规模。
- 保持较小并发。一次增加太多 worker,会让失败原因更难排查。
- 只对网络异常、HTTP
429和 HTTP5xx做重试;字段错误、凭据错误要先修正再重跑。 - 每次执行后检查
profile-update-results.json,并保留它作为审计记录。 - 为
profile_image和profile_banner使用稳定、可公开访问的图片 URL。